diff --git a/lib/components/content.js b/lib/components/content.js index 889587b0e..866717229 100644 --- a/lib/components/content.js +++ b/lib/components/content.js @@ -37,6 +37,7 @@ class Content extends React.Component { readOnly: React.PropTypes.bool, renderMark: React.PropTypes.func.isRequired, renderNode: React.PropTypes.func.isRequired, + spellCheck: React.PropTypes.bool, state: React.PropTypes.object.isRequired, style: React.PropTypes.object }; @@ -47,6 +48,7 @@ class Content extends React.Component { static defaultProps = { readOnly: false, + spellCheck: true, style: {} }; @@ -376,6 +378,50 @@ class Content extends React.Component { this.props.onDrop(e, drop) } + /** + * On input, handle spellcheck and other similar edits that don't go trigger + * the `onBeforeInput` and instead update the DOM directly. + * + * @param {Event} e + */ + + onInput = (e) => { + let { state } = this.props + const { selection } = state + const native = window.getSelection() + const { anchorNode, anchorOffset, focusOffset } = native + const { textContent } = anchorNode + const offsetKey = OffsetKey.findKey(anchorNode) + const { key, index } = OffsetKey.parse(offsetKey) + const { start, end } = OffsetKey.findBounds(key, index, state) + const range = OffsetKey.findRange(anchorNode, state) + const { text, marks } = range + + // If the text is no different, abort. + if (textContent == text) return + + // Determine what the selection should be after changing the text. + const delta = textContent.length - text.length + const after = selection.collapseToEnd().moveForward(delta) + + // Create an updated state with the text replaced. + state = state + .transform() + .moveTo({ + anchorKey: key, + anchorOffset: start, + focusKey: key, + focusOffset: end + }) + .delete() + .insertText(textContent, marks) + .moveTo(after) + .apply() + + // Change the current state. + this.onChange(state) + } + /** * On key down, prevent the default behavior of certain commands that will * leave the editor in an out-of-sync state, then bubble up. @@ -547,6 +593,11 @@ class Content extends React.Component { ...this.props.style, } + // COMPAT: In Firefox, spellchecking can remove entire wrapping elements + // including inline ones like ``, which is jarring for the user but also + // causes the DOM to get into an irreconilable state. + const spellCheck = IS_FIREFOX ? false : this.props.spellCheck + return (
{children} diff --git a/lib/models/state.js b/lib/models/state.js index 154915b63..274770ffa 100644 --- a/lib/models/state.js +++ b/lib/models/state.js @@ -703,10 +703,11 @@ class State extends new Record(DEFAULTS) { * Insert a `text` string at the current selection. * * @param {String} text + * @param {Set} marks (optional) * @return {State} state */ - insertText(text) { + insertText(text, marks) { let state = this let { cursorMarks, document, selection } = state let after = selection @@ -721,7 +722,7 @@ class State extends new Record(DEFAULTS) { } // Insert the text and update the selection. - document = document.insertTextAtRange(selection, text, cursorMarks) + document = document.insertTextAtRange(selection, text, marks || cursorMarks) selection = after state = state.merge({ document, selection }) return state diff --git a/lib/utils/offset-key.js b/lib/utils/offset-key.js index e54a7f740..e275aa4b0 100644 --- a/lib/utils/offset-key.js +++ b/lib/utils/offset-key.js @@ -66,7 +66,7 @@ function findKey(element) { } /** - * Find the selection point from an `element`, `offset`, and list of `ranges`. + * Find the selection point from an `element`, `offset`, and `state`. * * @param {Element} element * @param {Offset} offset @@ -90,6 +90,23 @@ function findPoint(element, offset, state) { } } +/** + * Find the range from an `element`. + * + * @param {Element} element + * @param {State} state + * @return {Range} range + */ + +function findRange(element, state) { + const offsetKey = findKey(element) + const { key, index } = parse(offsetKey) + const text = state.document.getDescendant(key) + const ranges = text.getDecoratedRanges() + const range = ranges.get(index) + return range +} + /** * Parse an offset key `string`. * @@ -103,7 +120,7 @@ function parse(string) { const [ original, key, index ] = matches return { key, - index + index: parseInt(index, 10) } } @@ -128,6 +145,7 @@ export default { findBounds, findKey, findPoint, + findRange, parse, stringify } diff --git a/test/rendering/index.js b/test/rendering/index.js index 9696da82e..6c773e47c 100644 --- a/test/rendering/index.js +++ b/test/rendering/index.js @@ -54,6 +54,7 @@ function clean(html) { $(el).removeAttr('data-offset-key') }) + $.root().children().removeAttr('spellcheck') $.root().children().removeAttr('style') return $.html()