1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-09-01 11:12:42 +02:00

cleanup logging, fix inline void selection

This commit is contained in:
Ian Storm Taylor
2016-11-29 12:35:46 -08:00
parent 2c7be1dcf0
commit bfbbd6d8fb
6 changed files with 166 additions and 129 deletions

View File

@@ -174,34 +174,34 @@ class Content extends React.Component {
/** /**
* On before input, bubble up. * On before input, bubble up.
* *
* @param {Event} e * @param {Event} event
*/ */
onBeforeInput = (e) => { onBeforeInput = (event) => {
if (this.props.readOnly) return if (this.props.readOnly) return
if (!this.isInContentEditable(e)) return if (!this.isInContentEditable(event)) return
const data = {} const data = {}
debug('onBeforeInput', data) debug('onBeforeInput', { event, data })
this.props.onBeforeInput(e, data) this.props.onBeforeInput(event, data)
} }
/** /**
* On blur, update the selection to be not focused. * 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.props.readOnly) return
if (this.tmp.isCopying) return if (this.tmp.isCopying) return
if (!this.isInContentEditable(e)) return if (!this.isInContentEditable(event)) return
const data = {} const data = {}
debug('onBlur', data) debug('onBlur', { event, data })
this.props.onBlur(e, data) this.props.onBlur(event, data)
} }
/** /**
@@ -218,16 +218,16 @@ class Content extends React.Component {
/** /**
* On composition start, set the `isComposing` flag. * On composition start, set the `isComposing` flag.
* *
* @param {Event} e * @param {Event} event
*/ */
onCompositionStart = (e) => { onCompositionStart = (event) => {
if (!this.isInContentEditable(e)) return if (!this.isInContentEditable(event)) return
this.tmp.isComposing = true this.tmp.isComposing = true
this.tmp.compositions++ 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 * increment the `forces` key, which will force the contenteditable element
* to completely re-render, since IME puts React in an unreconcilable state. * to completely re-render, since IME puts React in an unreconcilable state.
* *
* @param {Event} e * @param {Event} event
*/ */
onCompositionEnd = (e) => { onCompositionEnd = (event) => {
if (!this.isInContentEditable(e)) return if (!this.isInContentEditable(event)) return
this.forces++ this.forces++
const count = this.tmp.compositions const count = this.tmp.compositions
@@ -252,18 +252,18 @@ class Content extends React.Component {
this.tmp.isComposing = false this.tmp.isComposing = false
}) })
debug('onCompositionEnd') debug('onCompositionEnd', { event })
} }
/** /**
* On copy, defer to `onCutCopy`, then bubble up. * On copy, defer to `onCutCopy`, then bubble up.
* *
* @param {Event} e * @param {Event} event
*/ */
onCopy = (e) => { onCopy = (event) => {
if (!this.isInContentEditable(e)) return if (!this.isInContentEditable(event)) return
const window = getWindow(e.target) const window = getWindow(event.target)
this.tmp.isCopying = true this.tmp.isCopying = true
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
@@ -275,20 +275,20 @@ class Content extends React.Component {
data.type = 'fragment' data.type = 'fragment'
data.fragment = state.fragment data.fragment = state.fragment
debug('onCopy', data) debug('onCopy', { event, data })
this.props.onCopy(e, data) this.props.onCopy(event, data)
} }
/** /**
* On cut, defer to `onCutCopy`, then bubble up. * 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.props.readOnly) return
if (!this.isInContentEditable(e)) return if (!this.isInContentEditable(event)) return
const window = getWindow(e.target) const window = getWindow(event.target)
this.tmp.isCopying = true this.tmp.isCopying = true
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
@@ -300,61 +300,61 @@ class Content extends React.Component {
data.type = 'fragment' data.type = 'fragment'
data.fragment = state.fragment data.fragment = state.fragment
debug('onCut', data) debug('onCut', { event, data })
this.props.onCut(e, data) this.props.onCut(event, data)
} }
/** /**
* On drag end, unset the `isDragging` flag. * On drag end, unset the `isDragging` flag.
* *
* @param {Event} e * @param {Event} event
*/ */
onDragEnd = (e) => { onDragEnd = (event) => {
if (!this.isInContentEditable(e)) return if (!this.isInContentEditable(event)) return
this.tmp.isDragging = false this.tmp.isDragging = false
this.tmp.isInternalDrag = null this.tmp.isInternalDrag = null
debug('onDragEnd') debug('onDragEnd', { event })
} }
/** /**
* On drag over, set the `isDragging` flag and the `isInternalDrag` flag. * On drag over, set the `isDragging` flag and the `isInternalDrag` flag.
* *
* @param {Event} e * @param {Event} event
*/ */
onDragOver = (e) => { onDragOver = (event) => {
if (!this.isInContentEditable(e)) return if (!this.isInContentEditable(event)) return
const { dataTransfer } = e.nativeEvent const { dataTransfer } = event.nativeEvent
const transfer = new Transfer(dataTransfer) const transfer = new Transfer(dataTransfer)
// Prevent default when nodes are dragged to allow dropping. // Prevent default when nodes are dragged to allow dropping.
if (transfer.getType() == 'node') { if (transfer.getType() == 'node') {
e.preventDefault() event.preventDefault()
} }
if (this.tmp.isDragging) return if (this.tmp.isDragging) return
this.tmp.isDragging = true this.tmp.isDragging = true
this.tmp.isInternalDrag = false this.tmp.isInternalDrag = false
debug('onDragOver') debug('onDragOver', { event })
} }
/** /**
* On drag start, set the `isDragging` flag and the `isInternalDrag` flag. * On drag start, set the `isDragging` flag and the `isInternalDrag` flag.
* *
* @param {Event} e * @param {Event} event
*/ */
onDragStart = (e) => { onDragStart = (event) => {
if (!this.isInContentEditable(e)) return if (!this.isInContentEditable(event)) return
this.tmp.isDragging = true this.tmp.isDragging = true
this.tmp.isInternalDrag = true this.tmp.isInternalDrag = true
const { dataTransfer } = e.nativeEvent const { dataTransfer } = event.nativeEvent
const transfer = new Transfer(dataTransfer) const transfer = new Transfer(dataTransfer)
// If it's a node being dragged, the data type is already set. // 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) const encoded = Base64.serializeNode(fragment)
dataTransfer.setData(TYPES.FRAGMENT, encoded) dataTransfer.setData(TYPES.FRAGMENT, encoded)
debug('onDragStart') debug('onDragStart', { event })
} }
/** /**
* On drop. * On drop.
* *
* @param {Event} e * @param {Event} event
*/ */
onDrop = (e) => { onDrop = (event) => {
if (this.props.readOnly) return 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 { state } = this.props
const { dataTransfer, x, y } = e.nativeEvent const { nativeEvent } = event
const { dataTransfer, x, y } = nativeEvent
const transfer = new Transfer(dataTransfer) const transfer = new Transfer(dataTransfer)
const data = transfer.getData() const data = transfer.getData()
@@ -394,7 +395,7 @@ class Content extends React.Component {
range = window.document.caretRangeFromPoint(x, y) range = window.document.caretRangeFromPoint(x, y)
} else { } else {
range = window.document.createRange() range = window.document.createRange()
range.setStart(e.nativeEvent.rangeParent, e.nativeEvent.rangeOffset) range.setStart(nativeEvent.rangeParent, nativeEvent.rangeOffset)
} }
const startNode = range.startContainer const startNode = range.startContainer
@@ -419,24 +420,24 @@ class Content extends React.Component {
data.isInternal = this.tmp.isInternalDrag data.isInternal = this.tmp.isInternalDrag
} }
debug('onDrop', data) debug('onDrop', { event, data })
this.props.onDrop(e, data) this.props.onDrop(event, data)
} }
/** /**
* On input, handle spellcheck and other similar edits that don't go trigger * On input, handle spellcheck and other similar edits that don't go trigger
* the `onBeforeInput` and instead update the DOM directly. * 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.tmp.isComposing) return
if (this.props.state.isBlurred) return if (this.props.state.isBlurred) return
if (!this.isInContentEditable(e)) return if (!this.isInContentEditable(event)) return
debug('onInput') debug('onInput', { event })
const window = getWindow(e.target) const window = getWindow(event.target)
// Get the selection point. // Get the selection point.
const native = window.getSelection() 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 * On key down, prevent the default behavior of certain commands that will
* leave the editor in an out-of-sync state, then bubble up. * 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.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 = {} const data = {}
// When composing, these characters commit the composition but also move the // When composing, these characters commit the composition but also move the
@@ -515,22 +517,22 @@ class Content extends React.Component {
this.tmp.isComposing && this.tmp.isComposing &&
(key == 'left' || key == 'right' || key == 'up' || key == 'down') (key == 'left' || key == 'right' || key == 'up' || key == 'down')
) { ) {
e.preventDefault() event.preventDefault()
return return
} }
// Add helpful properties for handling hotkeys to the data object. // Add helpful properties for handling hotkeys to the data object.
data.code = e.which data.code = which
data.key = key data.key = key
data.isAlt = e.altKey data.isAlt = altKey
data.isCmd = IS_MAC ? e.metaKey && !e.altKey : false data.isCmd = IS_MAC ? metaKey && !altKey : false
data.isCtrl = e.ctrlKey && !e.altKey data.isCtrl = ctrlKey && !altKey
data.isLine = IS_MAC ? e.metaKey : false data.isLine = IS_MAC ? metaKey : false
data.isMeta = e.metaKey data.isMeta = metaKey
data.isMod = IS_MAC ? e.metaKey && !e.altKey : e.ctrlKey && !e.altKey data.isMod = IS_MAC ? metaKey && !altKey : ctrlKey && !altKey
data.isModAlt = IS_MAC ? e.metaKey && e.altKey : e.ctrlKey && e.altKey data.isModAlt = IS_MAC ? metaKey && altKey : ctrlKey && altKey
data.isShift = e.shiftKey data.isShift = shiftKey
data.isWord = IS_MAC ? e.altKey : e.ctrlKey data.isWord = IS_MAC ? altKey : ctrlKey
// These key commands have native behavior in contenteditable elements which // These key commands have native behavior in contenteditable elements which
// will cause our state to be out of sync, so prevent them. // 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 == 'y' && data.isMod) ||
(key == 'z' && data.isMod) (key == 'z' && data.isMod)
) { ) {
e.preventDefault() event.preventDefault()
} }
debug('onKeyDown', data) debug('onKeyDown', { event, data })
this.props.onKeyDown(e, data) this.props.onKeyDown(event, data)
} }
/** /**
* On paste, determine the type and bubble up. * 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.props.readOnly) return
if (!this.isInContentEditable(e)) return if (!this.isInContentEditable(event)) return
e.preventDefault() event.preventDefault()
const transfer = new Transfer(e.clipboardData) const transfer = new Transfer(event.clipboardData)
const data = transfer.getData() const data = transfer.getData()
debug('onPaste', data) debug('onPaste', { event, data })
this.props.onPaste(e, data) this.props.onPaste(event, data)
} }
/** /**
* On select, update the current state's selection. * 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.props.readOnly) return
if (this.tmp.isCopying) return if (this.tmp.isCopying) return
if (this.tmp.isComposing) 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 const { state } = this.props
let { document, selection } = state let { document, selection } = state
const native = window.getSelection() const native = window.getSelection()
@@ -626,8 +628,8 @@ class Content extends React.Component {
.normalize(document) .normalize(document)
} }
debug('onSelect', { data, selection: data.selection.toJS() }) debug('onSelect', { event, data })
this.props.onSelect(e, data) this.props.onSelect(event, data)
} }
/** /**
@@ -637,9 +639,8 @@ class Content extends React.Component {
*/ */
render = () => { render = () => {
debug('render') const { props } = this
const { className, readOnly, state } = props
const { className, readOnly, state } = this.props
const { document } = state const { document } = state
const children = document.nodes const children = document.nodes
.map(node => this.renderNode(node)) .map(node => this.renderNode(node))
@@ -657,13 +658,15 @@ class Content extends React.Component {
// weird ways. This hides that. (2016/06/21) // weird ways. This hides that. (2016/06/21)
...(readOnly ? {} : { WebkitUserModify: 'read-write-plaintext-only' }), ...(readOnly ? {} : { WebkitUserModify: 'read-write-plaintext-only' }),
// Allow for passed-in styles to override anything. // Allow for passed-in styles to override anything.
...this.props.style, ...props.style,
} }
// COMPAT: In Firefox, spellchecking can remove entire wrapping elements // COMPAT: In Firefox, spellchecking can remove entire wrapping elements
// including inline ones like `<a>`, which is jarring for the user but also // including inline ones like `<a>`, which is jarring for the user but also
// causes the DOM to get into an irreconcilable state. (2016/09/01) // 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 ( return (
<div <div

View File

@@ -258,25 +258,25 @@ class Editor extends React.Component {
*/ */
render = () => { render = () => {
debug('render') const { props, state } = this
const handlers = { onChange: this.onChange }
const handlers = {}
for (const property of EVENT_HANDLERS) { for (const property of EVENT_HANDLERS) {
handlers[property] = this[property] handlers[property] = this[property]
} }
debug('render', { props, state })
return ( return (
<Content <Content
{...handlers} {...handlers}
className={this.props.className} className={props.className}
editor={this} editor={this}
onChange={this.onChange} readOnly={props.readOnly}
readOnly={this.props.readOnly} schema={state.schema}
schema={this.state.schema} spellCheck={props.spellCheck}
spellCheck={this.props.spellCheck} state={state.state}
state={this.state.state} style={props.style}
style={this.props.style}
/> />
) )
} }

View File

@@ -4,6 +4,7 @@ import OffsetKey from '../utils/offset-key'
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import getWindow from 'get-window' import getWindow from 'get-window'
import { IS_FIREFOX } from '../constants/environment'
/** /**
* Debugger. * Debugger.
@@ -133,13 +134,13 @@ class Leaf extends React.Component {
*/ */
updateSelection() { updateSelection() {
const { state, ranges, isVoid } = this.props const { state, ranges } = this.props
const { selection } = state const { selection } = state
// If the selection is blurred we have nothing to do. // If the selection is blurred we have nothing to do.
if (selection.isBlurred) return if (selection.isBlurred) return
let { anchorOffset, focusOffset } = selection const { anchorOffset, focusOffset } = selection
const { node, index } = this.props const { node, index } = this.props
const { start, end } = OffsetKey.findBounds(index, ranges) const { start, end } = OffsetKey.findBounds(index, ranges)
@@ -148,13 +149,6 @@ class Leaf extends React.Component {
const hasFocus = selection.hasFocusBetween(node, start, end) const hasFocus = selection.hasFocusBetween(node, start, end)
if (!hasAnchor && !hasFocus) return 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... // We have a selection to render, so prepare a few things...
const ref = ReactDOM.findDOMNode(this) const ref = ReactDOM.findDOMNode(this)
const el = findDeepestNode(ref) 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 // COMPAT: In Firefox, it's not enough to create a range, you also need to
// focus the contenteditable element. (2016/11/16) // focus the contenteditable element. (2016/11/16)
function focus() { function focus() {
if (!IS_FIREFOX) return
if (parent) setTimeout(() => parent.focus()) 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() { render() {
this.debug('render')
const { props } = this const { props } = this
const { node, index } = props const { node, index } = props
const offsetKey = OffsetKey.stringify({ 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. // get out of sync, causing it to not realize the DOM needs updating.
this.tmp.renders++ this.tmp.renders++
this.debug('render', { props })
return ( return (
<span key={this.tmp.renders} data-offset-key={offsetKey}> <span key={this.tmp.renders} data-offset-key={offsetKey}>
{this.renderMarks(props)} {this.renderMarks(props)}

View File

@@ -1,15 +1,12 @@
import Immutable from 'immutable'
import Base64 from '../serializers/base-64' import Base64 from '../serializers/base-64'
import Debug from 'debug' import Debug from 'debug'
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import TYPES from '../constants/types' import TYPES from '../constants/types'
import IS_DEV from '../constants/is-dev'
import Leaf from './leaf' import Leaf from './leaf'
import Void from './void' import Void from './void'
import scrollTo from '../utils/scroll-to' import scrollTo from '../utils/scroll-to'
import warn from '../utils/warn'
/** /**
* Debug. * Debug.
@@ -232,9 +229,11 @@ class Node extends React.Component {
*/ */
render = () => { render = () => {
this.debug('render') const { props } = this
const { node } = this.props const { node } = this.props
this.debug('render', { props })
return node.kind == 'text' return node.kind == 'text'
? this.renderText() ? this.renderText()
: this.renderElement() : this.renderElement()

View File

@@ -1,4 +1,5 @@
import Debug from 'debug'
import Leaf from './leaf' import Leaf from './leaf'
import Mark from '../models/mark' import Mark from '../models/mark'
import OffsetKey from '../utils/offset-key' import OffsetKey from '../utils/offset-key'
@@ -6,6 +7,14 @@ import React from 'react'
import noop from '../utils/noop' import noop from '../utils/noop'
import { IS_FIREFOX } from '../constants/environment' import { IS_FIREFOX } from '../constants/environment'
/**
* Debug.
*
* @type {Function}
*/
const debug = Debug('slate:void')
/** /**
* 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) => { debug = (message, ...args) => {
e.preventDefault() 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 { node, editor } = this.props
const next = editor const next = editor
.getState() .getState()
.transform() .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() .focus()
.apply() .apply()
@@ -55,7 +84,8 @@ class Void extends React.Component {
*/ */
render = () => { render = () => {
const { children, node } = this.props const { props } = this
const { children, node } = props
const Tag = node.kind == 'block' ? 'div' : 'span' const Tag = node.kind == 'block' ? 'div' : 'span'
// Make the outer wrapper relative, so the spacer can overlay it. // Make the outer wrapper relative, so the spacer can overlay it.
@@ -63,6 +93,8 @@ class Void extends React.Component {
position: 'relative' position: 'relative'
} }
this.debug('render', { props })
return ( return (
<Tag style={style} onClick={this.onClick}> <Tag style={style} onClick={this.onClick}>
{this.renderSpacer()} {this.renderSpacer()}

View File

@@ -544,10 +544,17 @@ function Plugin(options = {}) {
debug('onKeyDownRight', { data }) 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() e.preventDefault()
return state return state
.transform() .transform()
.collapseToStartOf(nextText) [method](nextText)
.apply() .apply()
} }
} }