v0.2.0
This commit is contained in:
parent
dbb43915e4
commit
b9e3105f46
14
README.md
14
README.md
|
@ -19,7 +19,11 @@ The Cherry=Picked Content Web Part is a modern replacement for the classic Conte
|
|||
|
||||
## Prerequisites
|
||||
|
||||
> N/A
|
||||
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
|
||||
|
||||
|
@ -31,6 +35,7 @@ React-Cherry-Picked-Content | [Christophe Humbert](https://github.com/PathToShar
|
|||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
0.2.0|March 6, 2022|Refactoring
|
||||
0.1.0|February 21, 2022|Initial draft
|
||||
|
||||
## Disclaimer
|
||||
|
@ -43,6 +48,7 @@ Version|Date|Comments
|
|||
|
||||
- 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**
|
||||
|
@ -52,9 +58,11 @@ Version|Date|Comments
|
|||
|
||||
This Web Part illustrates the following concepts:
|
||||
|
||||
- Cascading dropdown in the Property Pane
|
||||
- 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 iframe
|
||||
- Various examples of client-side code in the samples: Microsoft Graph Toolkit (people, email, agenda), charts, widgets (map, stock, countdown, clock, video).
|
||||
|
||||
## References
|
||||
|
||||
|
@ -63,3 +71,5 @@ This Web Part illustrates the following concepts:
|
|||
- [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
|
||||
|
||||
<img src="https://pnptelemetry.azurewebsites.net/sp-dev-fx-webparts/samples/react-cherry-picked-content" />
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "react-cherry-picked-content",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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’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 “Property Pane Portal”), weeks to test it, and then months to start writing about it 😊. In the next episodes, I’ll share some samples of the PPP in action, and I’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>
|
|
@ -0,0 +1,79 @@
|
|||
<div style="background-color: CadetBlue; padding: 20px;">
|
||||
<h3>Chart.js Bubble Chart</h3>
|
||||
<p>Isolated mode: ⚠ 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>
|
|
@ -0,0 +1,39 @@
|
|||
<div style="background-color: CadetBlue; padding: 20px;">
|
||||
<h3>Chartist Animated Lines</h3>
|
||||
<p>Isolated mode: ⚠ 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>
|
|
@ -0,0 +1,49 @@
|
|||
|
||||
<div style="background-color: CadetBlue; padding: 20px;">
|
||||
<h3>Agenda Custom View - Microsoft Graph Toolkit</h3>
|
||||
<p>Isolated mode: ⚠ 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>
|
|
@ -0,0 +1,70 @@
|
|||
<div style="background-color: CadetBlue; padding: 20px;">
|
||||
<h3>Emails Custom View - Microsoft Graph Toolkit</h3>
|
||||
<p>Isolated mode: ⚠ 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>
|
|
@ -0,0 +1,25 @@
|
|||
<div style="background-color: CadetBlue; padding: 20px;">
|
||||
<h3>People Components - Microsoft Graph Toolkit</h3>
|
||||
<p>Isolated mode: ⚠ 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -22,7 +22,11 @@
|
|||
"description": { "default": "Cherry-Picked-Content description" },
|
||||
"officeFabricIconFontName": "BullseyeTargetEdit",
|
||||
"properties": {
|
||||
"description": "Cherry-Picked-Content"
|
||||
"description": "Cherry-Picked-Content",
|
||||
"isolated": true,
|
||||
"iframeWidth": "100%",
|
||||
"iframeHeight": "600px"
|
||||
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Version } from '@microsoft/sp-core-library';
|
|||
import {
|
||||
IPropertyPaneConfiguration,
|
||||
IPropertyPaneDropdownOption,
|
||||
PropertyPaneCheckbox,
|
||||
PropertyPaneDropdown,
|
||||
PropertyPaneTextField
|
||||
} from '@microsoft/sp-property-pane';
|
||||
|
@ -13,12 +14,14 @@ import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
|||
import * as strings from 'CherryPickedContentWebPartStrings';
|
||||
import CherryPickedContent from './components/CherryPickedContent';
|
||||
import { ICherryPickedContentProps } from './components/ICherryPickedContentProps';
|
||||
import { update } from '@microsoft/sp-lodash-subset';
|
||||
|
||||
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;
|
||||
|
@ -43,6 +46,9 @@ export default class CherryPickedContentWebPart extends BaseClientSideWebPart<IC
|
|||
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,
|
||||
|
@ -74,7 +80,6 @@ export default class CherryPickedContentWebPart extends BaseClientSideWebPart<IC
|
|||
this.domElement.style.setProperty('--bodyText', semanticColors.bodyText);
|
||||
this.domElement.style.setProperty('--link', semanticColors.link);
|
||||
this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered);
|
||||
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
|
@ -88,14 +93,6 @@ export default class CherryPickedContentWebPart extends BaseClientSideWebPart<IC
|
|||
// Only content from the approved libraries can be selected
|
||||
private approvedLibraries = approvedLibraries;
|
||||
|
||||
private updateWebPartProperty(property, value, refreshWebPart = true, refreshPropertyPane = true) {
|
||||
|
||||
update(this.properties, property, () => value);
|
||||
if (refreshWebPart) this.render();
|
||||
if (refreshPropertyPane) this.context.propertyPane.refresh();
|
||||
|
||||
}
|
||||
|
||||
// Dropdown gets disabled while retrieving items asynchronously
|
||||
private itemsDropdownDisabled: boolean = true;
|
||||
|
||||
|
@ -122,7 +119,7 @@ export default class CherryPickedContentWebPart extends BaseClientSideWebPart<IC
|
|||
this.getLibraryItemsList(this.properties.libraryPicker)
|
||||
.then((files): void => {
|
||||
// store items
|
||||
this.libraryItemsList = files.map(file => { return { key: file.Name, text: file.Name }; });
|
||||
this.libraryItemsList = files.map(file => file.Name).sort().map(name => { return { key: name, text: name }; });
|
||||
this.itemsDropdownDisabled = false;
|
||||
})
|
||||
.then(() => this.context.propertyPane.refresh());
|
||||
|
@ -140,21 +137,18 @@ export default class CherryPickedContentWebPart extends BaseClientSideWebPart<IC
|
|||
this.itemsDropdownDisabled = true;
|
||||
// push new item value
|
||||
this.onPropertyPaneFieldChanged('libraryItemPicker', previousItem, this.properties.libraryItemPicker);
|
||||
// this.render();
|
||||
// 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;
|
||||
// this.render();
|
||||
// refresh the item selector control by repainting the property pane
|
||||
this.context.propertyPane.refresh();
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -179,8 +173,7 @@ export default class CherryPickedContentWebPart extends BaseClientSideWebPart<IC
|
|||
PropertyPaneDropdown('libraryPicker', {
|
||||
label: strings.LibraryPickerLabel,
|
||||
options: this.approvedLibraries,
|
||||
selectedKey: this.properties.libraryPicker,
|
||||
|
||||
selectedKey: this.properties.libraryPicker
|
||||
}),
|
||||
// Cascading Library Item Picker
|
||||
PropertyPaneDropdown('libraryItemPicker', {
|
||||
|
@ -190,6 +183,21 @@ export default class CherryPickedContentWebPart extends BaseClientSideWebPart<IC
|
|||
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
|
||||
}),
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -4,18 +4,24 @@ 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 = props.libraryItemPicker? "Loading...":"Edit Web Part properties to select a file.";
|
||||
const defaultNode = document.createRange().createContextualFragment("<div><h3>" + message + "</h3></div>");
|
||||
|
||||
const [appendedNode, setAppendedNode] = React.useState(defaultNode);
|
||||
const message = "Loading...";
|
||||
const [htmlFragment, setHtmlFragment] = React.useState(message);
|
||||
|
||||
// Get the file content
|
||||
React.useEffect(() => {
|
||||
async function fetchSnippet() {
|
||||
|
||||
// Validate that the library is approved
|
||||
// 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)) {
|
||||
|
||||
|
@ -23,62 +29,56 @@ const CherryPickedContent: React.FunctionComponent<ICherryPickedContentProps> =
|
|||
|
||||
const webURLQuery = props.context.pageContext.web.absoluteUrl + `/_api/sp.web.getweburlfrompageurl(@v)?@v=%27${window.location.origin}${fileURL}%27`;
|
||||
|
||||
// if (props.url)
|
||||
// const htmlFragment: string = (props.url) ?
|
||||
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 htmlFragment = await props.context.spHttpClient.get(snippetURLQuery, SPHttpClient.configurations.v1)
|
||||
const fragment = await props.context.spHttpClient.get(snippetURLQuery, SPHttpClient.configurations.v1)
|
||||
.then((response: SPHttpClientResponse) => response.text());
|
||||
// : "<div>No content loaded.</div>";
|
||||
const node = document.createRange().createContextualFragment(htmlFragment);
|
||||
setAppendedNode(node);
|
||||
setHtmlFragment(fragment);
|
||||
}
|
||||
else {
|
||||
setAppendedNode(defaultNode);
|
||||
setHtmlFragment(message);
|
||||
}
|
||||
}
|
||||
fetchSnippet();
|
||||
}, [props.libraryPicker, props.libraryItemPicker]);
|
||||
}, [props.libraryItemPicker]);
|
||||
|
||||
return (
|
||||
if (!props.libraryItemPicker) {
|
||||
return (
|
||||
<section className={`${styles.cherryPickedContent} ${props.hasTeamsContext ? styles.teams : ''}`}>
|
||||
<div ref={ref => { if (ref) { ref.innerHTML = ""; ref.appendChild(appendedNode); } }}>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Approved libraries:</h3>
|
||||
<p>
|
||||
<ul>
|
||||
{props.approvedLibraries.map(lib => <li>{lib.text}</li>)}
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
<div 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>
|
||||
<h3>Welcome to SharePoint Framework!</h3>
|
||||
<p>
|
||||
The SharePoint Framework (SPFx) is a extensibility model for Microsoft Viva, Microsoft Teams and SharePoint. It's the easiest way to extend Microsoft 365 with automatic Single Sign On, automatic hosting and industry standard tooling.
|
||||
</p>
|
||||
<h4>Learn more about SPFx development:</h4>
|
||||
<ul className={styles.links}>
|
||||
<li><a href="https://aka.ms/spfx" target="_blank">SharePoint Framework Overview</a></li>
|
||||
<li><a href="https://aka.ms/spfx-yeoman-graph" target="_blank">Use Microsoft Graph in your solution</a></li>
|
||||
<li><a href="https://aka.ms/spfx-yeoman-teams" target="_blank">Build for Microsoft Teams using SharePoint Framework</a></li>
|
||||
<li><a href="https://aka.ms/spfx-yeoman-viva" target="_blank">Build for Microsoft Viva Connections using SharePoint Framework</a></li>
|
||||
<li><a href="https://aka.ms/spfx-yeoman-store" target="_blank">Publish SharePoint Framework applications to the marketplace</a></li>
|
||||
<li><a href="https://aka.ms/spfx-yeoman-api" target="_blank">SharePoint Framework API reference</a></li>
|
||||
<li><a href="https://aka.ms/m365pnp" target="_blank">Microsoft 365 Developer Community</a></li>
|
||||
</ul>
|
||||
<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;
|
||||
|
|
|
@ -5,6 +5,9 @@ export interface ICherryPickedContentProps {
|
|||
libraryPicker: string;
|
||||
libraryItemPicker: string;
|
||||
approvedLibraries: any[];
|
||||
isolated: boolean;
|
||||
width: string;
|
||||
height: string;
|
||||
context: WebPartContext;
|
||||
isDarkTheme: boolean;
|
||||
environmentMessage: string;
|
||||
|
|
|
@ -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;
|
|
@ -2,9 +2,13 @@ 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",
|
||||
"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",
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
declare interface ICherryPickedContentWebPartStrings {
|
||||
IsolatedMode: string;
|
||||
IframeHeight: string;
|
||||
IframeWidth: string;
|
||||
Isolated: string;
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
DescriptionFieldLabel: string;
|
||||
|
|
Loading…
Reference in New Issue