diff --git a/examples/index.js b/examples/index.js index 8904a7281..ece28c79a 100644 --- a/examples/index.js +++ b/examples/index.js @@ -21,6 +21,7 @@ import RTL from './rtl' import ReadOnly from './read-only' import RichText from './rich-text' import SearchHighlighting from './search-highlighting' +import SyncingOperations from './syncing-operations' import Tables from './tables' /** @@ -44,11 +45,12 @@ const EXAMPLES = [ ['Tables', Tables, '/tables'], ['Paste HTML', PasteHtml, '/paste-html'], ['Search Highlighting', SearchHighlighting, '/search-highlighting'], + ['Syncing Operations', SyncingOperations, '/syncing-operations'], ['Read-only', ReadOnly, '/read-only'], ['RTL', RTL, '/rtl'], ['Plugins', Plugins, '/plugins'], ['Forced Layout', ForcedLayout, '/forced-layout'], - ['Huge', HugeDocument, '/huge-document'], + ['Huge Document', HugeDocument, '/huge-document'], ] /** diff --git a/examples/rich-text/index.js b/examples/rich-text/index.js index f2c4667c0..f1a91710c 100644 --- a/examples/rich-text/index.js +++ b/examples/rich-text/index.js @@ -51,7 +51,7 @@ const schema = { * @type {Component} */ -class RichText extends React.Component { +class RichTextExample extends React.Component { /** * Deserialize the initial editor state. @@ -305,4 +305,4 @@ class RichText extends React.Component { * Export. */ -export default RichText +export default RichTextExample diff --git a/examples/syncing-operations/Readme.md b/examples/syncing-operations/Readme.md new file mode 100644 index 000000000..d4aa063d3 --- /dev/null +++ b/examples/syncing-operations/Readme.md @@ -0,0 +1,8 @@ + +# Rich Text Example + +![](../../docs/images/rich-text-example.png) + +This example shows you can add a very different concepts together: key commands, toolbars, and custom formatting, to get the functionality you'd expect from a rich text editor. Of course this is just the beginning, you can layer in whatever other behaviors you want! + +Check out the [Examples readme](..) to see how to run it! diff --git a/examples/syncing-operations/index.js b/examples/syncing-operations/index.js new file mode 100644 index 000000000..04736fc4a --- /dev/null +++ b/examples/syncing-operations/index.js @@ -0,0 +1,300 @@ + +import { Editor } from 'slate-react' +import { State } from 'slate' + +import React from 'react' +import initialState from './state.json' + +/** + * Define a schema. + * + * @type {Object} + */ + +const schema = { + marks: { + bold: { + fontWeight: 'bold' + }, + code: { + fontFamily: 'monospace', + backgroundColor: '#eee', + padding: '3px', + borderRadius: '4px' + }, + italic: { + fontStyle: 'italic' + }, + underlined: { + textDecoration: 'underline' + } + } +} + +/** + * A simple editor component to demo syncing with. + * + * @type {Component} + */ + +class SyncingEditor extends React.Component { + + /** + * Deserialize the initial editor state. + * + * @type {Object} + */ + + state = { + state: State.fromJSON(initialState), + } + + /** + * When new `operations` are received from one of the other editors that is in + * sync with this one, apply them in a new change. + * + * @param {Array} operations + */ + + applyOperations = (operations) => { + const { state } = this.state + const change = state.change().applyOperations(operations) + this.onChange(change, { remote: true }) + } + + /** + * Check if the current selection has a mark with `type` in it. + * + * @param {String} type + * @return {Boolean} + */ + + hasMark = (type) => { + const { state } = this.state + return state.activeMarks.some(mark => mark.type == type) + } + + /** + * On change, save the new `state`. And if it's a local change, call the + * passed-in `onChange` handler. + * + * @param {Change} change + * @param {Object} options + */ + + onChange = (change, options = {}) => { + this.setState({ state: change.state }) + + if (!options.remote) { + this.props.onChange(change) + } + } + + /** + * On key down, if it's a formatting command toggle a mark. + * + * @param {Event} e + * @param {Object} data + * @param {Change} change + * @return {Change} + */ + + onKeyDown = (e, data, change) => { + if (!data.isMod) return + let mark + + switch (data.key) { + case 'b': + mark = 'bold' + break + case 'i': + mark = 'italic' + break + case 'u': + mark = 'underlined' + break + case '`': + mark = 'code' + break + default: + return + } + + e.preventDefault() + change.toggleMark(mark) + return true + } + + /** + * When a mark button is clicked, toggle the current mark. + * + * @param {Event} e + * @param {String} type + */ + + onClickMark = (e, type) => { + e.preventDefault() + const { state } = this.state + const change = state.change().toggleMark(type) + this.onChange(change) + } + + /** + * Render. + * + * @return {Element} + */ + + render() { + return ( +
+ {this.renderToolbar()} + {this.renderEditor()} +
+ ) + } + + /** + * Render the toolbar. + * + * @return {Element} + */ + + renderToolbar = () => { + return ( +
+ {this.renderButton('bold', 'format_bold')} + {this.renderButton('italic', 'format_italic')} + {this.renderButton('underlined', 'format_underlined')} + {this.renderButton('code', 'code')} +
+ ) + } + + /** + * Render a mark-toggling toolbar button. + * + * @param {String} type + * @param {String} icon + * @return {Element} + */ + + renderButton = (type, icon) => { + const isActive = this.hasMark(type) + const onMouseDown = e => this.onClickMark(e, type) + + return ( + + {icon} + + ) + } + + /** + * Render the editor. + * + * @return {Element} + */ + + renderEditor = () => { + return ( +
+ +
+ ) + } + +} + +/** + * The syncing operations example. + * + * @type {Component} + */ + +class SyncingOperationsExample extends React.Component { + + /** + * Save a reference to editor `one`. + * + * @param {SyncingEditor} one + */ + + oneRef = (one) => { + this.one = one + } + + /** + * Save a reference to editor `two`. + * + * @param {SyncingEditor} two + */ + + twoRef = (two) => { + this.two = two + } + + /** + * When editor one changes, send document-alterting operations to edtior two. + * + * @param {Array} operations + */ + + onOneChange = (change) => { + const ops = change.operations.filter(o => o.type != 'set_selection' && o.type != 'set_state') + this.two.applyOperations(ops) + } + + /** + * When editor two changes, send document-alterting operations to edtior one. + * + * @param {Array} operations + */ + + onTwoChange = (change) => { + const ops = change.operations.filter(o => o.type != 'set_selection' && o.type != 'set_state') + this.one.applyOperations(ops) + } + + /** + * Render both editors. + * + * @return {Element} + */ + + render() { + return ( +
+ +
+ +
+ ) + } + +} + +/** + * Export. + */ + +export default SyncingOperationsExample diff --git a/examples/syncing-operations/state.json b/examples/syncing-operations/state.json new file mode 100644 index 000000000..730f7626d --- /dev/null +++ b/examples/syncing-operations/state.json @@ -0,0 +1,78 @@ +{ + "document": { + "nodes": [ + { + "kind": "block", + "type": "paragraph", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": "These two editors are kept " + }, + { + "text": "in sync", + "marks": [ + { + "type": "bold" + } + ] + }, + { + "text": " with one another as you type!" + } + ] + } + ] + }, + { + "kind": "block", + "type": "paragraph", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": "They achieve this by sending any document-altering operations to each other whenever a change occurs, and then applying them locally with " + }, + { + "text": "change.applyOperations()", + "marks": [ + { + "type": "code" + } + ] + }, + { + "text": "." + } + ] + } + ] + }, + { + "kind": "block", + "type": "paragraph", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": "Note: ", + "marks": [ + { + "type": "italic" + } + ] + }, + { + "text": "this example doesn't showcase operational transforms or network communication, which are required for realtime editing with multiple people at once." + } + ] + } + ] + } + ] + } +}