RichTextEditor in Spfx

Introduction

 
react-draft-wysiwyg is a 3rd party plugin to Create RichTextEditor in React.
 

Properties

 
Styling the editor
 
The editor by default will use DraftJS editor, as it is without any styling and will occupy 100% width of the container. Some styling to add border to editor and set width would be nice.
  1. wrapperClassName: class applied around both the editor and the toolbar
  2. editorClassName: class applied around the editor
  3. toolbarClassName: class applied around the toolbar
  4. wrapperStyle: style object applied around both the editor and the toolbar
  5. editorStyle: style object applied around the editor
  6. toolbarStyle: style object applied around the toolbar
Editor state
 
The editor can be implemented as a controlled component using EditorState or un-controlled component using EditorState or RawDraftContentState.
  1. defaultEditorState: Property to initialize editor state once when its created.
  2. editorState: Property to update editor state in a controlled way.
  3. onEditorStateChange: Function is called each time there is a change in the state of the editor. A function argument passed is an object of EditorState.
  4. defaultContentState: Property to initialize editor state once when its created. 
  5. contentState: Property to update editor state in a controlled way.
  6. onChange: Function is called each time there is a change in state of the editor. The function argument passed is an object of type RawDraftContentState.
  7. onContentStateChange: Function is called each time there is a change in state of the editor, function argument passed is object of type RawDraftContentState.
toolbar
 
The toolbar property provides a lot of parameters to customize the pre-built option in the toolbar. The default value of toolbar property is as shown below:
  1. {  
  2.   options: ['inline''blockType''fontSize''fontFamily''list''textAlign''colorPicker''link''embedded''emoji''image''remove''history'],  
  3.   inline: {  
  4.     inDropdown: false,  
  5.     className: undefined,  
  6.     component: undefined,  
  7.     dropdownClassName: undefined,  
  8.     options: ['bold''italic''underline''strikethrough''monospace''superscript''subscript'],  
  9.     bold: { icon: bold, className: undefined },  
  10.     italic: { icon: italic, className: undefined },  
  11.     underline: { icon: underline, className: undefined },  
  12.     strikethrough: { icon: strikethrough, className: undefined },  
  13.     monospace: { icon: monospace, className: undefined },  
  14.     superscript: { icon: superscript, className: undefined },  
  15.     subscript: { icon: subscript, className: undefined },  
  16.   },  
  17.   blockType: {  
  18.     inDropdown: true,  
  19.     options: ['Normal''H1''H2''H3''H4''H5''H6''Blockquote''Code'],  
  20.     className: undefined,  
  21.     component: undefined,  
  22.     dropdownClassName: undefined,  
  23.   },  
  24.   fontSize: {  
  25.     icon: fontSize,  
  26.     options: [8, 9, 10, 11, 12, 14, 16, 18, 24, 30, 36, 48, 60, 72, 96],  
  27.     className: undefined,  
  28.     component: undefined,  
  29.     dropdownClassName: undefined,  
  30.   },  
  31.   fontFamily: {  
  32.     options: ['Arial''Georgia''Impact''Tahoma''Times New Roman''Verdana'],  
  33.     className: undefined,  
  34.     component: undefined,  
  35.     dropdownClassName: undefined,  
  36.   },  
  37.   list: {  
  38.     inDropdown: false,  
  39.     className: undefined,  
  40.     component: undefined,  
  41.     dropdownClassName: undefined,  
  42.     options: ['unordered''ordered''indent''outdent'],  
  43.     unordered: { icon: unordered, className: undefined },  
  44.     ordered: { icon: ordered, className: undefined },  
  45.     indent: { icon: indent, className: undefined },  
  46.     outdent: { icon: outdent, className: undefined },  
  47.   },  
  48.   textAlign: {  
  49.     inDropdown: false,  
  50.     className: undefined,  
  51.     component: undefined,  
  52.     dropdownClassName: undefined,  
  53.     options: ['left''center''right''justify'],  
  54.     left: { icon: left, className: undefined },  
  55.     center: { icon: center, className: undefined },  
  56.     right: { icon: right, className: undefined },  
  57.     justify: { icon: justify, className: undefined },  
  58.   },  
  59.   colorPicker: {  
  60.     icon: color,  
  61.     className: undefined,  
  62.     component: undefined,  
  63.     popupClassName: undefined,  
  64.     colors: ['#61bd6d''#1abc9c''#54acd2''#2c82c9',  
  65.       '#9365b8''#475577''#cccccc''#41a85f''#00a885',  
  66.       '#3d8eb9''#2969b0''#553982''#28324e''#000000',  
  67.       '#f7da64''#fba026''#eb6b56''#e25041''#a38f84',  
  68.       '#efefef''#ffffff''#fac51c''#f37934''#d14841',  
  69.       '#b8312f''#7c706b''#d1d5d8'],  
  70.   },  
  71.   link: {  
  72.     inDropdown: false,  
  73.     className: undefined,  
  74.     component: undefined,  
  75.     popupClassName: undefined,  
  76.     dropdownClassName: undefined,  
  77.     showOpenOptionOnHover: true,  
  78.     defaultTargetOption: '_self',  
  79.     options: ['link''unlink'],  
  80.     link: { icon: link, className: undefined },  
  81.     unlink: { icon: unlink, className: undefined },  
  82.     linkCallback: undefined  
  83.   },  
  84.   emoji: {  
  85.     icon: emoji,  
  86.     className: undefined,  
  87.     component: undefined,  
  88.     popupClassName: undefined,  
  89.     emojis: [  
  90.       '๐Ÿ˜€''๐Ÿ˜''๐Ÿ˜‚''๐Ÿ˜ƒ''๐Ÿ˜‰''๐Ÿ˜‹''๐Ÿ˜Ž''๐Ÿ˜''๐Ÿ˜—''๐Ÿค—''๐Ÿค”''๐Ÿ˜ฃ''๐Ÿ˜ซ''๐Ÿ˜ด''๐Ÿ˜Œ''๐Ÿค“',  
  91.       '๐Ÿ˜›''๐Ÿ˜œ''๐Ÿ˜ ''๐Ÿ˜‡''๐Ÿ˜ท''๐Ÿ˜ˆ''๐Ÿ‘ป''๐Ÿ˜บ''๐Ÿ˜ธ''๐Ÿ˜น''๐Ÿ˜ป''๐Ÿ˜ผ''๐Ÿ˜ฝ''๐Ÿ™€''๐Ÿ™ˆ',  
  92.       '๐Ÿ™‰''๐Ÿ™Š''๐Ÿ‘ผ''๐Ÿ‘ฎ''๐Ÿ•ต''๐Ÿ’‚''๐Ÿ‘ณ''๐ŸŽ…''๐Ÿ‘ธ''๐Ÿ‘ฐ''๐Ÿ‘ฒ''๐Ÿ™''๐Ÿ™‡''๐Ÿšถ''๐Ÿƒ''๐Ÿ’ƒ',  
  93.       'โ›ท''๐Ÿ‚''๐ŸŒ''๐Ÿ„''๐Ÿšฃ''๐ŸŠ''โ›น''๐Ÿ‹''๐Ÿšด''๐Ÿ‘ซ''๐Ÿ’ช''๐Ÿ‘ˆ''๐Ÿ‘‰''๐Ÿ‘‰''๐Ÿ‘†''๐Ÿ–•',  
  94.       '๐Ÿ‘‡''๐Ÿ––''๐Ÿค˜''๐Ÿ–''๐Ÿ‘Œ''๐Ÿ‘''๐Ÿ‘Ž''โœŠ''๐Ÿ‘Š''๐Ÿ‘''๐Ÿ™Œ''๐Ÿ™''๐Ÿต''๐Ÿถ''๐Ÿ‡''๐Ÿฅ',  
  95.       '๐Ÿธ''๐ŸŒ''๐Ÿ›''๐Ÿœ''๐Ÿ''๐Ÿ‰''๐Ÿ„''๐Ÿ”''๐Ÿค''๐Ÿจ''๐Ÿช''๐ŸŽ‚''๐Ÿฐ''๐Ÿพ''๐Ÿท''๐Ÿธ',  
  96.       '๐Ÿบ''๐ŸŒ''๐Ÿš‘''⏰''๐ŸŒ™''๐ŸŒ''๐ŸŒž''โญ''๐ŸŒŸ''๐ŸŒ ''๐ŸŒจ''๐ŸŒฉ''โ›„''๐Ÿ”ฅ''๐ŸŽ„''๐ŸŽˆ',  
  97.       '๐ŸŽ‰''๐ŸŽŠ''๐ŸŽ''๐ŸŽ—''๐Ÿ€''๐Ÿˆ''๐ŸŽฒ''๐Ÿ”‡''๐Ÿ”ˆ''๐Ÿ“ฃ''๐Ÿ””''๐ŸŽต''๐ŸŽท''๐Ÿ’ฐ''๐Ÿ–Š''๐Ÿ“…',  
  98.       'โœ…''โŽ''๐Ÿ’ฏ',  
  99.     ],  
  100.   },  
  101.   embedded: {  
  102.     icon: embedded,  
  103.     className: undefined,  
  104.     component: undefined,  
  105.     popupClassName: undefined,  
  106.     embedCallback: undefined,  
  107.     defaultSize: {  
  108.       height: 'auto',  
  109.       width: 'auto',  
  110.     },  
  111.   },  
  112.   image: {  
  113.     icon: image,  
  114.     className: undefined,  
  115.     component: undefined,  
  116.     popupClassName: undefined,  
  117.     urlEnabled: true,  
  118.     uploadEnabled: true,  
  119.     alignmentEnabled: true,  
  120.     uploadCallback: undefined,  
  121.     previewImage: false,  
  122.     inputAccept: 'image/gif,image/jpeg,image/jpg,image/png,image/svg',  
  123.     alt: { present: false, mandatory: false },  
  124.     defaultSize: {  
  125.       height: 'auto',  
  126.       width: 'auto',  
  127.     },  
  128.   },  
  129.   remove: { icon: eraser, className: undefined, component: undefined },  
  130.   history: {  
  131.     inDropdown: false,  
  132.     className: undefined,  
  133.     component: undefined,  
  134.     dropdownClassName: undefined,  
  135.     options: ['undo''redo'],  
  136.     undo: { icon: undo, className: undefined },  
  137.     redo: { icon: redo, className: undefined },  
  138.   },  
  139. }  
Various parameters and their uses are:
  1. options: An array of available options in the toolbar and in each menu option. Only those options specified in this property are added to the toolbar and in the order in which they are specified. By default, all options are present. In the case of fontSize, options can be used to add more font-sizes.
  2. classname: This property can be used to add a classname to buttons, dropdowns, and popups in the toolbar.
  3. inDropdown: This property can be used to group the options in dropdown.
  4. component: This property can be used to configure a custom react component to be used for toolbar options, instead of the pre-built ones.
  5. icon: This can be used to specify an icon for toolbar buttons.
  6. colorPicker: colors: This is an array of colors to be shown in color-picker. The value should be of RGB value.
  7. link: showOpenOptionOnHover: If this is true, a small arrow icon is shown over links on hover. Clicking this icon will open the link in a new tab. Value is true by default.
  8. link: defaultTargetOption: This property sets the target of link in the editor. Default value is '_self'.
  9. link: linkCallback: This is a callback to process the link added by the user. By default, the library linkify-it is used for the purpose.
  10. The callback is passed an object with following details{title: <text>,target: <link>,targetOption: <_blank|_self|_parent|_top>}. It is expected to return a similar object with new details that will be saved in the link.
  11. emoji: emojis: The property is an array of emoji characters (unicodes). Which are shown in the emoji option.
  12. embedded: defaultSize: This property can be used to pass default size (height and width) of embedded links in the editor. The default values are 'auto'.
  13. embedded: embedCallBack: This callback is called after the user add a URL to be embedded, it can be used to do any required modifications to the URL. The callback is passed to a URL and should return the URL only.
  14. image: urlEnabled: The property can be used to configure if the option to specify an image source URL should be enabled. The default value is true.
  15. image: uploadEnabled: The property can be used to configure if the option to upload an image is enabled. The default value is true.
  16. image: uploadCallback: This is image upload callBack. It should return a promise that resolves to give image src. The default value is true.
  17. Both the above options of uploadEnabled and uploadCallback should be present for upload to be enabled.
  18. Promise should resolve to return an object { data: { link: <THE_URL>}}. 
  19. image: previewImage: The property can be used to configure image preview after upload in image popup, false by default.
  20. image: alignmentEnabled: The property can be used to configure if image alignment should be enabled. Alignment options are LEFT, RIGHT and CENTER. The default value is true.
  21. image: inputAccept: The property can be used to configure which file types should be allowed to upload by file input for image upload.
  22. image: alt: The property can be used to enable the alt field for images and optionally make it mandatory.
  23. image: defaultSize: This property can be used to pass default size (height and width) of an image in the editor. The default values are 'auto'.
Enabling mentions
  1. Mentions can be enabled in the editor as shown in the example below. The separator is a character that separates a mention from word preceding it. The default value is space ' '. A trigger is a character that causes mention suggestions to appear, default value is '@'. Each suggestion has 3 properties:
  2. text: this is a value that is displayed in the editor.
  3. value: the filtering of suggestions is done using this value.
  4. URL: mention is added as link to editor using this 'URL' in href. This is optional and if not present 'value' is used instead of this.
Enabling hashtag
 
Hashtag can be enabled in the editor as showed in the example below. Separator is a character that separates a mention from word preceding it, default value is space ' '. A trigger is a character that causes mention suggestions to appear, default value is '#'.
 
Open a command prompt. Create a directory for the SPFx solution.
 
md spfx-RichTextEditor
 
Navigate to the above-created directory.
 
cd spfx-RichTextEditor
 
Run the Yeoman SharePoint Generator to create the solution.
 
yo @microsoft/sharepoint
 
Solution Name
 
Hit Enter to have the default name (spfx-RichTextEditor in this case) or type in any other name for your solution.
Selected choice - Hit Enter
 
Target for the component
 
Here, we can select the target environment where we are planning to deploy the client web part, i.e., SharePoint Online or SharePoint OnPremise (SharePoint 2016 onwards).
Selected choice - SharePoint Online only (latest)
 
Place of files
 
We may choose to use the same folder or create a subfolder for our solution.
Selected choice - Same folder
 
Deployment option
 
Selecting Y will allow the app to be deployed instantly to all sites and be accessible everywhere.
Selected choice - N (install on each site explicitly)
 
Permissions to access web APIs
 
Choose if the components in the solution require permission to access web APIs that are unique and not shared with other components in the tenant.
Selected choice - N (solution contains unique permissions)
 
Type of client-side component to create
 
We can choose to create a client-side web part or an extension. Choose the web part option.
Selected choice - WebPart
 
Web part name
 
Hit Enter to select the default name or type in any other name.
Selected choice - SpfxRichTextEditor
 
Web part description
 
Hit Enter to select the default description or type in any other value.
 
Framework to use
 
Select any JavaScript framework to develop the component. Available choices are - No JavaScript Framework, React, and Knockout.
Selected choice - React
 
The Yeoman generator will perform a scaffolding process to generate the solution. The scaffolding process will take a significant amount of time.
 
Once the scaffolding process is completed, lock down the version of project dependencies by running the below command,
 
npm shrinkwrap
 
In the command prompt, type below command to open the solution in the code editor of your choice.
 
code .
 
NPM Packages Used,
 
On the command prompt, run the below command:
  1. npm install  react-draft-wysiwyg   (For Editor)
  1. npm i setimmediate  (for Resolve Error in React-dom whil intializing Editor)
  1. npm i draftjs-to-html  (for Converting Text editor content to html format)
  1. npm i prop-types  (Runtime type checking for React props and similar objects.)
Basic Initialization  
  1. <Editor  
  2.   toolbarHidden  
  3.   wrapperClassName="wrapper-class"  
  4.   editorClassName="editor-class"  
  5.   toolbarClassName="toolbar-class"  
  6. />  
Where toolbarHidden hides the default tool bar in SpfxRichTextEditor.tsx:
  1. import * as React from 'react';  
  2. import styles from './SpfxRichTextEditor.module.scss';  
  3. import { ISpfxRichTextEditorProps } from './ISpfxRichTextEditorProps';  
  4. import 'setimmediate';  
  5. import * as PropTypes from 'prop-types';  
  6. import { EditorState, convertToRaw,Modifier, ContentState, convertFromHTML } from 'draft-js';  
  7. import { Editor } from 'react-draft-wysiwyg';  
  8. import draftToHtml from 'draftjs-to-html';  
  9. /*import htmlToDraft from 'html-to-draftjs';*/  
  10. require('./main.css');  
  11. import { RichUtils } from 'draft-js';  
  12.   
  13. export default class SpfxRichTextEditor extends React.Component<ISpfxRichTextEditorProps, EditorState> {  
  14.   constructor(props) {  
  15.     super(props);  
  16.     this.state = {  
  17.       editorState: EditorState.createEmpty(),  
  18.     };  
  19.     /*this.state = { 
  20.       editorState: EditorState.createWithContent( 
  21.         ContentState.createFromBlockArray( 
  22.           convertFromHTML('<p>Heloโญ<ins>as</ins><strong><ins>asdsadsfsd </ins></strong><sup><strong><ins>scfsds</ins></strong></sup></p>') 
  23.         ) 
  24.       ), 
  25.     };*/  
  26.   }  
  27.    
  28.   private onEditorStateChange: Function = (editorState) => {  
  29.     this.setState({  
  30.       editorState,  
  31.     });  
  32.   }  
  33.   
  34.   public render(): React.ReactElement<ISpfxRichTextEditorProps> {  
  35.     const { editorState } = this.state;  
  36.     let hashConfig = {  
  37.       trigger: '#',  
  38.       separator: ' ',  
  39.     };  
  40.     return (  
  41.       <div className={ styles.spfxRichTextEditor }>  
  42.         <div className={ styles.container }>  
  43.         <Editor  
  44.           editorState={editorState}  
  45.           wrapperClassName="demo-wrapper"  
  46.           editorClassName="demo-editor"  
  47.           onEditorStateChange={this.onEditorStateChange}  
  48.           mention={{  
  49.             separator: ' ',  
  50.             trigger: '@',  
  51.             suggestions: [  
  52.               { text: 'APPLE', value: 'apple', url: 'apple' },  
  53.               { text: 'BANANA', value: 'banana', url: 'banana' },  
  54.               { text: 'CHERRY', value: 'cherry', url: 'cherry' },  
  55.               { text: 'DURIAN', value: 'durian', url: 'durian' },  
  56.               { text: 'EGGFRUIT', value: 'eggfruit', url: 'eggfruit' },  
  57.               { text: 'FIG', value: 'fig', url: 'fig' },  
  58.               { text: 'GRAPEFRUIT', value: 'grapefruit', url: 'grapefruit' },  
  59.               { text: 'HONEYDEW', value: 'honeydew', url: 'honeydew' },  
  60.             ],  
  61.           }}  
  62.           hashtag={{}}  
  63.           toolbarCustomButtons={[<CustomOption />]}  
  64.         />  
  65.         <textarea  
  66.           disabled  
  67.             
  68.           value={draftToHtml(convertToRaw(editorState.getCurrentContent()), hashConfig)}  
  69.         />  
  70.         </div>  
  71.       </div>  
  72.     );  
  73.   }  
  74. }  
  75. class CustomOption extends React.Component<EditorState> {  
  76.   constructor(props) {  
  77.     super(props);  
  78.       
  79.   }  
  80.   static  propTypes = {  
  81.     onChange: PropTypes.func,  
  82.     editorState: PropTypes.object,  
  83.   };  
  84.   
  85.   private addStar: Function = (): void => {  
  86.     const { editorState, onChange } = this.props;  
  87.     const contentState = Modifier.replaceText(  
  88.       editorState.getCurrentContent(),  
  89.       editorState.getSelection(),  
  90.       'โญ',  
  91.       editorState.getCurrentInlineStyle(),  
  92.     );  
  93.     onChange(EditorState.push(editorState, contentState, 'insert-characters'));  
  94.   }  
  95.   
  96.   public render(): React.ReactElement {  
  97.     return (  
  98.       <div onClick={() => this.addStar()} >โญ</div>  
  99.     );  
  100.   }  
  101. }  
  102. /*  
  103.  
  104. class CustomOption extends Component { 
  105.   static propTypes = { 
  106.     onChange: PropTypes.func, 
  107.     editorState: PropTypes.object, 
  108.   }; 
  109.  
  110.   toggleBold: Function = (): void => { 
  111.     const { editorState, onChange } = this.props; 
  112.     const newState = RichUtils.toggleInlineStyle( 
  113.       editorState, 
  114.       'BOLD', 
  115.     ); 
  116.     if (newState) { 
  117.       onChange(newState); 
  118.     } 
  119.   }; 
  120.  
  121.   render() { 
  122.     return ( 
  123.       <div className="rdw-storybook-custom-option" onClick={this.toggleBold}>B</div> 
  124.     ); 
  125.   } 
  126. } 
  127.  
  128. */  
For main.css, copy the styles from this link.
 
In this example, I am using the concept of hashtag, mentions, custom options, etc... To set the value to texteditor, use the following method for controlled component.
  1. this.state = {  
  2.       editorState: EditorState.createWithContent(  
  3.         ContentState.createFromBlockArray(  
  4.           convertFromHTML('<p>Heloโญ<ins>as</ins><strong><ins>asdsadsfsd </ins></strong><sup><strong><ins>scfsds</ins></strong></sup></p>')  
  5.         )  
  6.       ),  
  7.     };  
For hashtags, use format type as shown below:
  1. let hashConfig = {  
  2.     trigger: '#',  
  3.     separator: ' ',  
  4.   };  
 To convert data to HTML, the below method is used:
  1. draftToHtml(convertToRaw(editorState.getCurrentContent()), hashConfig)  
  For Mentions, use the below code:
  1. mention={{    
  2.          separator: ' ',    
  3.          trigger: '@',    
  4.          suggestions: [    
  5.            { text: 'APPLE', value: 'apple', url: 'apple' },    
  6.            { text: 'BANANA', value: 'banana', url: 'banana' },    
  7.            { text: 'CHERRY', value: 'cherry', url: 'cherry' },    
  8.            { text: 'DURIAN', value: 'durian', url: 'durian' },    
  9.            { text: 'EGGFRUIT', value: 'eggfruit', url: 'eggfruit' },    
  10.            { text: 'FIG', value: 'fig', url: 'fig' },    
  11.            { text: 'GRAPEFRUIT', value: 'grapefruit', url: 'grapefruit' },    
  12.            { text: 'HONEYDEW', value: 'honeydew', url: 'honeydew' },    
  13.          ],    
  14.        }}    
  15.        hashtag={{}}   
Sample Output 
 
 
 
For more examples, please click here.
 
Hope this helps someone, Happy Coding :)