Add 'samples/react-cherry-picked-content/' from commit '85f9d532109135b43305e2f00923188c16c04226'

git-subtree-dir: samples/react-cherry-picked-content
git-subtree-mainline: 51eebc82de
git-subtree-split: 85f9d53210
This commit is contained in:
PathToSharePoint 2022-03-09 23:43:24 -08:00
commit 8e5c265a01
47 changed files with 23795 additions and 0 deletions

View File

@ -0,0 +1,33 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
release
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,16 @@
!dist
config
gulpfile.js
release
src
temp
tsconfig.json
tslint.json
*.log
.yo-rc.json
.vscode

View File

@ -0,0 +1,23 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Hosted workbench",
"type": "pwa-chrome",
"request": "launch",
"url": "https://enter-your-SharePoint-site/_layouts/workbench.aspx",
"webRoot": "${workspaceRoot}",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///.././src/*": "${webRoot}/src/*",
"webpack:///../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../../src/*": "${webRoot}/src/*"
},
"runtimeArgs": [
"--remote-debugging-port=9222",
"-incognito"
]
}
]
}

View File

@ -0,0 +1,13 @@
// Place your settings in this file to overwrite default and user settings.
{
// Configure glob patterns for excluding files and folders in the file explorer.
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/bower_components": true,
"**/coverage": true,
"**/lib-amd": true,
"src/**/*.scss.ts": true
},
"typescript.tsdk": ".\\node_modules\\typescript\\lib"
}

View File

@ -0,0 +1,16 @@
{
"@microsoft/generator-sharepoint": {
"plusBeta": false,
"isCreatingSolution": true,
"version": "1.14.0",
"libraryName": "react-cherry-picked-content",
"libraryId": "6a0a135e-c421-4508-955a-dbb0b392104a",
"environment": "spo",
"packageManager": "npm",
"solutionName": "react-cherry-picked-content",
"solutionShortDescription": "react-cherry-picked-content description",
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,98 @@
# react-cherry-picked-content
## Summary
The Cherry=Picked Content Web Part is a modern replacement for the classic Content Editor Web Part, with a twist: code snippets can only be picked from approved document libraries.
![React Cherry=Picked Content Sample](./assets/React-Cherry-Picked-Content-Sample.png)
## Used SharePoint Framework Version
![version](https://img.shields.io/badge/version-1.14.0-green.svg)
## Applies to
- [SharePoint Framework](https://aka.ms/spfx)
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
## Prerequisites
Start by editing the ApprovedLibraries.ts file to list your approved libraries. Then upload your code snippets to those locations. If you are looking for ideas, check out the samples folder.
The code can be rendered in two ways:
- isolated: the code is wrapped in an iframe to prevent conflicts with other Web Parts. Note: this is not a security feature.
- non isolated: the code is directly inserted in the page.
## Solution
Solution|Author(s)
--------|---------
React-Cherry-Picked-Content | [Christophe Humbert](https://github.com/PathToSharePoint)
## Version history
Version|Date|Comments
-------|----|--------
0.3.0|March 9, 2022|4 samples added
0.2.0|March 6, 2022|Refactoring
0.1.0|February 21, 2022|Initial draft
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- Under components, edit ApprovedLibraries.ts to list your approved libraries that contain HTML snippets
- Upload the code snippets
- Ensure that you are at the solution folder
- in the command-line run:
- **npm install**
- **gulp serve**
## Features
This Web Part illustrates the following concepts:
- Cascading dropdown and conditional display in the Property Pane
- Use of SPHttpClient and the SharePoint REST API to query SharePoint content
- React function component with useState and useEffect hooks
- React Portal in combination with an iframe
- Various examples of client-side code in the samples: Microsoft Graph (teams), Microsoft Graph Toolkit (people, email, agenda), charts (Chart.js, Chartist), widgets (map, stock, countdown, clock, video, game), SharePoint SOAP and REST APIs.
## References
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
## Help
We do not support samples, but this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
If you're having issues building the solution, please run [spfx doctor](https://pnp.github.io/cli-microsoft365/cmd/spfx/spfx-doctor/) from within the solution folder to diagnose incompatibility issues with your environment.
You can try looking at [issues related to this sample](https://github.com/pnp/sp-dev-fx-webparts/issues?q=label%3A%22sample%3A%20react-ppw-html%22) to see if anybody else is having the same issues.
You can also try looking at [discussions related to this sample](https://github.com/pnp/sp-dev-fx-webparts/discussions?discussions_q=react-ppw-html) and see what the community is saying.
If you encounter any issues while using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected%2Csample%3A%20react-ppw-html&template=bug-report.yml&sample=react-ppw-html&authors=@PathToSharePoint&title=react-ppw-html%20-%20).
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aquestion%2Csample%3A%20react-ppw-html&template=question.yml&sample=react-ppw-html&authors=@PathToSharePoint&title=react-ppw-html%20-%20).
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aenhancement%2Csample%3A%20react-ppw-html&template=suggestion.yml&sample=react-ppw-html&authors=@PathToSharePoint&title=react-ppw-html%20-%20).
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
<img src="https://pnptelemetry.azurewebsites.net/sp-dev-fx-webparts/samples/react-cherry-picked-content" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"cherry-picked-content-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/cherryPickedContent/CherryPickedContentWebPart.js",
"manifest": "./src/webparts/cherryPickedContent/CherryPickedContentWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"CherryPickedContentWebPartStrings": "lib/webparts/cherryPickedContent/loc/{locale}.js"
}
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./release/assets/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-cherry-picked-content",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,40 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-cherry-picked-content-client-side-solution",
"id": "6a0a135e-c421-4508-955a-dbb0b392104a",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "Christophe Humbert",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": "Undefined-1.14.0"
},
"metadata": {
"shortDescription": {
"default": "react-cherry-picked-content description"
},
"longDescription": {
"default": "react-cherry-picked-content description"
},
"screenshotPaths": [],
"videoUrl": "",
"categories": []
},
"features": [
{
"title": "react-cherry-picked-content Feature",
"description": "The feature that activates elements of the react-cherry-picked-content solution.",
"id": "bb64f400-c482-4467-83ae-3a05b9eacf4d",
"version": "1.0.0.0"
}
]
},
"paths": {
"zippedPackage": "solution/react-cherry-picked-content.sppkg"
}
}

View File

@ -0,0 +1,6 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://enter-your-SharePoint-site/_layouts/workbench.aspx"
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,16 @@
'use strict';
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
var getTasks = build.rig.getTasks;
build.rig.getTasks = function () {
var result = getTasks.call(build.rig);
result.set('serve', result.get('serve-deprecated'));
return result;
};
build.initialize(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
{
"name": "react-cherry-picked-content",
"version": "0.3.0",
"private": true,
"main": "lib/index.js",
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"react": "16.13.1",
"react-dom": "16.13.1",
"office-ui-fabric-react": "7.174.1",
"@microsoft/sp-core-library": "1.14.0",
"@microsoft/sp-property-pane": "1.14.0",
"@microsoft/sp-webpart-base": "1.14.0",
"@microsoft/sp-lodash-subset": "1.14.0",
"@microsoft/sp-office-ui-fabric-core": "1.14.0"
},
"devDependencies": {
"@types/react": "16.9.51",
"@types/react-dom": "16.9.8",
"@microsoft/sp-build-web": "1.14.0",
"@microsoft/sp-tslint-rules": "1.14.0",
"@microsoft/sp-module-interfaces": "1.14.0",
"@microsoft/rush-stack-compiler-3.9": "0.4.47",
"gulp": "~4.0.2",
"ajv": "~5.2.2",
"@types/webpack-env": "1.13.1"
}
}

View File

@ -0,0 +1,79 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Chart.js Bubble Chart</h3>
<p>Isolated mode: &#9888; mandatory. Source: <a href="https://tobiasahlin.com/blog/chartjs-charts-to-get-you-started/" target="_blank">Tobias Ahlin</a>.</p>
<div style="width:500px; height:300px; background-color:#f7f7f7;">
<canvas id="bubble-chart"></canvas>
<div style="padding:10px;">Source: </div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js" onload="drawBubble()"></script>
<script>
function drawBubble() {
new Chart(document.getElementById("bubble-chart"), {
type: 'bubble',
data: {
labels: "Africa",
datasets: [
{
label: ["China"],
backgroundColor: "rgba(255,221,50,0.2)",
borderColor: "rgba(255,221,50,1)",
data: [{
x: 21269017,
y: 5.245,
r: 15
}]
}, {
label: ["Denmark"],
backgroundColor: "rgba(60,186,159,0.2)",
borderColor: "rgba(60,186,159,1)",
data: [{
x: 258702,
y: 7.526,
r: 10
}]
}, {
label: ["Germany"],
backgroundColor: "rgba(0,0,0,0.2)",
borderColor: "#000",
data: [{
x: 3979083,
y: 6.994,
r: 15
}]
}, {
label: ["Japan"],
backgroundColor: "rgba(193,46,12,0.2)",
borderColor: "rgba(193,46,12,1)",
data: [{
x: 4931877,
y: 5.921,
r: 15
}]
}
]
},
options: {
title: {
display: true,
text: 'Predicted world population (millions) in 2050'
}, scales: {
yAxes: [{
scaleLabel: {
display: true,
labelString: "Happiness"
}
}],
xAxes: [{
scaleLabel: {
display: true,
labelString: "GDP (PPP)"
}
}]
}
}
});
}
</script>
</div>

View File

@ -0,0 +1,39 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Chartist Animated Lines</h3>
<p>Isolated mode: &#9888; mandatory. Source: <a href="https://gionkunz.github.io/chartist-js/" target="_blank">Chartist.js</a>.</p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/chartist@0.11.4/dist/chartist.css">
<script src="https://cdn.jsdelivr.net/npm/chartist@0.11.4/dist/chartist.min.js" onload="drawChart()"></script>
<div class="ct-chart ct-perfect-fourth"></div>
</div>
<script>
function drawChart() {
var chart = new Chartist.Line('.ct-chart', {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
series: [
[1, 5, 2, 5, 4, 3],
[2, 3, 4, 8, 1, 2],
[5, 4, 3, 2, 1, 0.5]
]
}, {
low: 0,
showArea: true,
showPoint: false,
fullWidth: true
});
chart.on('draw', function(data) {
if(data.type === 'line' || data.type === 'area') {
data.element.animate({
d: {
begin: 2000 * data.index,
dur: 2000,
from: data.path.clone().scale(1, 0).translate(0, data.chartRect.height()).stringify(),
to: data.path.clone().stringify(),
easing: Chartist.Svg.Easing.easeOutQuint
}
});
}
});
}
</script>
</div>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,49 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Agenda Custom View - Microsoft Graph Toolkit</h3>
<p>Isolated mode: &#9888; mandatory. Find more samples on <a href="https://mgt.dev/?path=/story/overview--page" target="_blank">MGT Playground</a>. MGT components require API permissions, see the <a href="https://docs.microsoft.com/en-us/graph/toolkit/overview">Microsoft docs</a> for more info.</p>
<script>
function setProvider() {mgt.Providers.globalProvider = new mgt.SharePointProvider(props.context);}
</script>
<script src="https://unpkg.com/@microsoft/mgt/dist/bundle/wc/webcomponents-loader.js" type="text/javascript"></script>
<script src="https://unpkg.com/@microsoft/mgt/dist/bundle/mgt.es6.js" type="text/javascript" onload="setProvider()"></script>
<mgt-person person-query="me" view="twoLines"></mgt-person>
<mgt-agenda></mgt-agenda>
<!-- <mgt-agenda show-max="7" days="10">
<template data-type="event">
<div class="root">
<div class="time-container">
<div class="date">{{ dayFromDateTime(event.start.dateTime)}}</div>
<div class="time">{{ timeRangeFromEvent(event, '12') }}</div>
</div>
<div class="separator">
<div class="vertical-line top"></div>
<div class="circle">
<div data-if="!event.bodyPreview.includes('Join Microsoft Teams Meeting')" class="inner-circle">
</div>
</div>
<div class="vertical-line bottom"></div>
</div>
<div class="details">
<div class="subject">{{ event.subject }}</div>
<div class="location" data-if="event.location.displayName">
at
<a href="https://bing.com/maps/default.aspx?where1={{event.location.displayName}}"
target="_blank"><b>{{ event.location.displayName }}</b></a>
</div>
<div class="attendees" data-if="event.attendees.length">
<span class="attendee" data-for="attendee in event.attendees">
<mgt-person person-query="{{attendee.emailAddress.name}}"></mgt-person>
</span>
</div>
<div class="online-meeting" data-if="event.bodyPreview.includes('Join Microsoft Teams Meeting')">
<img class="online-meeting-icon" src="https://img.icons8.com/color/48/000000/microsoft-teams.png" />
<a class="online-meeting-link" href="{{ event.onlineMeetingUrl }}">Join Teams Meeting</a>
</div>
</div>
</div>
</template>
</mgt-agenda> -->
</div>

View File

@ -0,0 +1,70 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Emails Custom View - Microsoft Graph Toolkit</h3>
<p>Isolated mode: &#9888; mandatory. Find more samples on <a href="https://mgt.dev/?path=/story/overview--page" target="_blank">MGT Playground</a>. MGT components require API permissions, see the <a href="https://docs.microsoft.com/en-us/graph/toolkit/overview">Microsoft docs</a> for more info.</p>
<style>
.email {
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
padding: 10px;
margin: 8px 4px;
font-family: Segoe UI, Frutiger, Frutiger Linotype, Dejavu Sans, Helvetica Neue, Arial, sans-serif;
}
.email:hover {
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.3);
padding: 10px;
margin: 8px 4px;
}
.email h3 {
font-size: 12px;
margin-bottom: 4px;
}
.email h4 {
font-size: 10px;
margin-top: 0px;
margin-bottom: 4px;
}
.email mgt-person {
--font-size: 10px;
--avatar-size-s: 12px;
}
.email .preview {
font-size: 13px;
text-overflow: ellipsis;
word-wrap: break-word;
overflow: hidden;
max-height: 2.8em;
line-height: 1.4em;
}
</style>
<script>
function setProvider() {mgt.Providers.globalProvider = new mgt.SharePointProvider(props.context);}
</script>
<script src="https://unpkg.com/@microsoft/mgt/dist/bundle/wc/webcomponents-loader.js" type="text/javascript"></script>
<script src="https://unpkg.com/@microsoft/mgt/dist/bundle/mgt.es6.js" type="text/javascript" onload="setProvider()"></script>
<mgt-person person-query="me" view="twoLines"></mgt-person>
<mgt-get resource="/me/messages" version="beta" scopes="mail.read" max-pages="1">
<template>
<div class="email" data-for="email in value">
<h3>{{ email.subject }}</h3>
<h4>
<mgt-person person-query="{{email.sender.emailAddress.address}}" view="oneline" person-card="hover">
</mgt-person>
</h4>
<div data-if="email.bodyPreview" class="preview" innerHtml>{{email.bodyPreview}}</div>
<div data-else class="preview">
email body is empty
</div>
</div>
</template>
<template data-type="loading">
loading
</template>
<template data-type="error">
{{ this }}
</template>
</mgt-get>
</div>

View File

@ -0,0 +1,25 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>People Components - Microsoft Graph Toolkit</h3>
<p>Isolated mode: &#9888; mandatory. Find more samples on the <a href="https://mgt.dev/?path=/story/overview--page" target="_blank">MGT Playground</a>. MGT components require API permissions, see the <a href="https://docs.microsoft.com/en-us/graph/toolkit/overview">Microsoft docs</a> for more info.</p>
<script>
function setProvider() {mgt.Providers.globalProvider = new mgt.SharePointProvider(props.context);}
</script>
<script src="https://unpkg.com/@microsoft/mgt/dist/bundle/wc/webcomponents-loader.js" type="text/javascript">
</script>
<script src="https://unpkg.com/@microsoft/mgt/dist/bundle/mgt.es6.js" type="text/javascript" onload="setProvider()">
</script>
<div style="display: flex;">
<div style="flex: 50%;">
<h4>mgt-login</h4>
<mgt-login></mgt-login>
<h4>mgt-person</h4>
<mgt-person person-query="me" view="twoLines"></mgt-person>
<h4>mgt-people</h4>
<mgt-people show-max="10"></mgt-people>
</div>
<div style="flex: 50%;">
<h4>mgt-person-card</h4>
<mgt-person-card person-query="me"></mgt-person-card>
</div>
</div>
</div>

View File

@ -0,0 +1,23 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>My Teams - MS Graph Client</h3>
<p>Isolated mode: &#9888; mandatory. This sample leverages the SPFx built-in Microsoft Graph client.</p>
<script>
var logoURL = "https://static2.sharepointonline.com/files/fabric-cdn-prod_20200430.002/assets/brand-icons/product/svg/teams_48x1.svg";
var currentScript = document.currentScript;
props.context.msGraphClientFactory
.getClient()
.then((client) => {
client
.api("me/joinedTeams")
.version("v1.0")
.get((err, res) => {
if (res) {
let items = res.value.map(team => `<li><img src="${logoURL}" style="width:24px;"/> ${team.displayName}</li>`);
const newDiv = document.createElement('div');
newDiv.innerHTML = `<div>You are a member of ${items.length} teams:</div><ul style="list-style-type: none; columns: 3;">${items.join("")}</ul>`;
currentScript.insertAdjacentElement('afterend', newDiv);
}
});
});
</script>
</div>

View File

@ -0,0 +1,16 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Site Lists - SharePoint REST API</h3>
<p>Isolated mode: &#9888; mandatory. This sample uses the JavaScript fetch method to query the SharePoint REST API.</p>
<script>
var currentScript = document.currentScript;
fetch(props.context.pageContext.web.absoluteUrl + "/_api/web/lists?$select=Title,ImageUrl", {headers: {'Accept': 'application/json'}})
.then(response => response.json())
.then(result => result.value)
.then(lists => lists.map(list => `<li><img src="${list.ImageUrl}"/> ${list.Title}</li>`))
.then(items => {
const newDiv = document.createElement('div');
newDiv.innerHTML = `<div>${items.length} lists found on this site:</div><ul style="list-style-type: none; columns: 2;">${items.join("")}</ul>`;
currentScript.insertAdjacentElement('afterend', newDiv);
});
</script>
</div>

View File

@ -0,0 +1,30 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>TradingView Widget</h3>
<p>Isolated mode: recommended. Source: <a href="https://www.tradingview.com/widget/" target="_blank">TradingView widget</a>.</p>
<!-- TradingView Widget BEGIN -->
<div class="tradingview-widget-container">
<div id="tradingview_cd1aa"></div>
<div class="tradingview-widget-copyright"><a href="https://www.tradingview.com/symbols/NASDAQ-MSFT/" rel="noopener" target="_blank"><span class="blue-text">MSFT Chart</span></a> by TradingView</div>
<script type="text/javascript" src="https://s3.tradingview.com/tv.js" onload="drawChart()"></script>
<script type="text/javascript">
function drawChart() {
new TradingView.widget(
{
"autosize": true,
"symbol": "NASDAQ:MSFT",
"interval": "D",
"timezone": "Etc/UTC",
"theme": "light",
"style": "1",
"locale": "en",
"toolbar_bg": "#f1f3f6",
"enable_publishing": false,
"allow_symbol_change": true,
"container_id": "tradingview_cd1aa"
}
);
}
</script>
</div>
<!-- TradingView Widget END -->
</div>

View File

@ -0,0 +1,92 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Site Lists - pocketSOAP</h3>
<p>Isolated mode: &#9888; mandatory. Just for fun, a minimal implementation of the deprecated SharePoint SOAP API. Do not use in production!</p>
<script type="text/javascript">
/********************************************************************************
pocketSOAP
Copyright (c) 2009-2022 Christophe Humbert
********************************************************************************/
pS = {};
pS.cleanSpaces = function (string) { return string.replace(/^\s+|\s+$/g, "").replace(/\s+/g, " "); };
pS.cleanBreaks = function (string) { return string.replace(/[\r\n\t]/g, ""); };
pS.trim = function (string) { return string.replace(/^\s+|\s+$/g, ""); };
pS.htmlUnescape = function (str) {
return String(str)
.replace(/&quot;/g, '"')
.replace(/&#39;/g, '\'')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&');
};
pS.getWSProperties = function (site, service, operation) {
return fetch(site + "/" + '_vti_bin/' + service + '.asmx' + '?op=' + operation + '&_ts=' + new Date().getTime())
.then(result => result.text())
.then(text => {
var regexp = /Content-Type:([\s\S]*?)Content-Length:[\s\S]*?SOAPAction:\s*"(\S*?)"([\s\S]*?)\<\/pre\>/;
var results = regexp.exec(text);
let dv = document.createElement("div");
var spWS = {};
spWS.headers = {};
spWS.headers['Content-Type'] = pS.trim(results[1]).replace(/[\r\n]/g, "");
spWS.headers.SOAPAction = pS.trim(results[2]).replace(/[\r\n]/g, "");
spWS.soapEnvelope = results[3].replace(/&lt;xsd:schema&gt;[\s\S]+?&lt;\/xsd:schema&gt;/g, "");
spWS.soapEnvelope = spWS.soapEnvelope.replace(/&lt;([\S]+)&gt;[^;]+&lt;\/\1&gt;/g, '<$1/>');
spWS.soapEnvelope = pS.htmlUnescape(spWS.soapEnvelope);
// Trim and remove line breaks (\n,\r), tabs (\t), etc.
spWS.soapEnvelope = pS.trim(spWS.soapEnvelope).replace(/>\s+?</g, "><");
return spWS;
}); // end then
};
pS.soap = function (options) {
return pS.getWSProperties(options.site, options.service, options.operation)
.then(function (wsProperties) {
var wsURL = options.site + '/_vti_bin/' + options.service + '.asmx';
var wsData = wsProperties.soapEnvelope.replace(/<([\S]+?)\/>/g, function (m, key) {
if (options[key]) { return ('<' + key + '>' + options[key] + '</' + key + '>') }
else { return ""; }
});
options.method = 'POST'; // always POST for Web services
options.headers = wsProperties.headers;
options.body = wsData;
return fetch(wsURL, options);
}); // end then
}; // end pS.soap
</script>
<script type="text/javascript">
var currentScript = document.currentScript;
pS.soap({
// Service info
site: props.context.pageContext.web.absoluteUrl,
service: "Lists",
operation: "GetListCollection",
// Service parameters
viewFields: "<ViewFields><FieldRef Name='Title' /><FieldRef Name='ImageURL' /></ViewFields>"
})
.then(response => response.text())
.then(data => new DOMParser().parseFromString(data, "application/xml"))
.then(xml => {
const newDiv = document.createElement('div');
const items = [...xml.getElementsByTagName("List")].map(child => `<li><img src="${child.getAttribute("ImageUrl")}"/> ${child.getAttribute("Title")}</li>`);
newDiv.innerHTML = `<div>${items.length} lists found on this site:</div><ul style="list-style-type: none; columns: 2;">${items.join("")}</ul>`;
currentScript.insertAdjacentElement('afterend', newDiv);
});
</script>
</div>

View File

@ -0,0 +1,263 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Analog Clock</h3>
<p>Isolated mode: recommended.</p>
<p>Source: Tushar Nankani on <a href="https://github.com/tusharnankani/AnalogClock" target="_blank">Github</a>.</p>
</div>
<div class="clock">
<div class="hour">
<div class="hr" id="hr">
</div>
</div>
<div class="min">
<div class="mn" id="mn">
</div>
</div>
<div class="sec">
<div class="sc" id="sc">
</div>
</div>
</div>
<div class="toggleClass" onclick="toggleClass()"></div>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #090909;
background: #07141b;
}
body.light {
background: #d1dae3;
}
.clock {
width: 375px;
height: 375px;
display: flex;
justify-content: center;
align-items: center;
background: url(../clock.png);
background-size: cover;
border: 4px;
/* box-shadow: 15px 15px 15px rgba(255, 255, 255, 0.5); */
box-shadow: 0em -1.2em 1.2em rgba(255, 255, 255, 0.06),
inset 0em -1.2em 1.2em rgba(255, 255, 255, 0.06),
0em 1.2em 1.2em rgba(0, 0, 0, 0.3),
inset 0em 1.2em 1.2em rgba(0, 0, 0, 0.3);
border-radius: 50%;
}
body.light .clock {
box-shadow: 0em -1.2em 1.2em rgba(255, 255, 255, 0.3),
inset 1em 1em -1em rgba(255, 255, 255, 0.3),
0em -1.2em -1.2em rgba(0, 0, 0, 0.5),
inset 1em -1em 1em rgba(0, 0, 0, 0.1);
}
.clock :hover {
/* yet to be completed; when hovered, diplay complete information about time, `new Date().toLocaleString();` */
cursor: pointer;
}
/* The small circle int the center */
.clock:before {
content: '';
position: absolute;
width: 15px;
height: 15px;
background: rgb(255, 255, 255);
border-radius: 50%;
/* The z-index property specifies the stack order of an element.
/* An element with greater stack order is always in front of an element with a lower stack order. */
/* Note: z-index only works on positioned elements (position: absolute, position: relative, position: fixed, or position: sticky). */
z-index: 10000;
/* kept as a high value, since wanted at top */
}
body.light .clock:before {
background: #1a74be;
}
.clock .hour,
.clock .min,
.clock .sec {
position: absolute;
}
/* length of respective arms; */
.clock .hour, .hr {
width: 160px;
height: 160px;
}
.clock .min, .mn {
width: 190px;
height: 190px;
}
.clock .sec, .sc {
width: 230px;
height: 230px;
}
.hr, .mn, .sc {
display: flex;
justify-content: center;
/* align-items: center; */
position: absolute;
border-radius: 50%;
}
.hr:before {
content: '';
position: absolute;
width: 7.5px;
height: 80px;
background: #f81460;
z-index: 10;
/* z-index least */
border-radius: 2.8px;
}
.mn:before {
content: '';
position: absolute;
width: 3.5px;
height: 100px;
background: #ffffff;
z-index: 11;
/* z-index more than hour hand */
border-radius: 3px;
}
body.light .mn:before {
background: #091921;
}
.sc:before {
content: '';
position: absolute;
width: 2px;
height: 150px;
background: #0075fa80;
z-index: 12;
/* z-index more than hour minute hand */
border-radius: 3px;
}
.toggleClass {
position: absolute;
top: 35px;
right: 150px;
width: 20px;
margin: 2px;
height: 20px;
font-size: 18px;
border-radius: 50%;
background: #d1dae3;
color: #d1dae3;
font-family: 'Quicksand', sans-serif;
cursor: pointer;
display: flex;
align-items: center;
}
.toggleClass:before {
position: absolute;
content: 'Light Mode';
white-space: nowrap;
left: 25px;
}
body.light .toggleClass {
background: #091921;
color: #091921;
content: 'Dark Mode';
}
body.light .toggleClass:before {
content: 'Dark Mode';
white-space: nowrap;
}
</style>
<script>// For toggle button;
function toggleClass()
{
const body = document.querySelector('body');
body.classList.toggle('light');
body.style.transition = `0.3s linear`;
}
// for time;
const deg = 6;
// 360 / (12 * 5);
const hr = document.querySelector('#hr');
const mn = document.querySelector('#mn');
const sc = document.querySelector('#sc');
setInterval(() => {
let day = new Date();
let hh = day.getHours() * 30;
let mm = day.getMinutes() * deg;
let ss = day.getSeconds() * deg;
let msec = day.getMilliseconds();
// VERY IMPORTANT STEP:
hr.style.transform = `rotateZ(${(hh) + (mm / 12)}deg)`;
mn.style.transform = `rotateZ(${mm}deg)`;
sc.style.transform = `rotateZ(${ss}deg)`;
// gives the smooth transitioning effect, but there's a bug here!
// sc.style.transition = `1s`;
})
</script>

View File

@ -0,0 +1,14 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Page Banner</h3>
<p>Isolated mode: not recommended. Read more on the <a href="https://blog.pathtosharepoint.com/2020/10/26/a-temporary-message-on-top-of-your-sharepoint-page/" target="_blank">Path to SharePoint blog</a>.</p>
<style type="text/css">
body::before {
display:block;
width:100%;
background-color: DarkOrange;
font-size: 20px;
padding: 10px;
content: "Headed for the office? Remember to bring your badge!";
}
</style>
</div>

View File

@ -0,0 +1,187 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Flip Styled Countdown</h3>
<p>Isolated mode: recommended. Source: FlipDown on <a href="https://github.com/PButcher/flipdown" target="_blank">Github</a>.</p>
<div class="example">
<p>⏰ FlipDown.js, a lightweight flip styled countdown clock</p>
<div id="flipdown" class="flipdown"></div>
</div>
<link href="https://pbutcher.uk/flipdown/css/flipdown/flipdown.css" rel="stylesheet">
<style>
html {
height: 100%;
}
body {
margin: 0px;
height: 100%;
display: flex;
align-items: center;
align-content: space-around;
}
body,
.example h1,
.example p,
.example .button {
transition: all .2s ease-in-out;
}
body.light-theme {
background-color: #151515;
}
body.light-theme .example h1 {
color: #FFFFFF;
}
body.light-theme .example p {
color: #FFFFFF;
}
body.light-theme .buttons .button {
color: #FFFFFF;
border-color: #FFFFFF;
}
body.light-theme .buttons .button:hover {
color: #151515;
background-color: #FFFFFF;
}
.example {
font-family: 'Roboto', sans-serif;
width: 550px;
height: 378px;
margin: auto;
padding: 20px;
box-sizing: border-box;
}
.example .flipdown {
margin: auto;
}
.example h1 {
text-align: center;
font-weight: 100;
font-size: 3em;
margin-top: 0;
margin-bottom: 10px;
}
.example p {
text-align: center;
font-weight: 100;
margin-top: 0;
margin-bottom: 35px;
}
.example .buttons {
width: 100%;
height: 50px;
margin: 50px auto 0px auto;
display: flex;
align-items: center;
justify-content: space-around;
}
.example .buttons p {
height: 50px;
line-height: 50px;
font-weight: 400;
padding: 0px 25px 0px 0px;
color: #333;
margin: 0px;
}
.example .button {
display: inline-block;
height: 50px;
box-sizing: border-box;
line-height: 46px;
text-decoration: none;
color: #333;
padding: 0px 20px;
border: solid 2px #333;
border-radius: 4px;
text-transform: uppercase;
font-weight: 700;
transition: all .2s ease-in-out;
}
.example .button:hover {
background-color: #333;
color: #FFF;
}
.example .button i {
margin-right: 5px;
}
@media(max-width: 550px) {
.example {
width: 100%;
height: 362px;
}
.example h1 {
font-size: 2.5em;
}
.example p {
margin-bottom: 25px;
}
.example .buttons {
width: 100%;
margin-top: 25px;
text-align: center;
display: block;
}
.example .buttons p,
.example .buttons a {
float: none;
margin: 0 auto;
}
.example .buttons p {
padding-right: 0px;
}
.example .buttons a {
display: inline-block;
}
}
</style>
<script>
const runFlipDown = () => {
// Unix timestamp (in seconds) to count down to
var twoDaysFromNow = (new Date().getTime() / 1000) + (86400 * 2) + 1;
// Set up FlipDown
var flipdown = new FlipDown(twoDaysFromNow)
// Start the countdown
.start()
// Do something when the countdown ends
.ifEnded(() => {
console.log('The countdown has ended!');
});
// Toggle theme
var interval = setInterval(() => {
let body = document.body;
body.classList.toggle('light-theme');
body.querySelector('#flipdown').classList.toggle('flipdown__theme-dark');
body.querySelector('#flipdown').classList.toggle('flipdown__theme-light');
}, 5000);
var ver = document.getElementById('ver');
ver.innerHTML = flipdown.version;
};
</script>
<script src="https://pbutcher.uk/flipdown/js/flipdown/flipdown.js" onload="runFlipDown()"></script>
</div>

View File

@ -0,0 +1,7 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>HTML Sample</h3>
<p>Isolated mode: not recommended. Source: <a href="https://blog.pathtosharepoint.com/2021/07/29/introducing-the-property-pane-portal/" target="_blank">Path to SharePoint blog</a>.</p>
<p><i>This got me thinking. Could we in some way avoid the redundancy? And as a bonus, directly hook a regular Reactjs component in here? That&#8217;s what I pictured in the diagram below: a generic, reusable frame that could serve as host to any Reactjs control.</i></p>
<figure><img style="width:100%;" src="https://pathtosharepoint.files.wordpress.com/2021/07/image-3.png" /></figure>
<p><i>It took me a couple days to come up with a workable model (the &#8220;Property Pane Portal&#8221;), weeks to test it, and then months to start writing about it 😊. In the next episodes, I&#8217;ll share some samples of the PPP in action, and I&#8217;ll provide more details on the architecture and the code that supports it. The key ingredient is <a href="https://reactjs.org/docs/portals.html">Reactjs Portals</a>, which allow to beam an element to another part of the DOM.</i></p>
</div>

View File

@ -0,0 +1,5 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Google Maps</h3>
<p>Isolated mode: not recommended. Source: Google Maps embed code.</p>
<iframe src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d85972.3820324134!2d-122.17073538560777!3d47.67204903850155!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x54900cad2000ee23%3A0x5e0390eac5d804f2!2sRedmond%2C%20WA!5e0!3m2!1sen!2sus!4v1645677983596!5m2!1sen!2sus" width="600" height="450" style="border:0;" allowfullscreen="" loading="lazy"></iframe>
</div>

View File

@ -0,0 +1,5 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Embedded YouTube Video</h3>
<p>Isolated mode: not required. Source: YouTube embed code.</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/ozhbLz1gMi0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,32 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "3e86ecba-18ac-44e2-862e-fc268a929584",
"alias": "CherryPickedContentWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is approved.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart", "TeamsPersonalApp", "TeamsTab", "SharePointFullPage"],
"supportsThemeVariants": true,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "Cherry-Picked-Content" },
"description": { "default": "Cherry-Picked-Content description" },
"officeFabricIconFontName": "BullseyeTargetEdit",
"properties": {
"description": "Cherry-Picked-Content",
"isolated": true,
"iframeWidth": "100%",
"iframeHeight": "600px"
}
}]
}

View File

@ -0,0 +1,207 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
IPropertyPaneDropdownOption,
PropertyPaneCheckbox,
PropertyPaneDropdown,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import * as strings from 'CherryPickedContentWebPartStrings';
import CherryPickedContent from './components/CherryPickedContent';
import { ICherryPickedContentProps } from './components/ICherryPickedContentProps';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import { approvedLibraries } from './components/ApprovedLibraries';
export interface ICherryPickedContentWebPartProps {
isolated: boolean;
iframeWidth: string;
iframeHeight: string;
description: string;
libraryPicker: string;
libraryItemPicker: string;
}
export default class CherryPickedContentWebPart extends BaseClientSideWebPart<ICherryPickedContentWebPartProps> {
private _isDarkTheme: boolean = false;
private _environmentMessage: string = '';
protected onInit(): Promise<void> {
this._environmentMessage = this._getEnvironmentMessage();
return super.onInit();
}
public render(): void {
const element: React.ReactElement<ICherryPickedContentProps> = React.createElement(
CherryPickedContent,
{
description: this.properties.description,
libraryPicker: this.properties.libraryPicker,
libraryItemPicker: this.properties.libraryItemPicker,
approvedLibraries: this.approvedLibraries,
isolated: this.properties.isolated,
width: this.properties.iframeWidth,
height: this.properties.iframeHeight,
context: this.context,
isDarkTheme: this._isDarkTheme,
environmentMessage: this._environmentMessage,
hasTeamsContext: !!this.context.sdks.microsoftTeams,
userDisplayName: this.context.pageContext.user.displayName
}
);
ReactDom.render(element, this.domElement);
}
private _getEnvironmentMessage(): string {
if (!!this.context.sdks.microsoftTeams) { // running in Teams
return this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment;
}
return this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment;
}
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
if (!currentTheme) {
return;
}
this._isDarkTheme = !!currentTheme.isInverted;
const {
semanticColors
} = currentTheme;
this.domElement.style.setProperty('--bodyText', semanticColors.bodyText);
this.domElement.style.setProperty('--link', semanticColors.link);
this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
// Only content from the approved libraries can be selected
private approvedLibraries = approvedLibraries;
// Dropdown gets disabled while retrieving items asynchronously
private itemsDropdownDisabled: boolean = true;
// Files in the selected library
private libraryItemsList: IPropertyPaneDropdownOption[];
// Asynchronous library query
private getLibraryItemsList = (library) => {
// Validate approved location
const filesLocation = this.approvedLibraries.filter(loc => loc.key == library)[0];
const filesQuery = window.location.origin + filesLocation.siteRelativeURL + "/_api/web/lists/getbytitle('" + filesLocation.library + "')/files?$select=Name";
return this.context.spHttpClient.get(filesQuery, SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => response.json())
.then(data => data.value);
}
// Runs before getting the Property Pane configuration
protected onPropertyPaneConfigurationStart(): void {
this.itemsDropdownDisabled = true;
if (this.properties.libraryPicker)
this.getLibraryItemsList(this.properties.libraryPicker)
.then((files): void => {
// store items
this.libraryItemsList = files.map(file => file.Name).sort().map(name => { return { key: name, text: name }; });
this.itemsDropdownDisabled = false;
})
.then(() => this.context.propertyPane.refresh());
}
// This API is invoked after updating the new value of the property in the property bag (Reactive mode).
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
if ((propertyPath === 'libraryPicker') && (newValue)) {
// get previously selected item
const previousItem: string = this.properties.libraryItemPicker;
// reset selected item
this.properties.libraryItemPicker = "";
// disable item selector until new items are loaded
this.itemsDropdownDisabled = true;
// push new item value
this.onPropertyPaneFieldChanged('libraryItemPicker', previousItem, this.properties.libraryItemPicker);
// refresh the item selector control by repainting the property pane
this.context.propertyPane.refresh();
this.getLibraryItemsList(newValue)
.then((files): void => {
if (files.length) {
// store items
this.libraryItemsList = files.map(file => { return { key: file.Name, text: file.Name }; });
// enable item selector
this.itemsDropdownDisabled = false;
// refresh the item selector control by repainting the property pane
this.context.propertyPane.refresh();
}
});
}
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
// Web Part title
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
}),
// Library Picker (approved libraries only)
PropertyPaneDropdown('libraryPicker', {
label: strings.LibraryPickerLabel,
options: this.approvedLibraries,
selectedKey: this.properties.libraryPicker
}),
// Cascading Library Item Picker
PropertyPaneDropdown('libraryItemPicker', {
label: strings.LibraryItemPickerLabel,
options: this.libraryItemsList,
selectedKey: this.properties.libraryItemPicker,
disabled: this.itemsDropdownDisabled
})
]
},
{
groupName: strings.IsolatedMode,
groupFields: [
// Isolated options
PropertyPaneCheckbox('isolated', {
text: strings.Isolated,
}),
this.properties.isolated && PropertyPaneTextField('iframeWidth', {
label: strings.IframeWidth
}),
this.properties.isolated && PropertyPaneTextField('iframeHeight', {
label: strings.IframeHeight
}),
]
}
]
}
]
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,20 @@
export const approvedLibraries = [
{
key: "/sites/PublicCDN/SiteAssets",
siteRelativeURL: "/sites/PublicCDN",
library: "Site Assets",
text: "Public CDN Site Assets"
},
{
key: "/sites/PublicCDN/Shared%20Documents",
siteRelativeURL: "/sites/PublicCDN",
library: "Documents",
text: "Public CDN Documents"
},
{
key: "/sites/PrivateCDN/SiteAssets",
siteRelativeURL: "/sites/PrivateCDN",
library: "Site Assets",
text: "Private CDN Site Assets"
}
];

View File

@ -0,0 +1,34 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.cherryPickedContent {
overflow: hidden;
padding: 1em;
color: "[theme:bodyText, default: #323130]";
color: var(--bodyText);
&.teams {
font-family: $ms-font-family-fallbacks;
}
}
.welcome {
text-align: center;
}
.welcomeImage {
width: 100%;
max-width: 420px;
}
.links {
a {
text-decoration: none;
color: "[theme:link, default:#03787c]";
color: var(--link); // note: CSS Custom Properties support is limited to modern browsers only
&:hover {
text-decoration: underline;
color: "[theme:linkHovered, default: #014446]";
color: var(--linkHovered); // note: CSS Custom Properties support is limited to modern browsers only
}
}
}

View File

@ -0,0 +1,84 @@
import * as React from 'react';
import styles from './CherryPickedContent.module.scss';
import { ICherryPickedContentProps } from './ICherryPickedContentProps';
import { escape } from '@microsoft/sp-lodash-subset';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import PortalIframe from './PortalIframe';
const CherryPickedDiv = ({ htmlFragment }) =>
<div ref={ref => { if (ref) { ref.innerHTML = ""; ref.appendChild(document.createRange().createContextualFragment(htmlFragment)); } }}>
</div>;
const MemoDiv = React.memo(CherryPickedDiv);
const CherryPickedContent: React.FunctionComponent<ICherryPickedContentProps> = (props) => {
const message = "Loading...";
const [htmlFragment, setHtmlFragment] = React.useState(message);
// Get the file content
React.useEffect(() => {
async function fetchSnippet() {
// Validate that the library is in the approved list
let filteredApprovedLibraries = props.approvedLibraries.filter(lib => lib.key == props.libraryPicker);
if ((filteredApprovedLibraries.length > 0) && (props.libraryItemPicker)) {
let fileURL = props.libraryPicker + "/" + props.libraryItemPicker;
const webURLQuery = props.context.pageContext.web.absoluteUrl + `/_api/sp.web.getweburlfrompageurl(@v)?@v=%27${window.location.origin}${fileURL}%27`;
let webURL = await props.context.spHttpClient.get(webURLQuery, SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => response.json())
.then(data => data.value);
const snippetURLQuery = webURL + `/_api/web/getFileByServerRelativeUrl('${fileURL}')/$value`;
const fragment = await props.context.spHttpClient.get(snippetURLQuery, SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => response.text());
setHtmlFragment(fragment);
}
else {
setHtmlFragment(message);
}
}
fetchSnippet();
}, [props.libraryItemPicker]);
if (!props.libraryItemPicker) {
return (
<section className={`${styles.cherryPickedContent} ${props.hasTeamsContext ? styles.teams : ''}`}>
<div style={{ display: "flex" }}>
<div style={{ flex: "50%" }}>
<h3>Edit Web Part properties to select a file.</h3>
<h3>Approved libraries:</h3>
<p>
<ul>
{props.approvedLibraries.map(lib => <li>{lib.text}</li>)}
</ul>
</p>
</div>
<div style={{ flex: "50%" }} className={styles.welcome}>
<img alt="" src={props.isDarkTheme ? require('../assets/welcome-dark.png') : require('../assets/welcome-light.png')} className={styles.welcomeImage} />
<h2>Welcome, {escape(props.userDisplayName)}!</h2>
<div>{props.environmentMessage}</div>
</div>
</div>
</section>
);
}
else if (props.isolated) {
return (
<PortalIframe {...props}>
<MemoDiv htmlFragment={htmlFragment} />
</PortalIframe>
);
}
else {
return (
<MemoDiv htmlFragment={htmlFragment} />
);
}
};
export default CherryPickedContent;

View File

@ -0,0 +1,16 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface ICherryPickedContentProps {
description: string;
libraryPicker: string;
libraryItemPicker: string;
approvedLibraries: any[];
isolated: boolean;
width: string;
height: string;
context: WebPartContext;
isDarkTheme: boolean;
environmentMessage: string;
hasTeamsContext: boolean;
userDisplayName: string;
}

View File

@ -0,0 +1,23 @@
import * as React from 'react';
import { createPortal } from 'react-dom';
const PortalIframe = ({
children,
...props
}) => {
const [contentRef, setContentRef] = React.useState(null);
const mountWindow = contentRef?.contentWindow;
const mountNode = contentRef?.contentWindow?.document?.body;
// Pass the props to the child iframe
if (mountWindow) mountWindow.props = props;
return (
<iframe width={props.width} height={props.height} style={{border:0}} ref={setContentRef}>
{mountNode && createPortal(children, mountNode)}
</iframe>
);
};
export default PortalIframe;

View File

@ -0,0 +1,17 @@
define([], function() {
return {
"PropertyPaneDescription": "Modern Content Editor Web Part with a twist: content can only be picked from approved locations.",
"BasicGroupName": "Web Part Properties",
"IsolatedMode": "Keep content isolated to prevent conflicts with other Web Parts.",
"DescriptionFieldLabel": "Title",
"LibraryPickerLabel": "Pick an approved library",
"LibraryItemPickerLabel": "Pick a file",
"Isolated": "Isolated Content",
"IframeWidth": "Width",
"IframeHeight": "Height",
"AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",
"AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",
"AppSharePointEnvironment": "The app is running on SharePoint page",
"AppTeamsTabEnvironment": "The app is running in Microsoft Teams"
}
});

View File

@ -0,0 +1,20 @@
declare interface ICherryPickedContentWebPartStrings {
IsolatedMode: string;
IframeHeight: string;
IframeWidth: string;
Isolated: string;
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
LibraryPickerLabel: string;
LibraryItemPickerLabel: string;
AppLocalEnvironmentSharePoint: string;
AppLocalEnvironmentTeams: string;
AppSharePointEnvironment: string;
AppTeamsTabEnvironment: string;
}
declare module 'CherryPickedContentWebPartStrings' {
const strings: ICherryPickedContentWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

View File

@ -0,0 +1,35 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.9/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection",
"es2015.promise"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
]
}

View File

@ -0,0 +1,29 @@
{
"extends": "./node_modules/@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}