From 87143da454aa9766ee4e1fa46432335651a52ee1 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Sun, 20 Nov 2016 22:51:14 +0200 Subject: [PATCH 01/12] Change editor buttons to be button elts using onMouseDown. Fix #2, #3 --- lib/components/Button.js | 13 +++++++++---- lib/components/DraftailEditor.js | 8 ++------ lib/index.scss | 1 + 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/components/Button.js b/lib/components/Button.js index 961d81c4..e21fdbb3 100644 --- a/lib/components/Button.js +++ b/lib/components/Button.js @@ -1,13 +1,18 @@ import React from 'react'; +const onMouseDown = (onClick, e) => { + e.preventDefault(); + + onClick(); +}; + const Button = ({ icon, label, active, onClick }) => ( - {label} - + ); Button.propTypes = { diff --git a/lib/components/DraftailEditor.js b/lib/components/DraftailEditor.js index 8217ae4c..a60dade8 100644 --- a/lib/components/DraftailEditor.js +++ b/lib/components/DraftailEditor.js @@ -197,19 +197,15 @@ class DraftailEditor extends Component { return false; } - toggleBlockType(blockType, e) { + toggleBlockType(blockType) { const { editorState } = this.state; - e.preventDefault(); - this.onChange(RichUtils.toggleBlockType(editorState, blockType)); } - toggleInlineStyle(inlineStyle, e) { + toggleInlineStyle(inlineStyle) { const { editorState } = this.state; - e.preventDefault(); - this.onChange(RichUtils.toggleInlineStyle(editorState, inlineStyle)); } diff --git a/lib/index.scss b/lib/index.scss index 89681d56..296b1ada 100644 --- a/lib/index.scss +++ b/lib/index.scss @@ -23,6 +23,7 @@ $color-white: #ffffff; margin-right: 4px; margin-right: $button-padding; color: inherit; + border: none; cursor: pointer; &:hover { From b9318ce9ed5749edcfbd59bb9f5f92d2adc4ae93 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Sun, 20 Nov 2016 23:23:56 +0200 Subject: [PATCH 02/12] Add start of documentation --- docs/README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..44f8f042 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,30 @@ +draftail documentation +====================== + +## Other Draft.js editors + +> Full list on https://github.com/nikgraf/awesome-draft-js + +Other approaches: + +- https://github.com/ianstormtaylor/slate +- http://quilljs.com/ + +## Exporter behavior + +### Supported + +### Expected behavior + +### Unsupported scenarios + +- Nesting `ol` inside `ul` or the other way around. + +## R&D notes + +### Other Wagtail-integrated editors to learn from + +Things to borrow: keyboard shortcuts, Wagtail integration mechanism, + +- https://github.com/jaydensmith/wagtailfroala +- https://github.com/isotoma/wagtailtinymce From 52a23587aa3dbb4989fa3f3d305b77b5b2eda305 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Sun, 20 Nov 2016 23:47:41 +0200 Subject: [PATCH 03/12] Add GA tracking to examples site --- examples/index.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/index.html b/examples/index.html index d9fb0985..a52c389e 100644 --- a/examples/index.html +++ b/examples/index.html @@ -44,5 +44,11 @@

Usage

+ + + From 6b6aa9aaf38d8d3283f235c1bd0f85183158b580 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 23 Nov 2016 22:15:58 +0200 Subject: [PATCH 04/12] Fix button styles with button element --- lib/index.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.scss b/lib/index.scss index 296b1ada..02cc1198 100644 --- a/lib/index.scss +++ b/lib/index.scss @@ -23,7 +23,7 @@ $color-white: #ffffff; margin-right: 4px; margin-right: $button-padding; color: inherit; - border: none; + border: 0; cursor: pointer; &:hover { From 768deb8cf8369f0940233c64153f384fe0ee50a1 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 23 Nov 2016 22:16:30 +0200 Subject: [PATCH 05/12] Update documentation --- README.md | 1 + docs/README.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7746909a..9179fe0d 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ There are other, not so explicit dependencies at the moment: - jQuery 2, separately loaded onto the page. - Specific styles from Wagtail, including its icon font. +- ES6 polyfill (like for Draft.js) ## Development diff --git a/docs/README.md b/docs/README.md index 44f8f042..077b07fb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,7 +10,7 @@ Other approaches: - https://github.com/ianstormtaylor/slate - http://quilljs.com/ -## Exporter behavior +## Editor behavior ### Supported @@ -22,7 +22,7 @@ Other approaches: ## R&D notes -### Other Wagtail-integrated editors to learn from +### Other Wagtail-integrated editors to learn from. Things to borrow: keyboard shortcuts, Wagtail integration mechanism, From 66f32fc4c0faf3d1e69197b1345b4efd6106f7b8 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 23 Nov 2016 22:19:29 +0200 Subject: [PATCH 06/12] Expose onSave hook instead of auto field saving. Fix #23 --- examples/basic.js | 12 +++++++++-- examples/entities.js | 14 +++++++++++-- lib/api/conversion.js | 17 +++++++-------- lib/api/conversion.test.js | 6 +++--- lib/components/DraftailEditor.js | 36 ++++++++++++++++---------------- 5 files changed, 51 insertions(+), 34 deletions(-) diff --git a/examples/basic.js b/examples/basic.js index 44fa986d..de0642a6 100644 --- a/examples/basic.js +++ b/examples/basic.js @@ -24,7 +24,7 @@ const options = { ], }; -const value = { +const rawContentState = { entityMap: {}, blocks: [ { @@ -223,8 +223,16 @@ const value = { ], }; +const onSave = (contentState) => { + console.log('Save basic example:', contentState); +}; + const editor = ( - + ); ReactDOM.render(editor, mount); diff --git a/examples/entities.js b/examples/entities.js index 29df8668..6361f16e 100644 --- a/examples/entities.js +++ b/examples/entities.js @@ -88,11 +88,21 @@ const options = { ], }; +const saveField = document.createElement('input'); +saveField.type = 'hidden'; +mount.parentNode.appendChild(saveField); + +const onSave = (rawContentState) => { + const serialised = JSON.stringify(rawContentState); + saveField.value = serialised; + localStorage.setItem('entities:rawContentState', serialised); +}; + const editor = ( ); diff --git a/lib/api/conversion.js b/lib/api/conversion.js index da6c385d..67e96596 100644 --- a/lib/api/conversion.js +++ b/lib/api/conversion.js @@ -1,13 +1,12 @@ import { EditorState, convertFromRaw, convertToRaw } from 'draft-js'; export default { - // Serialised RawDraftContentState => EditorState. - createEditorState(serialisedState, decorators) { - const rawState = JSON.parse(serialisedState); + // RawDraftContentState + decorators => EditorState. + createEditorState(rawContentState, decorators) { let editorState; - if (rawState && Object.keys(rawState).length !== 0) { - const contentState = convertFromRaw(rawState); + if (rawContentState && Object.keys(rawContentState).length !== 0) { + const contentState = convertFromRaw(rawContentState); editorState = EditorState.createWithContent(contentState, decorators); } else { editorState = EditorState.createEmpty(decorators); @@ -16,17 +15,17 @@ export default { return editorState; }, - // EditorState => Serialised RawDraftContentState. + // EditorState => RawDraftContentState. serialiseEditorState(editorState) { const contentState = editorState.getCurrentContent(); - const rawContent = convertToRaw(contentState); + const rawContentState = convertToRaw(contentState); - const isEmpty = rawContent.blocks.every((block) => { + const isEmpty = rawContentState.blocks.every((block) => { return block.text.trim().length === 0 && block.entityRanges.length === 0 && block.inlineStyleRanges.length === 0; }); - return JSON.stringify(isEmpty ? {} : rawContent); + return isEmpty ? {} : rawContentState; }, }; diff --git a/lib/api/conversion.test.js b/lib/api/conversion.test.js index c6d3cbbf..f1b12e69 100644 --- a/lib/api/conversion.test.js +++ b/lib/api/conversion.test.js @@ -4,8 +4,8 @@ import conversion from '../api/conversion'; const emptyDecorator = new CompositeDecorator([]); const stubs = { - emptyContent: JSON.stringify({}), - realContent: JSON.stringify({ + emptyContent: {}, + realContent: { entityMap: {}, blocks: [ { @@ -27,7 +27,7 @@ const stubs = { data: {}, }, ], - }), + }, }; describe('conversion', () => { diff --git a/lib/components/DraftailEditor.js b/lib/components/DraftailEditor.js index a60dade8..058c86b0 100644 --- a/lib/components/DraftailEditor.js +++ b/lib/components/DraftailEditor.js @@ -41,10 +41,11 @@ const blockRenderMap = DefaultDraftBlockRenderMap.merge(Map({ class DraftailEditor extends Component { constructor(props) { super(props); - const { options } = props; + const { rawContentState, options } = props; + const decorators = new CompositeDecorator(options.decorators); this.state = { - editorState: conversion.createEditorState(props.value, new CompositeDecorator(options.decorators)), + editorState: conversion.createEditorState(rawContentState, decorators), }; let updateTimeout; @@ -54,7 +55,7 @@ class DraftailEditor extends Component { if (updateTimeout) { global.clearTimeout(updateTimeout); } - updateTimeout = global.setTimeout(this.saveRawState, STATE_SAVE_INTERVAL); + updateTimeout = global.setTimeout(this.saveState, STATE_SAVE_INTERVAL); }); }; @@ -72,7 +73,7 @@ class DraftailEditor extends Component { this.renderControls = this.renderControls.bind(this); this.onRequestDialog = this.onRequestDialog.bind(this); - this.saveRawState = this.saveRawState.bind(this); + this.saveState = this.saveState.bind(this); this.setSelectionToCurrentEntity = this.setSelectionToCurrentEntity.bind(this); this.updateState = this.updateState.bind(this); this.onDialogComplete = this.onDialogComplete.bind(this); @@ -111,10 +112,11 @@ class DraftailEditor extends Component { // } } - saveRawState() { + saveState() { + const { onSave } = this.props; const { editorState } = this.state; - this.inputRef.value = conversion.serialiseEditorState(editorState); + onSave(conversion.serialiseEditorState(editorState)); } // Sets a selection to encompass the containing entity. @@ -162,8 +164,8 @@ class DraftailEditor extends Component { updateState(state) { this.onChange(state); - // not sure we need this. - // setTimeout(() => this.editorRef.focus(), 0); + // not sure we need this. + // setTimeout(() => this.editorRef.focus(), 0); return true; } @@ -486,14 +488,13 @@ class DraftailEditor extends Component { } render() { - const { name } = this.props; const { editorState, readOnly } = this.state; return (
{ this.wrapperRef = ref; }} className="json-text" - onBlur={this.saveRawState} + onBlur={this.saveState} onClick={this.handleFocus} onMouseUp={this.onMouseUp} onKeyUp={this.onKeyUp} @@ -515,20 +516,14 @@ class DraftailEditor extends Component { {this.renderTooltip()} {this.renderDialog()} - - { this.inputRef = ref; }} - type="hidden" - name={name} - />
); } } DraftailEditor.propTypes = { - name: React.PropTypes.string.isRequired, - value: React.PropTypes.string.isRequired, + rawContentState: React.PropTypes.object, + onSave: React.PropTypes.func, options: React.PropTypes.shape({ modelPickerOptions: React.PropTypes.arrayOf(React.PropTypes.shape({ label: React.PropTypes.string.isRequired, @@ -546,4 +541,9 @@ DraftailEditor.propTypes = { }).isRequired, }; +DraftailEditor.defaultProps = { + rawContentState: {}, + onSave: () => {}, +}; + export default DraftailEditor; From 3e6e3a9a6866666383210fcd590d6cd9c9a0cdba Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 23 Nov 2016 22:22:13 +0200 Subject: [PATCH 07/12] Refactor function return values for linting --- lib/components/DraftailEditor.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/components/DraftailEditor.js b/lib/components/DraftailEditor.js index 058c86b0..dba296a5 100644 --- a/lib/components/DraftailEditor.js +++ b/lib/components/DraftailEditor.js @@ -164,8 +164,6 @@ class DraftailEditor extends Component { updateState(state) { this.onChange(state); - // not sure we need this. - // setTimeout(() => this.editorRef.focus(), 0); return true; } @@ -174,29 +172,29 @@ class DraftailEditor extends Component { const newState = RichUtils.onTab(event, editorState, MAX_LIST_NESTING); const isAtStart = newState.isSelectionAtStartOfContent(); const isAtEnd = newState.isSelectionAtEndOfContent(); + let ret; if (isAtStart && event.shiftKey) { - return; + ret = undefined; + } else if (isAtEnd) { + ret = undefined; + } else if (newState) { + ret = this.updateState(newState); } - if (isAtEnd) { - return; - } - - if (newState) { - return this.updateState(newState); - } + return ret; } handleKeyCommand(command) { const { editorState } = this.state; const newState = RichUtils.handleKeyCommand(editorState, command); + let ret = false; if (newState) { - return this.updateState(newState); + ret = this.updateState(newState); } - return false; + return ret; } toggleBlockType(blockType) { From 0bbbe7d6b1ffa20ecb274605c8486b193f73b6b1 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Fri, 25 Nov 2016 07:54:02 +0800 Subject: [PATCH 08/12] Document supported and upcoming keyboard shortcuts --- docs/README.md | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/docs/README.md b/docs/README.md index 077b07fb..03243781 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,18 +1,34 @@ draftail documentation ====================== -## Other Draft.js editors +## Editor behavior -> Full list on https://github.com/nikgraf/awesome-draft-js +### Keyboard shortcuts -Other approaches: +We support most of the common keyboard shortcuts users would expect to find in text editors thanks to [Draft.js key bindings](https://facebook.github.io/draft-js/docs/advanced-topics-key-bindings.html). -- https://github.com/ianstormtaylor/slate -- http://quilljs.com/ +Here are the most important shortcuts: -## Editor behavior +|Shortcut|Function| +|--------|--------| +|Cmd + B | Bolden text (if enabled) | +|Cmd + I | Italicise text (if enabled) | +|Cmd + U | Underline text (if enabled) | +|Cmd + J | Format as code (if enabled) | +|Cmd + Z | Undo | +|Cmd + Maj + Z | Redo | +|Cmd + Left | Move selection to start of block | +|Cmd + Right | Move selection to end of block | +|Cmd + Tab|Increase indentation of list items| +|Cmd + Maj + Tab|Decrease indentation of list items| -### Supported +Other shortcuts we would like to support in the future: + +|Shortcut|Function| +|--------|--------| +|Cmd + Option + 1/2/3/4/5/6 | Format as heading level | +|Cmd + Option + 0 | Format as paragraph | +|Cmd + K | Create a link (if enabled) | ### Expected behavior @@ -22,6 +38,15 @@ Other approaches: ## R&D notes +### Other Draft.js editors + +> Full list on https://github.com/nikgraf/awesome-draft-js + +Other approaches: + +- https://github.com/ianstormtaylor/slate +- http://quilljs.com/ + ### Other Wagtail-integrated editors to learn from. Things to borrow: keyboard shortcuts, Wagtail integration mechanism, From 2f7da7ccff4ef145ba036bc3f70c27142c117a3f Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Fri, 25 Nov 2016 11:36:28 +0800 Subject: [PATCH 09/12] Fix link removal not working --- lib/utils/DraftUtils.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/utils/DraftUtils.js b/lib/utils/DraftUtils.js index 71aced60..dbdb695f 100644 --- a/lib/utils/DraftUtils.js +++ b/lib/utils/DraftUtils.js @@ -22,7 +22,7 @@ export default { getEntityRange: DraftUtils.getEntityRange.bind(DraftUtils), - getEntityData: (entityKey) => { + getEntityData(entityKey) { const entity = Entity.get(entityKey); return entity.getData(); }, @@ -30,7 +30,7 @@ export default { /** * Returns the type of the block the current selection starts in. */ - getSelectedBlockType: (editorState) => { + getSelectedBlockType(editorState) { const selectionState = editorState.getSelection(); const contentState = editorState.getCurrentContent(); const startKey = selectionState.getStartKey(); @@ -42,7 +42,7 @@ export default { /** * Creates a selection for the entirety of an entity that can be partially selected. */ - getSelectedEntitySelection: (editorState) => { + getSelectedEntitySelection(editorState) { const selectionState = editorState.getSelection(); const contentState = editorState.getCurrentContent(); const entityKey = this.getSelectionEntity(editorState); @@ -61,14 +61,14 @@ export default { }); }, - hasCurrentInlineStyle: (editorState, style) => { + hasCurrentInlineStyle(editorState, style) { const currentStyle = editorState.getCurrentInlineStyle(); return currentStyle.has(style); }, // TODO Document. - insertBlock: (editorState, entityKey, character, blockType) => { + insertBlock(editorState, entityKey, character, blockType) { const contentState = editorState.getCurrentContent(); const selectionState = editorState.getSelection(); @@ -110,7 +110,7 @@ export default { }, // TODO Document. - createEntity: (editorState, entityType, entityData, entityText, entityMutability = 'IMMUTABLE') => { + createEntity(editorState, entityType, entityData, entityText, entityMutability = 'IMMUTABLE') { const entityKey = Entity.create(entityType, entityMutability, entityData); const contentState = editorState.getCurrentContent(); From 7ed73f5c098c3f4d629f0466b049d4de81cd2a04 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Fri, 25 Nov 2016 11:37:42 +0800 Subject: [PATCH 10/12] Add basic React tests to components --- lib/components/Button.js | 5 ++++ lib/components/Button.test.js | 13 +++++++++ lib/components/DraftailEditor.js | 42 ++++++++++++++++++++++++++- lib/components/DraftailEditor.test.js | 13 +++++++++ lib/components/Icon.test.js | 13 +++++++++ 5 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 lib/components/Button.test.js create mode 100644 lib/components/DraftailEditor.test.js create mode 100644 lib/components/Icon.test.js diff --git a/lib/components/Button.js b/lib/components/Button.js index e21fdbb3..7cc09e98 100644 --- a/lib/components/Button.js +++ b/lib/components/Button.js @@ -22,4 +22,9 @@ Button.propTypes = { active: React.PropTypes.bool, }; +Button.defaultProps = { + icon: '', + active: false, +}; + export default Button; diff --git a/lib/components/Button.test.js b/lib/components/Button.test.js new file mode 100644 index 00000000..fef8171f --- /dev/null +++ b/lib/components/Button.test.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Button from '../components/Button'; + +describe('Button', () => { + it('exists', () => { + expect(Button).toBeDefined(); + }); + + it('basic', () => { + expect(shallow(