From bfbbd6d8fb7e0c4c2d9acfb2f6b9b462ab98786f Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Tue, 29 Nov 2016 12:35:46 -0800 Subject: [PATCH] cleanup logging, fix inline void selection --- src/components/content.js | 193 +++++++++++++++++++------------------- src/components/editor.js | 20 ++-- src/components/leaf.js | 20 ++-- src/components/node.js | 9 +- src/components/void.js | 44 +++++++-- src/plugins/core.js | 9 +- 6 files changed, 166 insertions(+), 129 deletions(-) diff --git a/src/components/content.js b/src/components/content.js index 102a74cb5..388f77c66 100644 --- a/src/components/content.js +++ b/src/components/content.js @@ -174,34 +174,34 @@ class Content extends React.Component { /** * On before input, bubble up. * - * @param {Event} e + * @param {Event} event */ - onBeforeInput = (e) => { + onBeforeInput = (event) => { if (this.props.readOnly) return - if (!this.isInContentEditable(e)) return + if (!this.isInContentEditable(event)) return const data = {} - debug('onBeforeInput', data) - this.props.onBeforeInput(e, data) + debug('onBeforeInput', { event, data }) + this.props.onBeforeInput(event, data) } /** * On blur, update the selection to be not focused. * - * @param {Event} e + * @param {Event} event */ - onBlur = (e) => { + onBlur = (event) => { if (this.props.readOnly) return if (this.tmp.isCopying) return - if (!this.isInContentEditable(e)) return + if (!this.isInContentEditable(event)) return const data = {} - debug('onBlur', data) - this.props.onBlur(e, data) + debug('onBlur', { event, data }) + this.props.onBlur(event, data) } /** @@ -218,16 +218,16 @@ class Content extends React.Component { /** * On composition start, set the `isComposing` flag. * - * @param {Event} e + * @param {Event} event */ - onCompositionStart = (e) => { - if (!this.isInContentEditable(e)) return + onCompositionStart = (event) => { + if (!this.isInContentEditable(event)) return this.tmp.isComposing = true this.tmp.compositions++ - debug('onCompositionStart') + debug('onCompositionStart', { event }) } /** @@ -235,11 +235,11 @@ class Content extends React.Component { * increment the `forces` key, which will force the contenteditable element * to completely re-render, since IME puts React in an unreconcilable state. * - * @param {Event} e + * @param {Event} event */ - onCompositionEnd = (e) => { - if (!this.isInContentEditable(e)) return + onCompositionEnd = (event) => { + if (!this.isInContentEditable(event)) return this.forces++ const count = this.tmp.compositions @@ -252,18 +252,18 @@ class Content extends React.Component { this.tmp.isComposing = false }) - debug('onCompositionEnd') + debug('onCompositionEnd', { event }) } /** * On copy, defer to `onCutCopy`, then bubble up. * - * @param {Event} e + * @param {Event} event */ - onCopy = (e) => { - if (!this.isInContentEditable(e)) return - const window = getWindow(e.target) + onCopy = (event) => { + if (!this.isInContentEditable(event)) return + const window = getWindow(event.target) this.tmp.isCopying = true window.requestAnimationFrame(() => { @@ -275,20 +275,20 @@ class Content extends React.Component { data.type = 'fragment' data.fragment = state.fragment - debug('onCopy', data) - this.props.onCopy(e, data) + debug('onCopy', { event, data }) + this.props.onCopy(event, data) } /** * On cut, defer to `onCutCopy`, then bubble up. * - * @param {Event} e + * @param {Event} event */ - onCut = (e) => { + onCut = (event) => { if (this.props.readOnly) return - if (!this.isInContentEditable(e)) return - const window = getWindow(e.target) + if (!this.isInContentEditable(event)) return + const window = getWindow(event.target) this.tmp.isCopying = true window.requestAnimationFrame(() => { @@ -300,61 +300,61 @@ class Content extends React.Component { data.type = 'fragment' data.fragment = state.fragment - debug('onCut', data) - this.props.onCut(e, data) + debug('onCut', { event, data }) + this.props.onCut(event, data) } /** * On drag end, unset the `isDragging` flag. * - * @param {Event} e + * @param {Event} event */ - onDragEnd = (e) => { - if (!this.isInContentEditable(e)) return + onDragEnd = (event) => { + if (!this.isInContentEditable(event)) return this.tmp.isDragging = false this.tmp.isInternalDrag = null - debug('onDragEnd') + debug('onDragEnd', { event }) } /** * On drag over, set the `isDragging` flag and the `isInternalDrag` flag. * - * @param {Event} e + * @param {Event} event */ - onDragOver = (e) => { - if (!this.isInContentEditable(e)) return + onDragOver = (event) => { + if (!this.isInContentEditable(event)) return - const { dataTransfer } = e.nativeEvent + const { dataTransfer } = event.nativeEvent const transfer = new Transfer(dataTransfer) // Prevent default when nodes are dragged to allow dropping. if (transfer.getType() == 'node') { - e.preventDefault() + event.preventDefault() } if (this.tmp.isDragging) return this.tmp.isDragging = true this.tmp.isInternalDrag = false - debug('onDragOver') + debug('onDragOver', { event }) } /** * On drag start, set the `isDragging` flag and the `isInternalDrag` flag. * - * @param {Event} e + * @param {Event} event */ - onDragStart = (e) => { - if (!this.isInContentEditable(e)) return + onDragStart = (event) => { + if (!this.isInContentEditable(event)) return this.tmp.isDragging = true this.tmp.isInternalDrag = true - const { dataTransfer } = e.nativeEvent + const { dataTransfer } = event.nativeEvent const transfer = new Transfer(dataTransfer) // If it's a node being dragged, the data type is already set. @@ -365,24 +365,25 @@ class Content extends React.Component { const encoded = Base64.serializeNode(fragment) dataTransfer.setData(TYPES.FRAGMENT, encoded) - debug('onDragStart') + debug('onDragStart', { event }) } /** * On drop. * - * @param {Event} e + * @param {Event} event */ - onDrop = (e) => { + onDrop = (event) => { if (this.props.readOnly) return - if (!this.isInContentEditable(e)) return + if (!this.isInContentEditable(event)) return - e.preventDefault() + event.preventDefault() - const window = getWindow(e.target) + const window = getWindow(event.target) const { state } = this.props - const { dataTransfer, x, y } = e.nativeEvent + const { nativeEvent } = event + const { dataTransfer, x, y } = nativeEvent const transfer = new Transfer(dataTransfer) const data = transfer.getData() @@ -394,7 +395,7 @@ class Content extends React.Component { range = window.document.caretRangeFromPoint(x, y) } else { range = window.document.createRange() - range.setStart(e.nativeEvent.rangeParent, e.nativeEvent.rangeOffset) + range.setStart(nativeEvent.rangeParent, nativeEvent.rangeOffset) } const startNode = range.startContainer @@ -419,24 +420,24 @@ class Content extends React.Component { data.isInternal = this.tmp.isInternalDrag } - debug('onDrop', data) - this.props.onDrop(e, data) + debug('onDrop', { event, data }) + this.props.onDrop(event, data) } /** * On input, handle spellcheck and other similar edits that don't go trigger * the `onBeforeInput` and instead update the DOM directly. * - * @param {Event} e + * @param {Event} event */ - onInput = (e) => { + onInput = (event) => { if (this.tmp.isComposing) return if (this.props.state.isBlurred) return - if (!this.isInContentEditable(e)) return - debug('onInput') + if (!this.isInContentEditable(event)) return + debug('onInput', { event }) - const window = getWindow(e.target) + const window = getWindow(event.target) // Get the selection point. const native = window.getSelection() @@ -498,14 +499,15 @@ class Content extends React.Component { * On key down, prevent the default behavior of certain commands that will * leave the editor in an out-of-sync state, then bubble up. * - * @param {Event} e + * @param {Event} event */ - onKeyDown = (e) => { + onKeyDown = (event) => { if (this.props.readOnly) return - if (!this.isInContentEditable(e)) return + if (!this.isInContentEditable(event)) return - const key = keycode(e.which) + const { altKey, ctrlKey, metaKey, shiftKey, which } = event + const key = keycode(which) const data = {} // When composing, these characters commit the composition but also move the @@ -515,22 +517,22 @@ class Content extends React.Component { this.tmp.isComposing && (key == 'left' || key == 'right' || key == 'up' || key == 'down') ) { - e.preventDefault() + event.preventDefault() return } // Add helpful properties for handling hotkeys to the data object. - data.code = e.which + data.code = which data.key = key - data.isAlt = e.altKey - data.isCmd = IS_MAC ? e.metaKey && !e.altKey : false - data.isCtrl = e.ctrlKey && !e.altKey - data.isLine = IS_MAC ? e.metaKey : false - data.isMeta = e.metaKey - data.isMod = IS_MAC ? e.metaKey && !e.altKey : e.ctrlKey && !e.altKey - data.isModAlt = IS_MAC ? e.metaKey && e.altKey : e.ctrlKey && e.altKey - data.isShift = e.shiftKey - data.isWord = IS_MAC ? e.altKey : e.ctrlKey + data.isAlt = altKey + data.isCmd = IS_MAC ? metaKey && !altKey : false + data.isCtrl = ctrlKey && !altKey + data.isLine = IS_MAC ? metaKey : false + data.isMeta = metaKey + data.isMod = IS_MAC ? metaKey && !altKey : ctrlKey && !altKey + data.isModAlt = IS_MAC ? metaKey && altKey : ctrlKey && altKey + data.isShift = shiftKey + data.isWord = IS_MAC ? altKey : ctrlKey // These key commands have native behavior in contenteditable elements which // will cause our state to be out of sync, so prevent them. @@ -543,44 +545,44 @@ class Content extends React.Component { (key == 'y' && data.isMod) || (key == 'z' && data.isMod) ) { - e.preventDefault() + event.preventDefault() } - debug('onKeyDown', data) - this.props.onKeyDown(e, data) + debug('onKeyDown', { event, data }) + this.props.onKeyDown(event, data) } /** * On paste, determine the type and bubble up. * - * @param {Event} e + * @param {Event} event */ - onPaste = (e) => { + onPaste = (event) => { if (this.props.readOnly) return - if (!this.isInContentEditable(e)) return + if (!this.isInContentEditable(event)) return - e.preventDefault() - const transfer = new Transfer(e.clipboardData) + event.preventDefault() + const transfer = new Transfer(event.clipboardData) const data = transfer.getData() - debug('onPaste', data) - this.props.onPaste(e, data) + debug('onPaste', { event, data }) + this.props.onPaste(event, data) } /** * On select, update the current state's selection. * - * @param {Event} e + * @param {Event} event */ - onSelect = (e) => { + onSelect = (event) => { if (this.props.readOnly) return if (this.tmp.isCopying) return if (this.tmp.isComposing) return - if (!this.isInContentEditable(e)) return + if (!this.isInContentEditable(event)) return - const window = getWindow(e.target) + const window = getWindow(event.target) const { state } = this.props let { document, selection } = state const native = window.getSelection() @@ -626,8 +628,8 @@ class Content extends React.Component { .normalize(document) } - debug('onSelect', { data, selection: data.selection.toJS() }) - this.props.onSelect(e, data) + debug('onSelect', { event, data }) + this.props.onSelect(event, data) } /** @@ -637,9 +639,8 @@ class Content extends React.Component { */ render = () => { - debug('render') - - const { className, readOnly, state } = this.props + const { props } = this + const { className, readOnly, state } = props const { document } = state const children = document.nodes .map(node => this.renderNode(node)) @@ -657,13 +658,15 @@ class Content extends React.Component { // weird ways. This hides that. (2016/06/21) ...(readOnly ? {} : { WebkitUserModify: 'read-write-plaintext-only' }), // Allow for passed-in styles to override anything. - ...this.props.style, + ...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 irreconcilable state. (2016/09/01) - const spellCheck = IS_FIREFOX ? false : this.props.spellCheck + const spellCheck = IS_FIREFOX ? false : props.spellCheck + + debug('render', { props }) return (
{ - debug('render') - - const handlers = {} + const { props, state } = this + const handlers = { onChange: this.onChange } for (const property of EVENT_HANDLERS) { handlers[property] = this[property] } + debug('render', { props, state }) + return ( ) } diff --git a/src/components/leaf.js b/src/components/leaf.js index 062807da5..9c071dac7 100644 --- a/src/components/leaf.js +++ b/src/components/leaf.js @@ -4,6 +4,7 @@ import OffsetKey from '../utils/offset-key' import React from 'react' import ReactDOM from 'react-dom' import getWindow from 'get-window' +import { IS_FIREFOX } from '../constants/environment' /** * Debugger. @@ -133,13 +134,13 @@ class Leaf extends React.Component { */ updateSelection() { - const { state, ranges, isVoid } = this.props + const { state, ranges } = this.props const { selection } = state // If the selection is blurred we have nothing to do. if (selection.isBlurred) return - let { anchorOffset, focusOffset } = selection + const { anchorOffset, focusOffset } = selection const { node, index } = this.props const { start, end } = OffsetKey.findBounds(index, ranges) @@ -148,13 +149,6 @@ class Leaf extends React.Component { const hasFocus = selection.hasFocusBetween(node, start, end) if (!hasAnchor && !hasFocus) return - // If the leaf is a void leaf, ensure that it has no width. This is due to - // void nodes always rendering an empty leaf, for browser compatibility. - if (isVoid) { - anchorOffset = 0 - focusOffset = 0 - } - // We have a selection to render, so prepare a few things... const ref = ReactDOM.findDOMNode(this) const el = findDeepestNode(ref) @@ -165,6 +159,7 @@ class Leaf extends React.Component { // COMPAT: In Firefox, it's not enough to create a range, you also need to // focus the contenteditable element. (2016/11/16) function focus() { + if (!IS_FIREFOX) return if (parent) setTimeout(() => parent.focus()) } @@ -218,7 +213,7 @@ class Leaf extends React.Component { } } - this.debug('updateSelection') + this.debug('updateSelection', { selection }) } /** @@ -228,8 +223,6 @@ class Leaf extends React.Component { */ render() { - this.debug('render') - const { props } = this const { node, index } = props const offsetKey = OffsetKey.stringify({ @@ -243,6 +236,9 @@ class Leaf extends React.Component { // get out of sync, causing it to not realize the DOM needs updating. this.tmp.renders++ + + this.debug('render', { props }) + return ( {this.renderMarks(props)} diff --git a/src/components/node.js b/src/components/node.js index 173f984aa..a84bf01fe 100644 --- a/src/components/node.js +++ b/src/components/node.js @@ -1,15 +1,12 @@ -import Immutable from 'immutable' import Base64 from '../serializers/base-64' import Debug from 'debug' import React from 'react' import ReactDOM from 'react-dom' import TYPES from '../constants/types' -import IS_DEV from '../constants/is-dev' import Leaf from './leaf' import Void from './void' import scrollTo from '../utils/scroll-to' -import warn from '../utils/warn' /** * Debug. @@ -232,9 +229,11 @@ class Node extends React.Component { */ render = () => { - this.debug('render') - + const { props } = this const { node } = this.props + + this.debug('render', { props }) + return node.kind == 'text' ? this.renderText() : this.renderElement() diff --git a/src/components/void.js b/src/components/void.js index 360c01da5..5077e3004 100644 --- a/src/components/void.js +++ b/src/components/void.js @@ -1,4 +1,5 @@ +import Debug from 'debug' import Leaf from './leaf' import Mark from '../models/mark' import OffsetKey from '../utils/offset-key' @@ -6,6 +7,14 @@ import React from 'react' import noop from '../utils/noop' import { IS_FIREFOX } from '../constants/environment' +/** + * Debug. + * + * @type {Function} + */ + +const debug = Debug('slate:void') + /** * Void. * @@ -30,18 +39,38 @@ class Void extends React.Component { }; /** - * When one of the wrapper elements it clicked, select the void node. + * Debug. * - * @param {Event} e + * @param {String} message + * @param {Mixed} ...args */ - onClick = (e) => { - e.preventDefault() + debug = (message, ...args) => { + const { node } = this.props + const { key, type } = node + let id = `${key} (${type})` + debug(message, `${id}`, ...args) + } + + /** + * When one of the wrapper elements it clicked, select the void node. + * + * @param {Event} event + */ + + onClick = (event) => { + event.preventDefault() + this.debug('onClick', { event }) + const { node, editor } = this.props const next = editor .getState() .transform() - .collapseToStartOf(node) + // COMPAT: In Chrome & Safari, selections that are at the zero offset of + // an inline node will be automatically replaced to be at the last offset + // of a previous inline node, which screws us up, so we always want to set + // it to the end of the node. (2016/11/29) + .collapseToEndOf(node) .focus() .apply() @@ -55,7 +84,8 @@ class Void extends React.Component { */ render = () => { - const { children, node } = this.props + const { props } = this + const { children, node } = props const Tag = node.kind == 'block' ? 'div' : 'span' // Make the outer wrapper relative, so the spacer can overlay it. @@ -63,6 +93,8 @@ class Void extends React.Component { position: 'relative' } + this.debug('render', { props }) + return ( {this.renderSpacer()} diff --git a/src/plugins/core.js b/src/plugins/core.js index e8b06b4e5..361f0826e 100644 --- a/src/plugins/core.js +++ b/src/plugins/core.js @@ -544,10 +544,17 @@ function Plugin(options = {}) { debug('onKeyDownRight', { data }) + // COMPAT: In Chrome & Safari, selections that are at the zero offset of + // an inline node will be automatically replaced to be at the last offset + // of a previous inline node, which screws us up, so we always want to set + // it to the end of the node. (2016/11/29) + const hasNextVoidParent = document.hasVoidParent(nextText.key) + const method = hasNextVoidParent ? 'collapseToEndOf' : 'collapseToStartOf' + e.preventDefault() return state .transform() - .collapseToStartOf(nextText) + [method](nextText) .apply() } }