Updated script editor web part to 1.4 and minor updates (#396)

* Upgraded to drop 1.4 and asset bundling
Bumped version to 1.0.0.3

* Clean up of CSS

* Remove linting rule which caused warning on build

* Added property to toggle web part container padding on/off

* Refactored to have local function bindings at the top for better readability.
Fixed readonly warning in cancel dialog as property was set directly instead of via setState

* Replaced safeWithCustomScriptDisabled with requiresCustomScript
Added removePadding property.
Updated correct schema reference.
Removed comments.

* Added skipFeatureDeployment attribute for easier switch if needed.

* Added missing ;

* Fix for setting the default switch to require custom script enabled for the web part. Value got switched one the attribute changed.

* Added hyperlink on Puzzlepart logo.
This commit is contained in:
Mikael Svenson 2018-01-20 14:59:48 +01:00 committed by Vesa Juvonen
parent 120af41c95
commit d8e4df3aca
10 changed files with 260 additions and 276 deletions

View File

@ -48,7 +48,7 @@ If all you want is to add markup on the page, you can do that as well. Adding th
```
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.3.0-green.svg)
![drop](https://img.shields.io/badge/drop-1.4.0-green.svg)
## Applies to
@ -68,6 +68,7 @@ Version|Date|Comments
1.0|March 10th, 2017|Initial release
1.0.0.1|August 8th, 2017|Updated SPFx version and CSS loading
1.0.0.2|October 4th, 2017|Updated SPFx version, bundle Office UI Fabric and CSS in webpart
1.0.0.3|January 10th, 2018|Updated SPFx version, added remove padding property and refactoring
## 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.**

View File

@ -2,7 +2,9 @@
"solution": {
"name": "Modern Script Editor web part by Puzzlepart",
"id": "1425175f-3ed8-44d2-8fc4-dd1497191294",
"version": "1.0.0.2"
"version": "1.0.0.3",
"includeClientSideAssets": true,
"skipFeatureDeployment": false
},
"paths": {
"zippedPackage": "solution/pzl-script-editor.sppkg"

View File

@ -29,7 +29,6 @@
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-unused-imports": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,

View File

@ -6,25 +6,23 @@
"node": ">=0.10.0"
},
"dependencies": {
"@microsoft/sp-core-library": "~1.3.0",
"@microsoft/sp-webpart-base": "~1.3.0",
"@types/react": "15.0.38",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14",
"@types/react-dom": "0.14.18",
"@microsoft/sp-core-library": "~1.4.0",
"@microsoft/sp-webpart-base": "~1.4.0",
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"office-ui-fabric-react": "^4.40.1",
"react": "15.4.2",
"react-dom": "15.4.2"
"react": "15.6.2",
"react-dom": "15.6.2"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.3.0",
"@microsoft/sp-module-interfaces": "~1.3.0",
"@microsoft/sp-webpart-workbench": "~1.3.0",
"@microsoft/sp-build-web": "~1.4.0",
"@microsoft/sp-module-interfaces": "~1.4.0",
"@microsoft/sp-webpart-workbench": "~1.4.0",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0"
"@types/mocha": ">=2.2.33 <2.6.0",
"ajv": "~5.2.2"
},
"scripts": {
"build": "gulp bundle",

View File

@ -1,3 +1,4 @@
export interface IScriptEditorWebPartProps {
script: string;
removePadding: boolean;
}

View File

@ -1,25 +1,28 @@
{
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
"id": "3a328f0a-99c4-4b28-95ab-fe0847f657a3",
"alias": "ScriptEditorWebPart",
"componentType": "WebPart",
"version": "*", // The "*" signifies that the version should be taken from the package.json
"manifestVersion": 2,
/**
* This property should only be set to true if it is certain that the webpart does not
* allow arbitrary scripts to be called
*/
"safeWithCustomScriptDisabled": false,
"preconfiguredEntries": [{
"groupId": "3a328f0a-99c4-4b28-95ab-fe0847f657a3",
"group": { "default": "Puzzlepart" },
"title": { "default": "Modern Script Editor" },
"description": { "default": "Add arbitrary script to a page" },
"officeFabricIconFontName": "JS",
"properties": {
"script": ""
}
}]
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "3a328f0a-99c4-4b28-95ab-fe0847f657a3",
"alias": "ScriptEditorWebPart",
"componentType": "WebPart",
"version": "*",
"manifestVersion": 2,
"requiresCustomScript": true,
"preconfiguredEntries": [
{
"groupId": "3a328f0a-99c4-4b28-95ab-fe0847f657a3",
"group": {
"default": "Puzzlepart"
},
"title": {
"default": "Modern Script Editor"
},
"description": {
"default": "Add arbitrary script to a page"
},
"officeFabricIconFontName": "JS",
"properties": {
"script": "",
"removePadding": false
}
}
]
}

View File

@ -3,156 +3,167 @@ import * as ReactDom from 'react-dom';
import { Version, DisplayMode } from '@microsoft/sp-core-library';
import { SPComponentLoader } from '@microsoft/sp-loader';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneCustomField
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneCustomField
} from '@microsoft/sp-webpart-base';
import ScriptEditor from './components/ScriptEditor';
import { IScriptEditorProps } from './components/IScriptEditorProps';
import { IScriptEditorWebPartProps } from './IScriptEditorWebPartProps';
import { PropertyPaneToggle } from '@microsoft/sp-webpart-base/lib/propertyPane/propertyPaneFields/propertyPaneToggle/PropertyPaneToggle';
export default class ScriptEditorWebPart extends BaseClientSideWebPart<IScriptEditorWebPartProps> {
public save: (script: string) => void = (script: string) => {
this.properties.script = script;
this.render();
}
public render(): void {
const element: React.ReactElement<IScriptEditorProps> = React.createElement(
ScriptEditor,
{
script: this.properties.script,
save: this.save
}
);
if (this.displayMode == DisplayMode.Read) {
this.domElement.innerHTML = this.properties.script;
this.executeScript(this.domElement);
} else {
ReactDom.render(element, this.domElement);
public save: (script: string) => void = (script: string) => {
this.properties.script = script;
this.render();
}
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
public render(): void {
const element: React.ReactElement<IScriptEditorProps> = React.createElement(
ScriptEditor,
{
script: this.properties.script,
save: this.save
}
);
protected renderLogo(domElement: HTMLElement) {
domElement.innerHTML = `
if (this.displayMode == DisplayMode.Read) {
if (this.properties.removePadding) {
this.domElement.parentElement.parentElement.parentElement.style.paddingTop = "0";
this.domElement.parentElement.parentElement.parentElement.style.paddingBottom = "0";
} else {
this.domElement.parentElement.parentElement.parentElement.style.paddingTop = "";
this.domElement.parentElement.parentElement.parentElement.style.paddingBottom = "";
}
this.domElement.innerHTML = this.properties.script;
this.executeScript(this.domElement);
} else {
ReactDom.render(element, this.domElement);
}
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected renderLogo(domElement: HTMLElement) {
domElement.innerHTML = `
<div style="margin-top: 30px">
<div style="float:right">Author: <a href="mailto:mikael.svenson@puzzlepart.com" tabindex="-1">Mikael Svenson</a></div>
<div style="float:right"><img src="//www.puzzlepart.com/wp-content/uploads/2017/08/Pzl-LogoType-200.png" onerror="this.style.display = \'none\'";"></div>
<div style="float:right"><a href="https://www.puzzlepart.com/" target="_blank"><img src="//www.puzzlepart.com/wp-content/uploads/2017/08/Pzl-LogoType-200.png" onerror="this.style.display = \'none\'";"></a></div>
</div>`;
}
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: 'No settings to change for this web part.'
},
groups: [
{
groupFields: [
PropertyPaneCustomField({
onRender: this.renderLogo,
key: "logo"
})
]
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
groups: [
{
groupFields: [
PropertyPaneToggle("removePadding", {
label: "Remove top/bottom padding of web part container",
checked: this.properties.removePadding,
onText: "Remove padding",
offText: "Keep padding"
}),
PropertyPaneCustomField({
onRender: this.renderLogo,
key: "logo"
})
]
}
]
}
]
};
}
private evalScript(elem) {
const data = (elem.text || elem.textContent || elem.innerHTML || "");
const headTag = document.getElementsByTagName("head")[0] || document.documentElement;
const scriptTag = document.createElement("script");
scriptTag.type = "text/javascript";
if (elem.src && elem.src.length > 0) {
return;
}
if (elem.onload && elem.onload.length > 0) {
scriptTag.onload = elem.onload;
}
try {
// doesn't work on ie...
scriptTag.appendChild(document.createTextNode(data));
} catch (e) {
// IE has funky script nodes
scriptTag.text = data;
}
headTag.insertBefore(scriptTag, headTag.firstChild);
headTag.removeChild(scriptTag);
}
private nodeName(elem, name) {
return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase();
}
// Finds and executes scripts in a newly added element's body.
// Needed since innerHTML does not run scripts.
//
// Argument element is an element in the dom.
private executeScript(element: HTMLElement) {
// Define global name to tack scripts on in case script to be loaded is not AMD/UMD
(<any>window).ScriptGlobal = {};
// main section of function
const scripts = [];
const children_nodes = element.childNodes;
for (var i = 0; children_nodes[i]; i++) {
const child: any = children_nodes[i];
if (this.nodeName(child, "script") &&
(!child.type || child.type.toLowerCase() === "text/javascript")) {
scripts.push(child);
}
]
}
]
};
}
private evalScript(elem) {
const data = (elem.text || elem.textContent || elem.innerHTML || "");
const headTag = document.getElementsByTagName("head")[0] || document.documentElement;
const scriptTag = document.createElement("script");
scriptTag.type = "text/javascript";
if (elem.src && elem.src.length > 0) {
return;
}
if (elem.onload && elem.onload.length > 0) {
scriptTag.onload = elem.onload;
}
try {
// doesn't work on ie...
scriptTag.appendChild(document.createTextNode(data));
} catch (e) {
// IE has funky script nodes
scriptTag.text = data;
}
headTag.insertBefore(scriptTag, headTag.firstChild);
headTag.removeChild(scriptTag);
}
private nodeName(elem, name) {
return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase();
}
// Finds and executes scripts in a newly added element's body.
// Needed since innerHTML does not run scripts.
//
// Argument element is an element in the dom.
private executeScript(element: HTMLElement) {
// Define global name to tack scripts on in case script to be loaded is not AMD/UMD
(<any>window).ScriptGlobal = {};
// main section of function
const scripts = [];
const children_nodes = element.childNodes;
for (var i = 0; children_nodes[i]; i++) {
const child: any = children_nodes[i];
if (this.nodeName(child, "script") &&
(!child.type || child.type.toLowerCase() === "text/javascript")) {
scripts.push(child);
}
}
const urls = [];
const onLoads = [];
for (var j = 0; scripts[j]; j++) {
const scriptTag = scripts[j];
if (scriptTag.src && scriptTag.src.length > 0) {
urls.push(scriptTag.src);
}
if (scriptTag.onload && scriptTag.onload.length > 0) {
onLoads.push(scriptTag.onload);
}
}
// Execute promises in sequentially - https://hackernoon.com/functional-javascript-resolving-promises-sequentially-7aac18c4431e
// Use "ScriptGlobal" as the global namein case script is AMD/UMD
const allFuncs = urls.map(url => () => SPComponentLoader.loadScript(url, { globalExportsName: "ScriptGlobal" }));
const promiseSerial = funcs =>
funcs.reduce((promise, func) =>
promise.then(result => func().then(Array.prototype.concat.bind(result))),
Promise.resolve([]));
// execute Promises in serial
promiseSerial(allFuncs)
.then(() => {
// execute any onload people have added
for (j = 0; onLoads[j]; j++) {
onLoads[j]();
const urls = [];
const onLoads = [];
for (var j = 0; scripts[j]; j++) {
const scriptTag = scripts[j];
if (scriptTag.src && scriptTag.src.length > 0) {
urls.push(scriptTag.src);
}
if (scriptTag.onload && scriptTag.onload.length > 0) {
onLoads.push(scriptTag.onload);
}
}
// execute script blocks
for (j = 0; scripts[j]; j++) {
const scriptTag = scripts[j];
if (scriptTag.parentNode) { scriptTag.parentNode.removeChild(scriptTag); }
this.evalScript(scripts[j]);
}
}).catch(console.error);
}
// Execute promises in sequentially - https://hackernoon.com/functional-javascript-resolving-promises-sequentially-7aac18c4431e
// Use "ScriptGlobal" as the global namein case script is AMD/UMD
const allFuncs = urls.map(url => () => SPComponentLoader.loadScript(url, { globalExportsName: "ScriptGlobal" }));
const promiseSerial = funcs =>
funcs.reduce((promise, func) =>
promise.then(result => func().then(Array.prototype.concat.bind(result))),
Promise.resolve([]));
// execute Promises in serial
promiseSerial(allFuncs)
.then(() => {
// execute any onload people have added
for (j = 0; onLoads[j]; j++) {
onLoads[j]();
}
// execute script blocks
for (j = 0; scripts[j]; j++) {
const scriptTag = scripts[j];
if (scriptTag.parentNode) { scriptTag.parentNode.removeChild(scriptTag); }
this.evalScript(scripts[j]);
}
}).catch(console.error);
}
}

View File

@ -1,4 +1,4 @@
//@import url(https://publiccdn.sharepointonline.com/techmikael.sharepoint.com/11510075fe4212d19d3e6d07c91981263dd697bf111cb1e5f0efb15de0ec08b382cde399/5.0.1/office-ui-fabric.min.css);
@import '~office-ui-fabric-react/dist/sass/References.scss';
.scriptEditor {
.container {
@ -10,45 +10,4 @@
.row {
padding: 20px;
}
.listItem {
max-width: 715px;
margin: 5px auto 5px auto;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: #0078d7;
border-color: #0078d7;
color: #ffffff;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
font-weight: 400;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: 600;
font-size: 14px;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -8,72 +8,78 @@ import { loadStyles } from '@microsoft/load-themed-styles';
require('./overrides.css');
export default class ScriptEditor extends React.Component<IScriptEditorProps, any> {
constructor() {
super();
const uiFabricCSS: string = `
constructor() {
super();
this._showDialog = this._showDialog.bind(this);
this._closeDialog = this._closeDialog.bind(this);
this._cancelDialog = this._cancelDialog.bind(this);
this._onScriptEditorTextChanged = this._onScriptEditorTextChanged.bind(this);
const uiFabricCSS: string = `
.pzl-bgColor-themeDark, .pzl-bgColor-themeDark--hover:hover {
background-color: "[theme:themeDark, default:#005a9e]";
}
`;
loadStyles(uiFabricCSS);
this.state = {
showDialog: false
};
}
loadStyles(uiFabricCSS);
this.state = {
showDialog: false
};
}
public componentDidMount(): void {
this.setState({ script: this.props.script, loaded: this.props.script });
}
private _showDialog() {
this.setState({ showDialog: true });
}
public componentDidMount(): void {
this.setState({ script: this.props.script, loaded: this.props.script });
}
private _showDialog() {
this.setState({ showDialog: true });
}
private _closeDialog() {
this.setState({ showDialog: false });
this.props.save(this.state.script);
}
private _closeDialog() {
this.setState({ showDialog: false });
this.props.save(this.state.script);
}
private _cancelDialog() {
this.setState({ showDialog: false });
this.state.script = this.state.loaded;
}
private _cancelDialog() {
this.props.save(this.state.loaded);
this.setState({ showDialog: false, script: this.state.loaded });
}
private _onScriptEditorTextChanged(text: string) {
this.setState({ script: text });
}
private _onScriptEditorTextChanged(text: string) {
this.setState({ script: text });
}
public render(): React.ReactElement<IScriptEditorProps> {
const viewMode = <span dangerouslySetInnerHTML={{ __html: this.state.script }}></span>;
public render(): React.ReactElement<IScriptEditorProps> {
const viewMode = <span dangerouslySetInnerHTML={{ __html: this.state.script }}></span>;
return (
<div >
<div className={styles.scriptEditor}>
<div className={styles.container}>
<div className={`ms-Grid-row pzl-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
<div className="ms-Grid-col ms-lg10 ms-xl8 ms-xlPush2 ms-lgPush1">
<span className="ms-font-xl ms-fontColor-white">The Modern Script Editor web part!</span>
<p className="ms-font-l ms-fontColor-white"></p>
<DefaultButton description='Opens the Sample Dialog' onClick={this._showDialog.bind(this)}>Edit snippet</DefaultButton>
</div>
</div>
</div>
</div>
<Dialog
isOpen={this.state.showDialog}
type={DialogType.normal}
onDismiss={this._closeDialog.bind(this)}
title='Embed'
subText='Paste your script, markup or embed code below. Note that scripts will only run in view mode.'
isBlocking={true}
className={'ScriptPart'}
>
<TextField multiline rows={15} onChanged={this._onScriptEditorTextChanged.bind(this)} value={this.state.script} />
<DialogFooter>
<PrimaryButton onClick={this._closeDialog.bind(this)}>Save</PrimaryButton>
<DefaultButton onClick={this._cancelDialog.bind(this)}>Cancel</DefaultButton>
</DialogFooter>
{viewMode}
</Dialog>
</div >);
}
return (
<div >
<div className={styles.scriptEditor}>
<div className={styles.container}>
<div className={`ms-Grid-row pzl-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
<div className="ms-Grid-col ms-lg10 ms-xl8 ms-xlPush2 ms-lgPush1">
<span className="ms-font-xl ms-fontColor-white">The Modern Script Editor web part!</span>
<p className="ms-font-l ms-fontColor-white"></p>
<DefaultButton description='Opens the snippet dialog' onClick={this._showDialog}>Edit snippet</DefaultButton>
</div>
</div>
</div>
</div>
<Dialog
isOpen={this.state.showDialog}
type={DialogType.normal}
onDismiss={this._closeDialog}
title='Embed'
subText='Paste your script, markup or embed code below. Note that scripts will only run in view mode.'
isBlocking={true}
className={'ScriptPart'}
>
<TextField multiline rows={15} onChanged={this._onScriptEditorTextChanged} value={this.state.script} />
<DialogFooter>
<PrimaryButton onClick={this._closeDialog}>Save</PrimaryButton>
<DefaultButton onClick={this._cancelDialog}>Cancel</DefaultButton>
</DialogFooter>
{viewMode}
</Dialog>
</div >);
}
}

View File

@ -8,8 +8,12 @@
"sourceMap": true,
"types": [
"es6-promise",
"es6-collections",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
}
}