Added enhanced list formatting web part sample (#1175)

* Added enhanced list formatting web part sample

* Update README.md

* Fixed minor issue with read only render

* Added descriptions to monaco editor window

* Removed duplicated resources

* Fixed markdown lint issues
This commit is contained in:
Hugo Bernier 2020-03-19 11:44:32 -04:00 committed by GitHub
parent 90e1658e18
commit 0868acab74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 30894 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
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,10 @@
{
"extends": "stylelint-config-standard",
"plugins": [
"stylelint-scss"
],
"rules": {
"at-rule-no-unknown": null,
"scss/at-rule-no-unknown": true
}
}

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": false,
"environment": "spo",
"version": "1.10.0",
"libraryName": "react-enhanced-list-formatting",
"libraryId": "af9a29d7-413a-4763-8ae0-926101bd010a",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "library"
}
}

View File

@ -0,0 +1,174 @@
---
page_type: sample
products:
- office-sp
languages:
- typescript
extensions:
contentType: samples
technologies:
- SharePoint Framework
platforms:
- react
createdDate: 5/1/2017 12:00:00 AM
---
# Enhanced List Formatting
## Summary
This web part allows you to add custom CSS on a page to enhance list formatting.
![picture of the web part in action](./assets/EnhancedListFormatting.gif)
## Used SharePoint Framework Version
![1.10.0](https://img.shields.io/badge/version-1.10.0-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Prerequisites
To use this web part, you must be familiar with SharePoint list formatting and CSS.
## Solution
Solution|Author(s)
--------|---------
react-enhanced-list-formatting | Hugo Bernier ([Tahoe Ninjas](http://tahoeninjas.blog), [@bernierh](https://twitter.com/bernierh))
react-enhanced-list-formatting | David Warner II ([@DavidWarnerII](https://twitter.com/davidwarnerii) / [Warner Digital](http://warner.digital))
## Version history
Version|Date|Comments
-------|----|--------
1.0|March 17, 2020|Initial release
## 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
* in the command line run:
* `npm install`
* `gulp serve`
## Features
This web part demonstrates how to use a custom property pane control to allow users to inject custom CSS into the page at runtime.
> **Important**
>
> This web part is not intended to be used to override global CSS styles. It should only be used on custom CSS class names.
>
> At the time that we built this solution, the only codeless way to add custom CSS classes in a SharePoint page is to use the **Format view** option in a list view, then insert the **List** web part on a page.
>
> If you change any global styles, you may introduce unpredictable issues in your environment. Please remove the web part if you experience any issues.
>
> Injecting custom CSS is *not* supported by Microsoft or the creators of this sample.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-enhanced-list-formatting" />
To use this web part, follow these steps:
1. Create a custom list view
2. From your custom list view, select **Format current view** from the view drop-down.
![Format current view](./assets/Viewformatting.png)
3. In the **Format view** pane, add the `class` attribute in an element node, as follows:
```json
"attributes": {
"class": "yourcustomclassgoeshere"
},
```
3. **Preview** and **Save** your custom format.
4. Add the list web part to a page and select the custom view you created
5. Add the **Enhanced List Formatting** web part (this web part) to the same page where you added the **List** web part.
6. After dismissing the disclaimer, use the web part's property pane to add your own CSS styles.
7. Save your page and preview it in **View** mode.
> **TIP**
>
> Try to use the out-of-the-box custom view format schema by using the `style` attribute wherever possible. Your users may want to use your custom view in areas where the web part will not be available -- for example, within Microsoft Teams.
>
> Rely on custom CSS styles to *augment* your design, not replace the custom view format.
### Suitable uses of this web part
Here are some examples of how you should use this web part responsibly:
- Add styles to your custom CSS classes that the custom view format schema does not support (e.g.: RGBA values)
- Add [pseudo-elements](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements) styles to your custom CSS classes (e.g.: `::first-letter`, `::after`, `::before`)
- Add [pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes) styles to your custom CSS classes (e.g.: `:hover`, `:first`, `:nth-child`)
- Add [animations](https://developer.mozilla.org/en-US/docs/Web/CSS/animation) to your custom CSS classes
### Unsuitable uses
At the risk of repeating ourselves, do not use this web part to do the following style changes:
- Changing any CSS classes that begin with `ms-`, as they indicate a Microsoft global style.
- Changing element styles, unless you use your custom CSS class as a selector to ensure that your styles only apply to your list (e.g.: `div.mycustomclass`, `.mycustomclass > div`)
### Removing the annoying disclaimer
The sample has a disclaimer that is inspired by that annoying disclaimer you see on most in-dashboard GPS systems. If you want to remove it, you can do so by following these steps:
1. Open `EnhancedListFormattingWebPart.manifest.json'
2. Find the following section:
```json
"properties": {
"description": "Enhanced List Formatting"
}
```
3. Add the following JSON:
```json
"acceptedDisclaimer": true
```
4. Your `properties` JSON should now look like this:
```json
"properties": {
"description": "Enhanced List Formatting",
"acceptedDisclaimer": true
}
```
5. Test that your changes work by using `gulp build` and `gulp serve` and re-add a new version of the web part to your page
6. Build a production version of the solution using `gulp dist`. See [Building the code](#Building_the_code)
### Building the code
```bash
git clone the repo
npm i
npm i -g gulp
gulp
```
This package produces the following:
* lib/* - intermediate-stage commonjs build artifacts
* dist/* - the bundled script, along with other resources
* deploy/* - all resources which should be uploaded to a CDN.
### Build options
* gulp clean - Cleans the solution
* gulp test - Runs unit tests
* gulp serve - Runs the solution for testing purposes
* gulp bundle - Bundles the solution
* gulp package-solution - Packages the solution
* gulp dev -- Builds a clean instance of the solution for development purposes
* gulp dist -- Builds a clean instance of the solution for distribution purposes

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -0,0 +1,20 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"enhanced-list-formatting-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/enhancedListFormatting/EnhancedListFormattingWebPart.js",
"manifest": "./src/webparts/enhancedListFormatting/EnhancedListFormattingWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"EnhancedListFormattingWebPartStrings": "lib/webparts/enhancedListFormatting/loc/{locale}.js",
"MonacoControlsLibraryStrings": "lib/controls/loc/{locale}.js",
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

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

View File

@ -0,0 +1,4 @@
{
"preset": "@voitanos/jest-preset-spfx-react16",
"rootDir": "../src"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "Enhanced List Formatting",
"id": "af9a29d7-413a-4763-8ae0-926101bd010a",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/enhanced-list-formatting.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

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,74 @@
'use strict';
// check if gulp dist was called
if (process.argv.indexOf('dist') !== -1) {
// add ship options to command call
process.argv.push('--ship');
}
const path = require('path');
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
const gulpSequence = require('gulp-sequence');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
// Create clean distrubution package
gulp.task('dist', gulpSequence('clean', 'bundle', 'package-solution'));
// Create clean development package
gulp.task('dev', gulpSequence('clean', 'bundle', 'package-solution'));
/**
* Webpack Bundle Anlayzer
* Reference and gulp task
*/
const bundleAnalyzer = require('webpack-bundle-analyzer');
build.configureWebpack.mergeConfig({
additionalConfiguration: (generatedConfiguration) => {
const lastDirName = path.basename(__dirname);
const dropPath = path.join(__dirname, 'temp', 'stats');
generatedConfiguration.plugins.push(new bundleAnalyzer.BundleAnalyzerPlugin({
openAnalyzer: false,
analyzerMode: 'static',
reportFilename: path.join(dropPath, `${lastDirName}.stats.html`),
generateStatsFile: true,
statsFilename: path.join(dropPath, `${lastDirName}.stats.json`),
logLevel: 'error'
}));
return generatedConfiguration;
}
});
/**
* StyleLinter configuration
* Reference and custom gulp task
*/
const stylelint = require('gulp-stylelint');
/* Stylelinter sub task */
let styleLintSubTask = build.subTask('stylelint', (gulp) => {
return gulp
.src('src/**/*.scss')
.pipe(stylelint({
failAfterError: false,
reporters: [{
formatter: 'string',
console: true
}]
}));
});
/* end sub task */
build.rig.addPreBuildTask(styleLintSubTask);
/**
* Custom Framework Specific gulp tasks
*/
build.initialize(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
{
"name": "react-enhanced-list-formatting",
"version": "0.0.1",
"private": false,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "./node_modules/.bin/jest --config ./config/jest.config.json",
"test:watch": "./node_modules/.bin/jest --config ./config/jest.config.json --watchAll"
},
"dependencies": {
"@microsoft/sp-core-library": "1.10.0",
"@microsoft/sp-lodash-subset": "1.10.0",
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
"@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@pnp/spfx-property-controls": "^1.17.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"@microsoft/sp-build-web": "1.10.0",
"@microsoft/sp-module-interfaces": "1.10.0",
"@microsoft/sp-tslint-rules": "1.10.0",
"@microsoft/sp-webpart-workbench": "1.10.0",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"@voitanos/jest-preset-spfx-react16": "^1.3.2",
"ajv": "~5.2.2",
"gulp": "~3.9.1",
"gulp-sequence": "1.0.0",
"gulp-stylelint": "^11.0.0",
"jest": "^23.6.0",
"stylelint": "^13.0.0",
"stylelint-config-standard": "^19.0.0",
"stylelint-scss": "^3.13.0",
"webpack-bundle-analyzer": "^3.6.0"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,13 @@
export interface IMonacoEditorProps {
value: string;
theme: string;
language: string;
readOnly?: boolean;
showLineNumbers?: boolean;
showMiniMap?: boolean;
showIndentGuides?: boolean;
folding?: boolean;
className?: string;
onValueChange?: (newValue: string) => void;
onDidBlurEditorText?: (value: string)=> void;
}

View File

@ -0,0 +1,25 @@
@import "~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss";
@import "~office-ui-fabric-react/dist/sass/References.scss";
.codeEditor {
width: 100%;
min-height: 300px;
height: 100%;
box-sizing: border-box;
//margin-bottom: 20px;
//border:1px solid RGB(194,194,194);
border-width: 1px;
border-style: solid;
//border-color: $ms-color-neutralLight;
border-color: $ms-color-neutralTertiaryAlt;
border-radius: 2px;
// border-color: rgb(51, 51, 51);
&:hover {
border-color: $ms-color-neutralPrimary;
}
&:focus-within {
border-color: $ms-color-themePrimary;
}
}

View File

@ -0,0 +1,108 @@
import * as React from 'react';
import { css } from "@uifabric/utilities/lib/css";
import styles from './MonacoEditor.module.scss';
import { IMonacoEditorProps } from './IMonacoEditorProps';
const monaco = require('../MonacoCustomBuild') as any;
export class MonacoEditor extends React.Component<IMonacoEditorProps, {}> {
private _container: HTMLElement;
private _editor: any;
public componentDidMount(): void {
this.createEditor();
}
private createEditor() {
if (this._editor) {
this._editor.dispose();
}
//Create the editor
this._editor = monaco.editor.create(this._container, {
value: this.props.value,
scrollBeyondLastLine: false,
theme: this.props.theme,
language: this.props.language,
folding: this.props.folding,
renderIndentGuides: this.props.showIndentGuides,
readOnly: this.props.readOnly,
lineNumbers: this.props.showLineNumbers,
lineNumbersMinChars: 4,
minimap: {
enabled: this.props.showMiniMap
}
});
//Subscribe to changes
this._editor.onDidChangeModelContent((e:any)=>this.onDidChangeModelContent(e));
this._editor.onDidBlurEditorText((e:any)=>this.onDidBlurEditorText(e));
this._editor.layout();
//KLUDGE: The Monaco editor does not draw if layout is called too early
//introduce a slight delay to make sure it is ready
setTimeout(() => {
this._editor.layout();
}, 100);
}
public componentDidUpdate(prevProps: IMonacoEditorProps) {
if (this.props.value !== prevProps.value) {
console.log("Editor value changed", this.props.value);
if (this._editor) {
this._editor.setValue(this.props.value);
}
}
if (this.props.theme !== prevProps.theme) {
console.log("Editor theme changed", this.props.theme);
monaco.editor.setTheme(this.props.theme);
}
if (this.props.showLineNumbers != prevProps.showLineNumbers ||
this.props.showMiniMap != prevProps.showMiniMap ||
this.props.showIndentGuides != prevProps.showIndentGuides ||
this.props.folding != prevProps.folding ) {
console.log("Editor various settings changed");
this.createEditor();
}
if (this._editor) {
console.log("Calling layout", this.props.theme);
this._editor.layout();
}
}
public componentWillUnmount(): void {
if (this._editor) {
this._editor.dispose();
}
}
public render(): React.ReactElement<IMonacoEditorProps> {
return (
<div ref={(container) => this._container = container!} className={css(styles.codeEditor, this.props.className)} />
);
}
private onDidBlurEditorText(e: any): void {
if (this.props.onDidBlurEditorText && this._editor) {
let curVal: string = this._editor.getValue();
if (curVal !== this.props.value) {
this.props.onDidBlurEditorText(curVal);
}
}
}
private onDidChangeModelContent(e: any): void {
if (this.props.onValueChange && this._editor) {
let curVal: string = this._editor.getValue();
if (curVal !== this.props.value) {
this.props.onValueChange(curVal);
}
}
}
}

View File

@ -0,0 +1,2 @@
export * from './MonacoEditor';
export * from './IMonacoEditorProps';

View File

@ -0,0 +1,72 @@
@import "~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss";
@import "~office-ui-fabric-react/dist/sass/References.scss";
.codeEditor {
width: 100%;
min-height: 300px;
height: 100%;
box-sizing: border-box;
//margin-bottom: 20px;
//border:1px solid RGB(194,194,194);
border-width: 1px;
border-style: solid;
//border-color: $ms-color-neutralLight;
border-color: $ms-color-neutralTertiaryAlt;
border-radius: 2px;
// border-color: rgb(51, 51, 51);
&:hover {
border-color: $ms-color-neutralPrimary;
}
&:focus-within {
border-color: $ms-color-themePrimary;
}
}
.actionButtonsContainer {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
width: 100%;
background-color: $ms-color-white;
-ms-flex-negative: 0;
flex-shrink: 0;
}
.actionButtons {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
padding-top: 10px;
padding-bottom: 10px;
}
.actionButton {
height: 30px;
padding-bottom: 3px;
}
[dir="ltr"] .actionButton {
padding-left: 16px;
margin-left: 0;
margin-right: 10px;
}
[dir="ltr"] .actionButtons {
padding-left: 20px;
padding-right: 11px;
}
:global {
.ms-Panel-content {
height: calc(100% - 80px);
}
}

View File

@ -0,0 +1,128 @@
import * as React from 'react';
import styles from './EditorPanel.module.scss';
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
import { DefaultButton, PrimaryButton, IconButton } from 'office-ui-fabric-react/lib/Button';
import { Async } from 'office-ui-fabric-react/lib/Utilities';
import * as strings from 'MonacoControlsLibraryStrings';
import { MonacoEditor } from '../MonacoEditor';
export interface IEditorPanelProps {
//defaultValue?: string;
deferredValidationTime?: number;
disabled?: boolean;
editorClassName?: string;
editorHeight?: string;
errorMessage?: string;
initialValue?: string;
label?: string;
language: string;
targetProperty: string;
showIndentGuides?: boolean;
showLineNumbers?: boolean;
showMiniMap?: boolean;
folding?: boolean;
theme?: string;
value?: string;
onClose(): void;
onSave(value: string): void;
}
export interface IEditorPanelState {
value: string;
}
export class EditorPanel extends React.Component<IEditorPanelProps, IEditorPanelState> {
private _async: Async;
//private _editor: AceEditor;
private _delayedChange: (value: string) => void;
/**
*
*/
constructor(props: IEditorPanelProps) {
super(props);
this.state = {
value: this.props.value,
};
this._async = new Async(this);
this._delayedChange = this._async.debounce(this._handleOnChanged, this.props.deferredValidationTime ? this.props.deferredValidationTime : 200);
}
// public componentDidMount(): void {
// if (this.props.customMode !== undefined) {
// try {
// // execute the custom mode function
// this.props.customMode();
// // get a reference to ace
// const aceThingy: any = this._editor as any;
// // get a reference to brace
// var ace = require('brace') as any;
// // set the mode to custom
// var editor = ace.edit(aceThingy.editor);
// editor.session.setMode(`ace/mode/custom`);
// } catch (error) {
// console.log("Error with refs", error);
// }
// }
// }
public render(): React.ReactElement<IEditorPanelProps> {
return (
<Panel
isOpen={true}
onDismiss={() => this._handleClose()}
type={PanelType.large}
headerText={this.props.label}
onRenderFooterContent={() => (
<div className={styles.actionButtonsContainer}>
<div className={styles.actionButtons}>
<PrimaryButton
onClick={() => this._handleSave()} className={styles.actionButton}>{strings.SaveButtonLabel}</PrimaryButton>
<DefaultButton onClick={() => this._handleClose()} className={styles.actionButton}>{strings.CancelButtonLabel}</DefaultButton>
</div>
</div>
)}>
<MonacoEditor
value={this.props.value}
theme={this.props.theme}
readOnly={this.props.disabled}
language={this.props.language}
onValueChange={(editorString: string) => {
this._delayedChange(editorString);
}}
showLineNumbers={this.props.showLineNumbers !== undefined ? this.props.showLineNumbers : false}
showMiniMap={this.props.showMiniMap !== undefined ? this.props.showMiniMap : false}
showIndentGuides={this.props.showIndentGuides !== undefined ? this.props.showIndentGuides : false}
folding={this.props.folding !== undefined ? this.props.folding : false}
/>
</Panel>
);
}
private _handleSave = () => {
this.props.onSave(this.state.value);
}
private _handleClose = () => {
this.props.onClose();
}
/**
* On field change event handler
*/
private _handleOnChanged = (value: string): void => {
// Update state
this.setState({
value
});
}
}

View File

@ -0,0 +1,108 @@
import { IPropertyPaneCustomFieldProps } from "@microsoft/sp-property-pane";
import { IMonacoEditorProps } from "../MonacoEditor";
export interface IPropertyFieldMonacoEditorProps {
/**
* Custom Field will start to validate after users stop typing for `deferredValidationTime` milliseconds.
* Default value is 200.
*/
deferredValidationTime?: number;
defaultValue?: string;
/**
* Specify if the control needs to be disabled
*/
disabled?: boolean;
/**
* The CSS class name to apply to the code editor component
*/
editorClassName?: string;
/**
* The height style to apply to the code editor component
*/
editorHeight?: string;
/**
* If set, this will be displayed as an error message.
*
* When onGetErrorMessage returns empty string, if this property has a value set then this will
* be displayed as the error message.
*
* So, make sure to set this only if you want to see an error message dispalyed for the text field.
*/
errorMessage?: string;
/**
* The initial code to display in the code editor
*/
initialValue?: string;
/**
* An UNIQUE key indicates the identity of this control
*/
key: string;
/**
* Property field label displayed on top
*/
label?: string;
/**
* The language you wish to use with the editor.
* */
language: string;
/**
* The theme you wish to use.
*/
theme?: string;
/**
* The code you wish to display in the code editor
*/
value?: string;
/**
* Indicates whether the editor should be read only
*/
readOnly?: boolean;
/**
* Indicates whether the editor should show line numbers
*/
showLineNumbers?: boolean;
/**
* Indicates whether the editor should show a mini-map
*/
showMiniMap?: boolean;
/**
* Indicates whether the editor should show indent guides
*/
showIndentGuides?: boolean;
/**
* Indicates whether the editor should allow code folding
*/
folding?: boolean;
/**
* Indicates whether you should show a Full Page Editor button
*/
showFullScreen?: boolean;
/**
* Defines a onPropertyChange function to raise when the code changes.
* Normally this function must be always defined with the 'this.onPropertyChange'
* method of the web part object.
*/
onPropertyChange(propertyPath: string, oldValue: any, newValue: any): void;
}
export interface IPropertyFieldMonacoEditorPropsInternal extends IPropertyPaneCustomFieldProps, IPropertyFieldMonacoEditorProps {
targetProperty: string;
}

View File

@ -0,0 +1,18 @@
import { IPropertyFieldMonacoEditorPropsInternal } from './IPropertyFieldMonacoEditor';
/**
* PropertyFieldNumberHost properties interface
*/
export interface IPropertyFieldMonacoEditorHostProps extends IPropertyFieldMonacoEditorPropsInternal {
onChange: (targetProperty?: string, newValue?: any) => void;
}
export interface IPropertyFieldMonacoEditorHostState {
//annotations: string[];
editorClassName?: string;
editorHeight?: string;
//errorMessage?: string;
value: string;
fullScreen: boolean;
}

View File

@ -0,0 +1,10 @@
@import "~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss";
@import '~office-ui-fabric-react/dist/sass/References.scss';
.embeddedMonaco {
height: 300px;
}
.fullScreenButton {
color: inherit;
}

View File

@ -0,0 +1,68 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { IPropertyPaneField, PropertyPaneFieldType } from "@microsoft/sp-property-pane";
import PropertyFieldAceEditorHost from './PropertyFieldMonacoEditorHost';
import { IPropertyFieldMonacoEditorPropsInternal, IPropertyFieldMonacoEditorProps } from './IPropertyFieldMonacoEditor';
class PropertyFieldMonacoEditorBuilder implements IPropertyPaneField<IPropertyFieldMonacoEditorPropsInternal> {
public targetProperty: string;
public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom;
public properties: IPropertyFieldMonacoEditorPropsInternal;
public onPropertyChange(propertyPath: string, oldValue: any, newValue: any): void { }
private onGetErrorMessage?: (value: string, annotations: string[]) => string | Promise<string>;
//private _onChangeCallback: (targetProperty?: string, newValue?: any) => void;
public constructor(_targetProperty: string, _properties: IPropertyFieldMonacoEditorPropsInternal) {
this.targetProperty = _targetProperty;
this.properties = _properties;
this.onPropertyChange = _properties.onPropertyChange;
this.properties.onRender = this._render.bind(this);
this.properties.onDispose = this._dispose.bind(this);
}
private _render(elem: HTMLElement, context?: any, changeCallback?: (targetProperty?: string, newValue?: any) => void): void {
const props: IPropertyFieldMonacoEditorProps = <IPropertyFieldMonacoEditorProps>this.properties;
const element = React.createElement(PropertyFieldAceEditorHost, {
...props,
targetProperty: this.targetProperty,
onRender: this._render,
onChange: changeCallback,
onPropertyChange: this.onPropertyChange,
});
ReactDOM.render(element, elem);
// if (changeCallback) {
// this._onChangeCallback = changeCallback;
// }
}
private _dispose(elem: HTMLElement) {
ReactDOM.unmountComponentAtNode(elem);
}
}
/**
* Use this property pane control to allow users to edit in-place code.
*
* To allow full-page code editing, please use the PnP codeeditor property control.
*
* @param targetProperty
* @param properties
*/
export function PropertyFieldMonacoEditor(targetProperty: string, properties: IPropertyFieldMonacoEditorProps): IPropertyPaneField<IPropertyFieldMonacoEditorPropsInternal> {
return new PropertyFieldMonacoEditorBuilder(targetProperty, {
...properties,
targetProperty: targetProperty,
onRender: null,
onDispose: null
});
}

View File

@ -0,0 +1,115 @@
import * as strings from 'MonacoControlsLibraryStrings';
import * as React from 'react';
// Custom props and state
import { IPropertyFieldMonacoEditorHostProps, IPropertyFieldMonacoEditorHostState } from './IPropertyFieldMonacoEditorHost';
// Office Fabric
import { Label } from 'office-ui-fabric-react/lib/Label';
import { IconButton } from 'office-ui-fabric-react/lib/Button';
// Custom styles
import styles from './PropertyFieldMonacoEditor.module.scss';
// Custom code editor panel
import { EditorPanel } from './EditorPanel';
import { MonacoEditor } from '../MonacoEditor';
export default class PropertyFieldNumberHost extends React.Component<IPropertyFieldMonacoEditorHostProps, IPropertyFieldMonacoEditorHostState> {
constructor(props: IPropertyFieldMonacoEditorHostProps) {
super(props);
this.state = {
value: this.props.value,
fullScreen: false,
};
}
/**
* Render field
*/
public render(): JSX.Element {
return (
<div>
<Label>{this.props.label}</Label>
<MonacoEditor
value={this.props.defaultValue}
theme={this.props.theme}
readOnly={this.props.disabled}
language={this.props.language}
onDidBlurEditorText={(editorString: string)=> {
this._handleOnChanged(editorString);
}}
showLineNumbers={this.props.showLineNumbers || false}
showMiniMap={this.props.showMiniMap || false}
showIndentGuides={this.props.showIndentGuides || false}
folding={this.props.folding || false}
className={styles.embeddedMonaco}
/>
{ this.props.showFullScreen !== false && <IconButton
title={strings.ExpandButtonLabel}
className={styles.fullScreenButton}
iconProps={{ iconName: 'MiniExpand' }}
onClick={() => this._handleOpenFullScreen()}
/>}
{this.state.fullScreen &&
<EditorPanel
label={this.props.label}
language={this.props.language}
theme={this.props.theme}
onSave={(value: string) => this._handleSaveFullScreen(value)}
onClose={() => this._handleCloseFullScreen()}
value={this.state.value}
disabled={this.props.disabled}
targetProperty={this.props.targetProperty}
showMiniMap={true}
showLineNumbers={true}
/>
}
</div>
);
}
/**
* On field change event handler
*/
private _handleOnChanged = (value: string): void => {
// Update state
this.setState({
value
});
this.props.onPropertyChange(this.props.targetProperty, this.props.initialValue, value);
if (typeof this.props.onChange !== 'undefined' && this.props.onChange !== null) {
this.props.onChange(value);
}
}
/**
* Called when user clicks on the expand button
*/
private _handleOpenFullScreen = () => {
this.setState({
fullScreen: true
});
}
/**
* Gets called by the editor pane when it is time to save
*/
private _handleSaveFullScreen = (newValue: string) => {
this.setState({ fullScreen: false });
this._handleOnChanged(newValue);
}
/**
* ets called by the editor pane when it is closed
*/
private _handleCloseFullScreen = () => {
this.setState({ fullScreen: false });
}
}

View File

@ -0,0 +1,5 @@
export * from './IPropertyFieldMonacoEditor';
export * from './PropertyFieldMonacoEditor';
export * from './IPropertyFieldMonacoEditorHost';
export * from './PropertyFieldMonacoEditorHost';

View File

@ -0,0 +1,7 @@
define([], function() {
return {
ExpandButtonLabel: "Open full-screen editor",
SaveButtonLabel: "Save",
CancelButtonLabel: "Cancel",
}
});

View File

@ -0,0 +1,10 @@
declare interface IMonacoControlsLibraryStrings {
ExpandButtonLabel: string;
SaveButtonLabel: string;
CancelButtonLabel: string;
}
declare module 'MonacoControlsLibraryStrings' {
const strings: IMonacoControlsLibraryStrings;
export = strings;
}

View File

@ -0,0 +1,23 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "d7608466-3f72-4e48-b3b7-f7cf47a93786",
"alias": "EnhancedListFormattingWebPart",
"componentType": "WebPart",
"version": "*",
"manifestVersion": 2,
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
"group": { "default": "Other" },
"title": { "default": "Enhanced List Formatting" },
"description": { "default": "Adds support for custom CSS styles with list formatting" },
"iconImageUrl": "data:image/svg+xml,%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 512 512' style='enable-background:new 0 0 512 512;' xml:space='preserve'%3E %3Cg%3E %3Cpath style='fill:%230F81E0;' d='M370.759,110.345H17.655C7.901,110.345,0,102.444,0,92.69V22.069C0,12.314,7.901,4.414,17.655,4.414 h353.103c9.754,0,17.655,7.901,17.655,17.655V92.69C388.414,102.444,380.513,110.345,370.759,110.345'/%3E %3Cpath style='fill:%23ECBA16;' d='M370.759,242.759H17.655C7.901,242.759,0,234.858,0,225.103v-70.621 c0-9.754,7.901-17.655,17.655-17.655h353.103c9.754,0,17.655,7.901,17.655,17.655v70.621 C388.414,234.858,380.513,242.759,370.759,242.759'/%3E %3Cpath style='fill:%23ED7161;' d='M494.345,375.172H141.241c-9.754,0-17.655-7.901-17.655-17.655v-70.621 c0-9.754,7.901-17.655,17.655-17.655h353.103c9.754,0,17.655,7.901,17.655,17.655v70.621 C512,367.272,504.099,375.172,494.345,375.172'/%3E %3Cg%3E %3Cpath style='fill:%2342B05C;' d='M370.759,507.586H17.655C7.901,507.586,0,499.686,0,489.931V419.31 c0-9.754,7.901-17.655,17.655-17.655h353.103c9.754,0,17.655,7.901,17.655,17.655v70.621 C388.414,499.686,380.513,507.586,370.759,507.586'/%3E %3Cpath style='fill:%2342B05C;' d='M194.207,331.034H61.793c-4.882,0-8.828-3.955-8.828-8.828c0-4.873,3.946-8.828,8.828-8.828 h132.414c4.882,0,8.828,3.955,8.828,8.828C203.034,327.08,199.089,331.034,194.207,331.034'/%3E %3Cpath style='fill:%2342B05C;' d='M194.207,331.034c-2.26,0-4.52-0.865-6.241-2.586l-26.483-26.483 c-3.452-3.452-3.452-9.031,0-12.482s9.031-3.452,12.482,0l26.483,26.483c3.452,3.452,3.452,9.031,0,12.482 C198.727,330.169,196.467,331.034,194.207,331.034'/%3E %3Cpath style='fill:%2342B05C;' d='M167.724,357.517c-2.26,0-4.52-0.865-6.241-2.586c-3.452-3.452-3.452-9.031,0-12.482l26.483-26.483 c3.452-3.452,9.031-3.452,12.482,0c3.452,3.452,3.452,9.031,0,12.482l-26.483,26.483 C172.244,356.652,169.984,357.517,167.724,357.517'/%3E %3C/g%3E %3Cg%3E %3Cpath style='fill:%23FFFFFF;' d='M459.034,331.034H247.172c-4.882,0-8.828-3.955-8.828-8.828c0-4.873,3.946-8.828,8.828-8.828 h211.862c4.882,0,8.828,3.955,8.828,8.828C467.862,327.08,463.916,331.034,459.034,331.034'/%3E %3Cpath style='fill:%23FFFFFF;' d='M335.448,198.621H123.586c-4.882,0-8.828-3.955-8.828-8.828c0-4.873,3.946-8.828,8.828-8.828 h211.862c4.882,0,8.828,3.955,8.828,8.828C344.276,194.666,340.33,198.621,335.448,198.621'/%3E %3Cpath style='fill:%23FFFFFF;' d='M79.448,189.793c0-9.754-7.901-17.655-17.655-17.655s-17.655,7.901-17.655,17.655 c0,9.754,7.901,17.655,17.655,17.655S79.448,199.548,79.448,189.793'/%3E %3Cpath style='fill:%23FFFFFF;' d='M335.448,66.207H123.586c-4.882,0-8.828-3.955-8.828-8.828s3.946-8.828,8.828-8.828h211.862 c4.882,0,8.828,3.955,8.828,8.828S340.33,66.207,335.448,66.207'/%3E %3Cpath style='fill:%23FFFFFF;' d='M79.448,57.379c0-9.754-7.901-17.655-17.655-17.655s-17.655,7.901-17.655,17.655 s7.901,17.655,17.655,17.655S79.448,67.134,79.448,57.379'/%3E %3Cpath style='fill:%23FFFFFF;' d='M335.448,463.448H123.586c-4.882,0-8.828-3.955-8.828-8.828c0-4.873,3.946-8.828,8.828-8.828 h211.862c4.882,0,8.828,3.955,8.828,8.828C344.276,459.493,340.33,463.448,335.448,463.448'/%3E %3Cpath style='fill:%23FFFFFF;' d='M79.448,454.621c0-9.754-7.901-17.655-17.655-17.655s-17.655,7.901-17.655,17.655 c0,9.754,7.901,17.655,17.655,17.655S79.448,464.375,79.448,454.621'/%3E %3C/g%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3Cg%3E %3C/g%3E %3C/svg%3E ",
"properties": {
"description": "Enhanced List Formatting"
}
}]
}

View File

@ -0,0 +1,88 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'EnhancedListFormattingWebPartStrings';
import EnhancedListFormatting from './components/EnhancedListFormatting';
import { IEnhancedListFormattingProps } from './components/IEnhancedListFormattingProps';
import { PropertyPaneWebPartInformation } from '@pnp/spfx-property-controls/lib/PropertyPaneWebPartInformation';
import { PropertyFieldMonacoEditor } from '../../controls/PropertyFieldMonacoEditor';
export interface IEnhancedListFormattingWebPartProps {
css: string;
acceptedDisclaimer?: boolean;
}
export default class EnhancedListFormattingWebPart extends BaseClientSideWebPart <IEnhancedListFormattingWebPartProps> {
public render(): void {
const element: React.ReactElement<IEnhancedListFormattingProps> = React.createElement(
EnhancedListFormatting,
{
css: this.properties.css,
acceptedDisclaimer: this.properties.acceptedDisclaimer,
displayMode: this.displayMode,
context: this.context,
onAcceptDisclaimer: ()=>this._handleAcceptDisclaimer()
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyFieldMonacoEditor('css', {
label: strings.CSSFieldLabel,
key: "cssText",
value: this.properties.css,
defaultValue: this.properties.css,
language: "css",
theme: "vs-light",
showLineNumbers: false,
onPropertyChange: (_propertyPath: string, _oldValue: string, value: string) => this._handleSave(value),
}),
PropertyPaneWebPartInformation({
description: strings.CSSDisclaimer,
key: 'cssDisclaimer'
})
]
}
]
}
]
};
}
private _handleAcceptDisclaimer = () => {
this.properties.acceptedDisclaimer = true;
this.render();
}
private _handleSave = (value: string) => {
this.properties.css = value;
}
}

View File

@ -0,0 +1,9 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
.enhancedListFormatting {
padding-bottom: 20px;
}
.disclaimerText {
color:$ms-color-black;
}

View File

@ -0,0 +1,66 @@
import * as React from 'react';
import styles from './EnhancedListFormatting.module.scss';
import * as strings from 'EnhancedListFormattingWebPartStrings';
import { IEnhancedListFormattingProps } from './IEnhancedListFormattingProps';
import { MessageBarButton, MessageBar, MessageBarType } from 'office-ui-fabric-react';
import { DisplayMode } from '@microsoft/sp-core-library';
export default class EnhancedListFormatting extends React.Component<IEnhancedListFormattingProps, {}> {
public render(): React.ReactElement<IEnhancedListFormattingProps> {
const { css, displayMode, acceptedDisclaimer } = this.props;
// If we accepted the disclaimer, let's work as expected
// Determine if there is a CSS value
const hasCSS: boolean = css !== undefined && css !== "";
// Create a style element
const styleElem: JSX.Element = <style type="text/css">{css}</style>;
// If we're not in Edit mode, hide this web part
if (displayMode !== DisplayMode.Edit) {
return styleElem;
}
// if we didn't accept the disclaimer, show a disclaimer and nothing else
if (acceptedDisclaimer !== true) {
return (<MessageBar
onDismiss={()=>this._onAcceptDisclaimer()}
dismissButtonAriaLabel={strings.DismissDisclaimerAriaLabel}
messageBarType={MessageBarType.warning}
actions={
<div>
<MessageBarButton onClick={()=>this._onAcceptDisclaimer()}>{strings.AcceptDisclaimerButton}</MessageBarButton>
</div>
}
>
<div className={styles.disclaimerText} dangerouslySetInnerHTML={{__html: strings.DisclaimerText}}></div>
</MessageBar>);
}
return (
<div className={styles.enhancedListFormatting}>
{styleElem}
<MessageBar
messageBarType={hasCSS ? MessageBarType.success : null}
isMultiline={false}
actions={
<div>
<MessageBarButton onClick={() => this._onConfigure()}>{hasCSS ? strings.PlaceholderButtonTitleHasStyles : strings.PlaceholderButtonTitleNoStyles}</MessageBarButton>
</div>
}
>
{hasCSS ? strings.PlaceholderDescriptionHasStyles : strings.PlaceholderDescriptionNoStyles}
</MessageBar>
</div>
);
}
private _onAcceptDisclaimer() {
this.props.onAcceptDisclaimer();
}
private _onConfigure() {
this.props.context.propertyPane.open();
}
}

View File

@ -0,0 +1,10 @@
import { DisplayMode } from '@microsoft/sp-core-library';
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IEnhancedListFormattingProps {
css: string;
acceptedDisclaimer?: boolean;
displayMode: DisplayMode;
context: WebPartContext;
onAcceptDisclaimer: () => void;
}

View File

@ -0,0 +1,30 @@
define([], function() {
return {
DisclaimerText: `<h2 style="font-size: 20px;margin-top:0px;font-weight:400;">Warning!</h2>
This web part should ONLY be used to enhance your own custom HTML, such as List Formatting Definitions.<br/><br/>
Misuse of this web part can result in:
<ul>
<li> Loss of styles applied
<li> Broken styles
<li> User calls asking why SharePoint is "Broken"
<li> Tears
<li> Nightmares
<li> Blue Screens of Death
</ul>
Changing the CSS on a SharePoint page is unsupported. If you experience issues, please remove this web part.
<br/><br/>
By dismissing this message or clicking <b>I Accept</b>, you agree that will will use this web part responsibly.
`,
AcceptDisclaimerButton: "I Accept",
DismissDisclaimerAriaLabel: "Close and accept responsibility",
CSSDisclaimer: "You should only change the styles for your own custom CSS classes. Avoid changing global styles.<br/><br/>For more information on how to define custom CSS classes using list formatting, visit: <a href=\"https://aka.ms/list-formatting\" target='_blank'>aka.ms/list-formatting</a>.",
PlaceholderButtonTitleNoStyles: "Add custom styles",
PlaceholderButtonTitleHasStyles: "Change custom styles",
PlaceholderDescriptionNoStyles: "Edit this web part to add custom styles to your page.",
PlaceholderDescriptionHasStyles: "You are currently applying custom CSS styles to this page",
PlaceholderIconText: "Inject Custom CSS",
PropertyPaneDescription: `Use this web part to inject custom CSS to be used with list formatting. Don't worry, this web part is only visible when the page is in Edit mode.`,
BasicGroupName: "Custom CSS",
CSSFieldLabel: "Define your custom CSS"
}
});

View File

@ -0,0 +1,19 @@
declare interface IEnhancedListFormattingWebPartStrings {
DisclaimerText: string;
AcceptDisclaimerButton: string;
DismissDisclaimerAriaLabel: string;
CSSDisclaimer: string;
PlaceholderButtonTitleNoStyles: string;
PlaceholderButtonTitleHasStyles: string;
PlaceholderDescriptionNoStyles: string;
PlaceholderDescriptionHasStyles: string;
PlaceholderIconText: string;
PropertyPaneDescription: string;
BasicGroupName: string;
CSSFieldLabel: string;
}
declare module 'EnhancedListFormattingWebPartStrings' {
const strings: IEnhancedListFormattingWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,38 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/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": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "@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-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}