1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-31 19:01:54 +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.
*
* @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 `<a>`, 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 (
<div

View File

@@ -258,25 +258,25 @@ class Editor extends React.Component {
*/
render = () => {
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 (
<Content
{...handlers}
className={this.props.className}
className={props.className}
editor={this}
onChange={this.onChange}
readOnly={this.props.readOnly}
schema={this.state.schema}
spellCheck={this.props.spellCheck}
state={this.state.state}
style={this.props.style}
readOnly={props.readOnly}
schema={state.schema}
spellCheck={props.spellCheck}
state={state.state}
style={props.style}
/>
)
}

View File

@@ -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 (
<span key={this.tmp.renders} data-offset-key={offsetKey}>
{this.renderMarks(props)}

View File

@@ -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()

View File

@@ -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 (
<Tag style={style} onClick={this.onClick}>
{this.renderSpacer()}

View File

@@ -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()
}
}