mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-17 20:51:20 +02:00
Introduce annotations (#2747)
* first stab at removing leaves with tests passing * fixes * add iterables to the element interface * use iterables in more places * update examples to use iterables * update naming * fix tests * convert more key-based logic to paths * add range support to iterables * refactor many methods to use iterables, deprecate cruft * clean up existing iterables * more cleanup * more cleaning * fix word count example * work * split decoration and annotations * update examples for `renderNode` useage * deprecate old DOM-based helpers, update examples * make formats first class, refactor leaf rendering * fix examples, fix isAtomic checking * deprecate leaf model * convert Text and Leaf to functional components * fix lint and tests
This commit is contained in:
@@ -32,8 +32,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"immutable": ">=3.8.1 || >4.0.0-rc",
|
||||
"react": ">=0.14.0",
|
||||
"react-dom": ">=0.14.0",
|
||||
"react": ">=16.6.0",
|
||||
"slate": ">=0.43.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -49,7 +48,6 @@
|
||||
"umdGlobals": {
|
||||
"immutable": "Immutable",
|
||||
"react": "React",
|
||||
"react-dom": "ReactDOM",
|
||||
"slate": "Slate"
|
||||
},
|
||||
"keywords": [
|
||||
|
@@ -4,6 +4,7 @@ import Types from 'prop-types'
|
||||
import getWindow from 'get-window'
|
||||
import warning from 'tiny-warning'
|
||||
import throttle from 'lodash/throttle'
|
||||
import { List } from 'immutable'
|
||||
import {
|
||||
IS_ANDROID,
|
||||
IS_FIREFOX,
|
||||
@@ -11,10 +12,9 @@ import {
|
||||
} from 'slate-dev-environment'
|
||||
|
||||
import EVENT_HANDLERS from '../constants/event-handlers'
|
||||
import DATA_ATTRS from '../constants/data-attributes'
|
||||
import SELECTORS from '../constants/selectors'
|
||||
import Node from './node'
|
||||
import findDOMRange from '../utils/find-dom-range'
|
||||
import findRange from '../utils/find-range'
|
||||
import getChildrenDecorations from '../utils/get-children-decorations'
|
||||
import scrollToSelection from '../utils/scroll-to-selection'
|
||||
import removeAllRanges from '../utils/remove-all-ranges'
|
||||
|
||||
@@ -82,8 +82,18 @@ class Content extends React.Component {
|
||||
|
||||
tmp = {
|
||||
isUpdatingSelection: false,
|
||||
nodeRef: React.createRef(),
|
||||
nodeRefs: {},
|
||||
}
|
||||
|
||||
/**
|
||||
* A ref for the contenteditable DOM node.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
ref = React.createRef()
|
||||
|
||||
/**
|
||||
* Create a set of bound event handlers.
|
||||
*
|
||||
@@ -103,7 +113,7 @@ class Content extends React.Component {
|
||||
*/
|
||||
|
||||
componentDidMount() {
|
||||
const window = getWindow(this.element)
|
||||
const window = getWindow(this.ref.current)
|
||||
|
||||
window.document.addEventListener(
|
||||
'selectionchange',
|
||||
@@ -113,7 +123,10 @@ class Content extends React.Component {
|
||||
// COMPAT: Restrict scope of `beforeinput` to clients that support the
|
||||
// Input Events Level 2 spec, since they are preventable events.
|
||||
if (HAS_INPUT_EVENTS_LEVEL_2) {
|
||||
this.element.addEventListener('beforeinput', this.handlers.onBeforeInput)
|
||||
this.ref.current.addEventListener(
|
||||
'beforeinput',
|
||||
this.handlers.onBeforeInput
|
||||
)
|
||||
}
|
||||
|
||||
this.updateSelection()
|
||||
@@ -124,7 +137,7 @@ class Content extends React.Component {
|
||||
*/
|
||||
|
||||
componentWillUnmount() {
|
||||
const window = getWindow(this.element)
|
||||
const window = getWindow(this.ref.current)
|
||||
|
||||
if (window) {
|
||||
window.document.removeEventListener(
|
||||
@@ -134,7 +147,7 @@ class Content extends React.Component {
|
||||
}
|
||||
|
||||
if (HAS_INPUT_EVENTS_LEVEL_2) {
|
||||
this.element.removeEventListener(
|
||||
this.ref.current.removeEventListener(
|
||||
'beforeinput',
|
||||
this.handlers.onBeforeInput
|
||||
)
|
||||
@@ -159,7 +172,7 @@ class Content extends React.Component {
|
||||
const { value } = editor
|
||||
const { selection } = value
|
||||
const { isBackward } = selection
|
||||
const window = getWindow(this.element)
|
||||
const window = getWindow(this.ref.current)
|
||||
const native = window.getSelection()
|
||||
const { activeElement } = window.document
|
||||
|
||||
@@ -178,8 +191,8 @@ class Content extends React.Component {
|
||||
|
||||
// If the Slate selection is blurred, but the DOM's active element is still
|
||||
// the editor, we need to blur it.
|
||||
if (selection.isBlurred && activeElement === this.element) {
|
||||
this.element.blur()
|
||||
if (selection.isBlurred && activeElement === this.ref.current) {
|
||||
this.ref.current.blur()
|
||||
updated = true
|
||||
}
|
||||
|
||||
@@ -193,15 +206,15 @@ class Content extends React.Component {
|
||||
// If the Slate selection is focused, but the DOM's active element is not
|
||||
// the editor, we need to focus it. We prevent scrolling because we handle
|
||||
// scrolling to the correct selection.
|
||||
if (selection.isFocused && activeElement !== this.element) {
|
||||
this.element.focus({ preventScroll: true })
|
||||
if (selection.isFocused && activeElement !== this.ref.current) {
|
||||
this.ref.current.focus({ preventScroll: true })
|
||||
updated = true
|
||||
}
|
||||
|
||||
// Otherwise, figure out which DOM nodes should be selected...
|
||||
if (selection.isFocused && selection.isSet) {
|
||||
const current = !!rangeCount && native.getRangeAt(0)
|
||||
const range = findDOMRange(selection, window)
|
||||
const range = editor.findDOMRange(selection)
|
||||
|
||||
if (!range) {
|
||||
warning(
|
||||
@@ -269,8 +282,8 @@ class Content extends React.Component {
|
||||
setTimeout(() => {
|
||||
// COMPAT: In Firefox, it's not enough to create a range, you also need
|
||||
// to focus the contenteditable element too. (2016/11/16)
|
||||
if (IS_FIREFOX && this.element) {
|
||||
this.element.focus()
|
||||
if (IS_FIREFOX && this.ref.current) {
|
||||
this.ref.current.focus()
|
||||
}
|
||||
|
||||
this.tmp.isUpdatingSelection = false
|
||||
@@ -283,16 +296,6 @@ class Content extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The React ref method to set the root content element locally.
|
||||
*
|
||||
* @param {Element} element
|
||||
*/
|
||||
|
||||
ref = element => {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an event `target` is fired from within the contenteditable
|
||||
* element. This should be false for edits happening in non-contenteditable
|
||||
@@ -303,8 +306,6 @@ class Content extends React.Component {
|
||||
*/
|
||||
|
||||
isInEditor = target => {
|
||||
const { element } = this
|
||||
|
||||
let el
|
||||
|
||||
try {
|
||||
@@ -331,7 +332,8 @@ class Content extends React.Component {
|
||||
|
||||
return (
|
||||
el.isContentEditable &&
|
||||
(el === element || el.closest('[data-slate-editor]') === element)
|
||||
(el === this.ref.current ||
|
||||
el.closest(SELECTORS.EDITOR) === this.ref.current)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -369,8 +371,8 @@ class Content extends React.Component {
|
||||
const { value } = editor
|
||||
const { selection } = value
|
||||
const window = getWindow(event.target)
|
||||
const native = window.getSelection()
|
||||
const range = findRange(native, editor)
|
||||
const domSelection = window.getSelection()
|
||||
const range = editor.findRange(domSelection)
|
||||
|
||||
if (range && range.equals(selection.toRange())) {
|
||||
this.updateSelection()
|
||||
@@ -388,9 +390,9 @@ class Content extends React.Component {
|
||||
handler === 'onDragStart' ||
|
||||
handler === 'onDrop'
|
||||
) {
|
||||
const closest = event.target.closest('[data-slate-editor]')
|
||||
const closest = event.target.closest(SELECTORS.EDITOR)
|
||||
|
||||
if (closest !== this.element) {
|
||||
if (closest !== this.ref.current) {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -433,7 +435,7 @@ class Content extends React.Component {
|
||||
|
||||
const window = getWindow(event.target)
|
||||
const { activeElement } = window.document
|
||||
if (activeElement !== this.element) return
|
||||
if (activeElement !== this.ref.current) return
|
||||
|
||||
this.props.onEvent('onSelect', event)
|
||||
}, 100)
|
||||
@@ -458,16 +460,7 @@ class Content extends React.Component {
|
||||
} = props
|
||||
const { value } = editor
|
||||
const Container = tagName
|
||||
const { document, selection, decorations } = value
|
||||
const indexes = document.getSelectionIndexes(selection)
|
||||
const decs = document.getDecorations(editor).concat(decorations)
|
||||
const childrenDecorations = getChildrenDecorations(document, decs)
|
||||
|
||||
const children = document.nodes.toArray().map((child, i) => {
|
||||
const isSelected = !!indexes && indexes.start <= i && i < indexes.end
|
||||
|
||||
return this.renderNode(child, isSelected, childrenDecorations[i])
|
||||
})
|
||||
const { document, selection } = value
|
||||
|
||||
const style = {
|
||||
// Prevent the default outline styles.
|
||||
@@ -486,20 +479,16 @@ class Content extends React.Component {
|
||||
|
||||
debug('render', { props })
|
||||
|
||||
if (debug.enabled) {
|
||||
debug.update('render', {
|
||||
text: value.document.text,
|
||||
selection: value.selection.toJSON(),
|
||||
value: value.toJSON(),
|
||||
})
|
||||
const data = {
|
||||
[DATA_ATTRS.EDITOR]: true,
|
||||
[DATA_ATTRS.KEY]: document.key,
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
{...handlers}
|
||||
data-slate-editor
|
||||
{...data}
|
||||
ref={this.ref}
|
||||
data-key={document.key}
|
||||
contentEditable={readOnly ? null : true}
|
||||
suppressContentEditableWarning
|
||||
id={id}
|
||||
@@ -514,39 +503,20 @@ class Content extends React.Component {
|
||||
// so we have to disable it like this. (2017/04/24)
|
||||
data-gramm={false}
|
||||
>
|
||||
{children}
|
||||
<Node
|
||||
annotations={value.annotations}
|
||||
block={null}
|
||||
decorations={List()}
|
||||
editor={editor}
|
||||
node={document}
|
||||
parent={null}
|
||||
readOnly={readOnly}
|
||||
selection={selection}
|
||||
ref={this.tmp.nodeRef}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a `child` node of the document.
|
||||
*
|
||||
* @param {Node} child
|
||||
* @param {Boolean} isSelected
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
renderNode = (child, isSelected, decorations) => {
|
||||
const { editor, readOnly } = this.props
|
||||
const { value } = editor
|
||||
const { document, selection } = value
|
||||
const { isFocused } = selection
|
||||
|
||||
return (
|
||||
<Node
|
||||
block={null}
|
||||
editor={editor}
|
||||
decorations={decorations}
|
||||
isSelected={isSelected}
|
||||
isFocused={isFocused && isSelected}
|
||||
key={child.key}
|
||||
node={child}
|
||||
parent={document}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -8,6 +8,7 @@ import warning from 'tiny-warning'
|
||||
import { Editor as Controller } from 'slate'
|
||||
|
||||
import EVENT_HANDLERS from '../constants/event-handlers'
|
||||
import Content from './content'
|
||||
import ReactPlugin from '../plugins/react'
|
||||
|
||||
/**
|
||||
@@ -91,6 +92,7 @@ class Editor extends React.Component {
|
||||
change: null,
|
||||
resolves: 0,
|
||||
updates: 0,
|
||||
contentRef: React.createRef(),
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,25 +142,54 @@ class Editor extends React.Component {
|
||||
|
||||
render() {
|
||||
debug('render', this)
|
||||
const props = { ...this.props, editor: this }
|
||||
|
||||
// Re-resolve the controller if needed based on memoized props.
|
||||
const { commands, placeholder, plugins, queries, schema } = props
|
||||
const { commands, placeholder, plugins, queries, schema } = this.props
|
||||
this.resolveController(plugins, schema, commands, queries, placeholder)
|
||||
|
||||
// Set the current props on the controller.
|
||||
const { options, readOnly, value: valueFromProps } = props
|
||||
const { options, readOnly, value: valueFromProps } = this.props
|
||||
const { value: valueFromState } = this.state
|
||||
const value = valueFromProps || valueFromState
|
||||
this.controller.setReadOnly(readOnly)
|
||||
this.controller.setValue(value, options)
|
||||
|
||||
const {
|
||||
autoCorrect,
|
||||
className,
|
||||
id,
|
||||
role,
|
||||
spellCheck,
|
||||
tabIndex,
|
||||
style,
|
||||
tagName,
|
||||
} = this.props
|
||||
|
||||
const children = (
|
||||
<Content
|
||||
ref={this.tmp.contentRef}
|
||||
autoCorrect={autoCorrect}
|
||||
className={className}
|
||||
editor={this}
|
||||
id={id}
|
||||
onEvent={(handler, event) => this.run(handler, event)}
|
||||
readOnly={readOnly}
|
||||
role={role}
|
||||
spellCheck={spellCheck}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
tagName={tagName}
|
||||
/>
|
||||
)
|
||||
|
||||
// Render the editor's children with the controller.
|
||||
const children = this.controller.run('renderEditor', {
|
||||
...props,
|
||||
value,
|
||||
const element = this.controller.run('renderEditor', {
|
||||
...this.props,
|
||||
editor: this,
|
||||
children,
|
||||
})
|
||||
return children
|
||||
|
||||
return element
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,192 +1,219 @@
|
||||
import Debug from 'debug'
|
||||
import React from 'react'
|
||||
import Types from 'prop-types'
|
||||
import SlateTypes from 'slate-prop-types'
|
||||
import ImmutableTypes from 'react-immutable-proptypes'
|
||||
|
||||
import OffsetKey from '../utils/offset-key'
|
||||
import DATA_ATTRS from '../constants/data-attributes'
|
||||
|
||||
/**
|
||||
* Debugger.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
const debug = Debug('slate:leaves')
|
||||
|
||||
/**
|
||||
* Leaf.
|
||||
* Leaf strings with text in them.
|
||||
*
|
||||
* @type {Component}
|
||||
*/
|
||||
|
||||
class Leaf extends React.Component {
|
||||
/**
|
||||
* Property types.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
const TextString = ({ text = '', isTrailing = false }) => {
|
||||
return (
|
||||
<span
|
||||
{...{
|
||||
[DATA_ATTRS.STRING]: true,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
{isTrailing ? '\n' : null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
block: SlateTypes.block.isRequired,
|
||||
editor: Types.object.isRequired,
|
||||
index: Types.number.isRequired,
|
||||
leaves: SlateTypes.leaves.isRequired,
|
||||
marks: SlateTypes.marks.isRequired,
|
||||
node: SlateTypes.node.isRequired,
|
||||
offset: Types.number.isRequired,
|
||||
parent: SlateTypes.node.isRequired,
|
||||
text: Types.string.isRequired,
|
||||
}
|
||||
/**
|
||||
* Leaf strings without text, render as zero-width strings.
|
||||
*
|
||||
* @type {Component}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
*
|
||||
* @param {String} message
|
||||
* @param {Mixed} ...args
|
||||
*/
|
||||
const ZeroWidthString = ({ length = 0, isLineBreak = false }) => {
|
||||
return (
|
||||
<span
|
||||
{...{
|
||||
[DATA_ATTRS.ZERO_WIDTH]: isLineBreak ? 'n' : 'z',
|
||||
[DATA_ATTRS.LENGTH]: length,
|
||||
}}
|
||||
>
|
||||
{'\uFEFF'}
|
||||
{isLineBreak ? <br /> : null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
debug = (message, ...args) => {
|
||||
debug(message, `${this.props.node.key}-${this.props.index}`, ...args)
|
||||
}
|
||||
/**
|
||||
* Individual leaves in a text node with unique formatting.
|
||||
*
|
||||
* @type {Component}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Should component update?
|
||||
*
|
||||
* @param {Object} props
|
||||
* @return {Boolean}
|
||||
*/
|
||||
const Leaf = props => {
|
||||
const {
|
||||
marks,
|
||||
annotations,
|
||||
decorations,
|
||||
node,
|
||||
index,
|
||||
offset,
|
||||
text,
|
||||
editor,
|
||||
parent,
|
||||
block,
|
||||
leaves,
|
||||
} = props
|
||||
|
||||
shouldComponentUpdate(props) {
|
||||
// If any of the regular properties have changed, re-render.
|
||||
if (
|
||||
props.index !== this.props.index ||
|
||||
props.marks !== this.props.marks ||
|
||||
props.text !== this.props.text ||
|
||||
props.parent !== this.props.parent
|
||||
) {
|
||||
return true
|
||||
}
|
||||
const offsetKey = OffsetKey.stringify({
|
||||
key: node.key,
|
||||
index,
|
||||
})
|
||||
|
||||
// Otherwise, don't update.
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the leaf.
|
||||
*
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
render() {
|
||||
this.debug('render', this)
|
||||
|
||||
const { node, index } = this.props
|
||||
const offsetKey = OffsetKey.stringify({
|
||||
key: node.key,
|
||||
index,
|
||||
})
|
||||
|
||||
return (
|
||||
<span data-slate-leaf data-offset-key={offsetKey}>
|
||||
{this.renderMarks()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render all of the leaf's mark components.
|
||||
*
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
renderMarks() {
|
||||
const { marks, node, offset, text, editor } = this.props
|
||||
const leaf = this.renderText()
|
||||
const attributes = {
|
||||
'data-slate-mark': true,
|
||||
}
|
||||
|
||||
return marks.reduce((children, mark) => {
|
||||
const props = {
|
||||
editor,
|
||||
mark,
|
||||
marks,
|
||||
node,
|
||||
offset,
|
||||
text,
|
||||
children,
|
||||
attributes,
|
||||
}
|
||||
const element = editor.run('renderMark', props)
|
||||
return element || children
|
||||
}, leaf)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the text content of the leaf, accounting for browsers.
|
||||
*
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
renderText() {
|
||||
const { block, node, editor, parent, text, index, leaves } = this.props
|
||||
let children
|
||||
|
||||
if (editor.query('isVoid', parent)) {
|
||||
// COMPAT: Render text inside void nodes with a zero-width space.
|
||||
// So the node can contain selection but the text is not visible.
|
||||
if (editor.query('isVoid', parent)) {
|
||||
return (
|
||||
<span data-slate-zero-width="z" data-slate-length={parent.text.length}>
|
||||
{'\uFEFF'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
children = <ZeroWidthString length={parent.text.length} />
|
||||
} else if (
|
||||
text === '' &&
|
||||
parent.object === 'block' &&
|
||||
parent.text === '' &&
|
||||
parent.nodes.last() === node
|
||||
) {
|
||||
// COMPAT: If this is the last text node in an empty block, render a zero-
|
||||
// width space that will convert into a line break when copying and pasting
|
||||
// to support expected plain text.
|
||||
if (
|
||||
text === '' &&
|
||||
parent.object === 'block' &&
|
||||
parent.text === '' &&
|
||||
parent.nodes.last() === node
|
||||
) {
|
||||
return (
|
||||
<span data-slate-zero-width="n" data-slate-length={0}>
|
||||
{'\uFEFF'}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
children = <ZeroWidthString isLineBreak />
|
||||
} else if (text === '') {
|
||||
// COMPAT: If the text is empty, it's because it's on the edge of an inline
|
||||
// node, so we render a zero-width space so that the selection can be
|
||||
// inserted next to it still.
|
||||
if (text === '') {
|
||||
return (
|
||||
<span data-slate-zero-width="z" data-slate-length={0}>
|
||||
{'\uFEFF'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
children = <ZeroWidthString />
|
||||
} else {
|
||||
// COMPAT: Browsers will collapse trailing new lines at the end of blocks,
|
||||
// so we need to add an extra trailing new lines to prevent that.
|
||||
const lastText = block.getLastText()
|
||||
const lastChar = text.charAt(text.length - 1)
|
||||
const isLastText = node === lastText
|
||||
const isLastLeaf = index === leaves.size - 1
|
||||
if (isLastText && isLastLeaf && lastChar === '\n')
|
||||
return <span data-slate-content>{`${text}\n`}</span>
|
||||
|
||||
// Otherwise, just return the content.
|
||||
return <span data-slate-content>{text}</span>
|
||||
if (isLastText && isLastLeaf && lastChar === '\n') {
|
||||
children = <TextString isTrailing text={text} />
|
||||
} else {
|
||||
children = <TextString text={text} />
|
||||
}
|
||||
}
|
||||
|
||||
const renderProps = {
|
||||
editor,
|
||||
marks,
|
||||
annotations,
|
||||
decorations,
|
||||
node,
|
||||
offset,
|
||||
text,
|
||||
}
|
||||
|
||||
// COMPAT: Having the `data-` attributes on these leaf elements ensures that
|
||||
// in certain misbehaving browsers they aren't weirdly cloned/destroyed by
|
||||
// contenteditable behaviors. (2019/05/08)
|
||||
for (const mark of marks) {
|
||||
const ret = editor.run('renderMark', {
|
||||
...renderProps,
|
||||
mark,
|
||||
children,
|
||||
attributes: {
|
||||
[DATA_ATTRS.OBJECT]: 'mark',
|
||||
},
|
||||
})
|
||||
|
||||
if (ret) {
|
||||
children = ret
|
||||
}
|
||||
}
|
||||
|
||||
for (const decoration of decorations) {
|
||||
const ret = editor.run('renderDecoration', {
|
||||
...renderProps,
|
||||
decoration,
|
||||
children,
|
||||
attributes: {
|
||||
[DATA_ATTRS.OBJECT]: 'decoration',
|
||||
},
|
||||
})
|
||||
|
||||
if (ret) {
|
||||
children = ret
|
||||
}
|
||||
}
|
||||
|
||||
for (const annotation of annotations) {
|
||||
const ret = editor.run('renderAnnotation', {
|
||||
...renderProps,
|
||||
annotation,
|
||||
children,
|
||||
attributes: {
|
||||
[DATA_ATTRS.OBJECT]: 'annotation',
|
||||
},
|
||||
})
|
||||
|
||||
if (ret) {
|
||||
children = ret
|
||||
}
|
||||
}
|
||||
|
||||
const attrs = {
|
||||
[DATA_ATTRS.LEAF]: true,
|
||||
[DATA_ATTRS.OFFSET_KEY]: offsetKey,
|
||||
}
|
||||
|
||||
return <span {...attrs}>{children}</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Prop types.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
Leaf.propTypes = {
|
||||
annotations: ImmutableTypes.list.isRequired,
|
||||
block: SlateTypes.block.isRequired,
|
||||
decorations: ImmutableTypes.list.isRequired,
|
||||
editor: Types.object.isRequired,
|
||||
index: Types.number.isRequired,
|
||||
leaves: Types.object.isRequired,
|
||||
marks: SlateTypes.marks.isRequired,
|
||||
node: SlateTypes.node.isRequired,
|
||||
offset: Types.number.isRequired,
|
||||
parent: SlateTypes.node.isRequired,
|
||||
text: Types.string.isRequired,
|
||||
}
|
||||
|
||||
/**
|
||||
* A memoized version of `Leaf` that updates less frequently.
|
||||
*
|
||||
* @type {Component}
|
||||
*/
|
||||
|
||||
const MemoizedLeaf = React.memo(Leaf, (prev, next) => {
|
||||
return (
|
||||
next.index === prev.index &&
|
||||
next.marks === prev.marks &&
|
||||
next.parent === prev.parent &&
|
||||
next.block === prev.block &&
|
||||
next.annotations.equals(prev.annotations) &&
|
||||
next.decorations.equals(prev.decorations)
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Component}
|
||||
*/
|
||||
|
||||
export default Leaf
|
||||
export default MemoizedLeaf
|
||||
|
@@ -4,10 +4,11 @@ import React from 'react'
|
||||
import SlateTypes from 'slate-prop-types'
|
||||
import warning from 'tiny-warning'
|
||||
import Types from 'prop-types'
|
||||
import { PathUtils } from 'slate'
|
||||
|
||||
import Void from './void'
|
||||
import Text from './text'
|
||||
import getChildrenDecorations from '../utils/get-children-decorations'
|
||||
import DATA_ATTRS from '../constants/data-attributes'
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
@@ -31,16 +32,34 @@ class Node extends React.Component {
|
||||
*/
|
||||
|
||||
static propTypes = {
|
||||
annotations: ImmutableTypes.map.isRequired,
|
||||
block: SlateTypes.block,
|
||||
decorations: ImmutableTypes.list.isRequired,
|
||||
editor: Types.object.isRequired,
|
||||
isFocused: Types.bool.isRequired,
|
||||
isSelected: Types.bool.isRequired,
|
||||
node: SlateTypes.node.isRequired,
|
||||
parent: SlateTypes.node.isRequired,
|
||||
parent: SlateTypes.node,
|
||||
readOnly: Types.bool.isRequired,
|
||||
selection: SlateTypes.selection,
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary values.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
tmp = {
|
||||
nodeRefs: {},
|
||||
}
|
||||
|
||||
/**
|
||||
* A ref for the contenteditable DOM node.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
ref = React.createRef()
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
*
|
||||
@@ -77,6 +96,11 @@ class Node extends React.Component {
|
||||
// needs to be updated or not, return true if it returns true. If it returns
|
||||
// false, we need to ignore it, because it shouldn't be allowed it.
|
||||
if (shouldUpdate != null) {
|
||||
warning(
|
||||
false,
|
||||
'As of slate-react@0.22 the `shouldNodeComponentUpdate` middleware is deprecated. You can pass specific values down the tree using React\'s built-in "context" construct instead.'
|
||||
)
|
||||
|
||||
if (shouldUpdate) {
|
||||
return true
|
||||
}
|
||||
@@ -89,24 +113,40 @@ class Node extends React.Component {
|
||||
|
||||
// If the `readOnly` status has changed, re-render in case there is any
|
||||
// user-land logic that depends on it, like nested editable contents.
|
||||
if (n.readOnly !== p.readOnly) return true
|
||||
if (n.readOnly !== p.readOnly) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If the node has changed, update. PERF: There are cases where it will have
|
||||
// changed, but it's properties will be exactly the same (eg. copy-paste)
|
||||
// which this won't catch. But that's rare and not a drag on performance, so
|
||||
// for simplicity we just let them through.
|
||||
if (n.node !== p.node) return true
|
||||
if (n.node !== p.node) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If the selection value of the node or of some of its children has changed,
|
||||
// re-render in case there is any user-land logic depends on it to render.
|
||||
// if the node is selected update it, even if it was already selected: the
|
||||
// selection value of some of its children could have been changed and they
|
||||
// need to be rendered again.
|
||||
if (n.isSelected || p.isSelected) return true
|
||||
if (n.isFocused || p.isFocused) return true
|
||||
if (
|
||||
(!n.selection && p.selection) ||
|
||||
(n.selection && !p.selection) ||
|
||||
(n.selection && p.selection && !n.selection.equals(p.selection))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If the annotations have changed, update.
|
||||
if (!n.annotations.equals(p.annotations)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If the decorations have changed, update.
|
||||
if (!n.decorations.equals(p.decorations)) return true
|
||||
if (!n.decorations.equals(p.decorations)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Otherwise, don't update.
|
||||
return false
|
||||
@@ -121,32 +161,61 @@ class Node extends React.Component {
|
||||
render() {
|
||||
this.debug('render', this)
|
||||
const {
|
||||
editor,
|
||||
isSelected,
|
||||
isFocused,
|
||||
node,
|
||||
annotations,
|
||||
block,
|
||||
decorations,
|
||||
editor,
|
||||
node,
|
||||
parent,
|
||||
readOnly,
|
||||
selection,
|
||||
} = this.props
|
||||
const { value } = editor
|
||||
const { selection } = value
|
||||
const indexes = node.getSelectionIndexes(selection, isSelected)
|
||||
const decs = decorations.concat(node.getDecorations(editor))
|
||||
const childrenDecorations = getChildrenDecorations(node, decs)
|
||||
const children = []
|
||||
|
||||
node.nodes.forEach((child, i) => {
|
||||
const isChildSelected = !!indexes && indexes.start <= i && i < indexes.end
|
||||
const newDecorations = node.getDecorations(editor)
|
||||
const children = node.nodes.toArray().map((child, i) => {
|
||||
const Component = child.object === 'text' ? Text : Node
|
||||
const sel = selection && getRelativeRange(node, i, selection)
|
||||
|
||||
children.push(
|
||||
this.renderNode(child, isChildSelected, childrenDecorations[i])
|
||||
const decs = newDecorations
|
||||
.map(d => getRelativeRange(node, i, d))
|
||||
.filter(d => d)
|
||||
.concat(decorations)
|
||||
|
||||
const anns = annotations
|
||||
.map(a => getRelativeRange(node, i, a))
|
||||
.filter(a => a)
|
||||
|
||||
return (
|
||||
<Component
|
||||
block={node.object === 'block' ? node : block}
|
||||
editor={editor}
|
||||
annotations={anns}
|
||||
decorations={decs}
|
||||
selection={sel}
|
||||
key={child.key}
|
||||
node={child}
|
||||
parent={node}
|
||||
readOnly={readOnly}
|
||||
// COMPAT: We use this map of refs to lookup a DOM node down the
|
||||
// tree of components by path.
|
||||
ref={ref => {
|
||||
if (ref) {
|
||||
this.tmp.nodeRefs[i] = ref
|
||||
} else {
|
||||
delete this.tmp.nodeRefs[i]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
// Attributes that the developer must mix into the element in their
|
||||
// custom node renderer component.
|
||||
const attributes = { 'data-key': node.key }
|
||||
const attributes = {
|
||||
[DATA_ATTRS.OBJECT]: node.object,
|
||||
[DATA_ATTRS.KEY]: node.key,
|
||||
ref: this.ref,
|
||||
}
|
||||
|
||||
// If it's a block node with inline children, add the proper `dir` attribute
|
||||
// for text direction.
|
||||
@@ -155,56 +224,101 @@ class Node extends React.Component {
|
||||
if (direction === 'rtl') attributes.dir = 'rtl'
|
||||
}
|
||||
|
||||
const props = {
|
||||
key: node.key,
|
||||
let render
|
||||
|
||||
if (node.object === 'block') {
|
||||
render = 'renderBlock'
|
||||
} else if (node.object === 'document') {
|
||||
render = 'renderDocument'
|
||||
} else if (node.object === 'inline') {
|
||||
render = 'renderInline'
|
||||
}
|
||||
|
||||
const element = editor.run(render, {
|
||||
attributes,
|
||||
children,
|
||||
editor,
|
||||
isFocused,
|
||||
isSelected,
|
||||
isFocused: !!selection && selection.isFocused,
|
||||
isSelected: !!selection,
|
||||
node,
|
||||
parent,
|
||||
readOnly,
|
||||
}
|
||||
|
||||
const element = editor.run('renderNode', {
|
||||
...props,
|
||||
attributes,
|
||||
children,
|
||||
})
|
||||
|
||||
return editor.query('isVoid', node) ? (
|
||||
<Void {...this.props}>{element}</Void>
|
||||
return editor.isVoid(node) ? (
|
||||
<Void
|
||||
{...this.props}
|
||||
textRef={ref => {
|
||||
if (ref) {
|
||||
this.tmp.nodeRefs[0] = ref
|
||||
} else {
|
||||
delete this.tmp.nodeRefs[0]
|
||||
}
|
||||
}}
|
||||
>
|
||||
{element}
|
||||
</Void>
|
||||
) : (
|
||||
element
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a `child` node.
|
||||
*
|
||||
* @param {Node} child
|
||||
* @param {Boolean} isSelected
|
||||
* @param {Array<Decoration>} decorations
|
||||
* @return {Element}
|
||||
*/
|
||||
/**
|
||||
* Return a `range` relative to a child at `index`.
|
||||
*
|
||||
* @param {Range} range
|
||||
* @param {Number} index
|
||||
* @return {Range}
|
||||
*/
|
||||
|
||||
renderNode = (child, isSelected, decorations) => {
|
||||
const { block, editor, node, readOnly, isFocused } = this.props
|
||||
const Component = child.object === 'text' ? Text : Node
|
||||
|
||||
return (
|
||||
<Component
|
||||
block={node.object === 'block' ? node : block}
|
||||
decorations={decorations}
|
||||
editor={editor}
|
||||
isSelected={isSelected}
|
||||
isFocused={isFocused && isSelected}
|
||||
key={child.key}
|
||||
node={child}
|
||||
parent={node}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)
|
||||
function getRelativeRange(node, index, range) {
|
||||
if (range.isUnset) {
|
||||
return null
|
||||
}
|
||||
|
||||
const child = node.nodes.get(index)
|
||||
let { start, end } = range
|
||||
const { path: startPath } = start
|
||||
const { path: endPath } = end
|
||||
const startIndex = startPath.first()
|
||||
const endIndex = endPath.first()
|
||||
|
||||
if (startIndex === index) {
|
||||
start = start.setPath(startPath.rest())
|
||||
} else if (startIndex < index && index <= endIndex) {
|
||||
if (child.object === 'text') {
|
||||
start = start.moveTo(PathUtils.create([index]), 0)
|
||||
} else {
|
||||
const [first] = child.texts()
|
||||
const [, firstPath] = first
|
||||
start = start.moveTo(firstPath, 0)
|
||||
}
|
||||
} else {
|
||||
start = null
|
||||
}
|
||||
|
||||
if (endIndex === index) {
|
||||
end = end.setPath(endPath.rest())
|
||||
} else if (startIndex <= index && index < endIndex) {
|
||||
if (child.object === 'text') {
|
||||
end = end.moveTo(PathUtils.create([index]), child.text.length)
|
||||
} else {
|
||||
const [last] = child.texts({ direction: 'backward' })
|
||||
const [lastNode, lastPath] = last
|
||||
end = end.moveTo(lastPath, lastNode.text.length)
|
||||
}
|
||||
} else {
|
||||
end = null
|
||||
}
|
||||
|
||||
if (!start || !end) {
|
||||
return null
|
||||
}
|
||||
|
||||
range = range.setStart(start)
|
||||
range = range.setEnd(end)
|
||||
return range
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,182 +1,97 @@
|
||||
import Debug from 'debug'
|
||||
import ImmutableTypes from 'react-immutable-proptypes'
|
||||
import Leaf from './leaf'
|
||||
import { PathUtils } from 'slate'
|
||||
import React from 'react'
|
||||
import SlateTypes from 'slate-prop-types'
|
||||
import Types from 'prop-types'
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
const debug = Debug('slate:node')
|
||||
import Leaf from './leaf'
|
||||
import DATA_ATTRS from '../constants/data-attributes'
|
||||
|
||||
/**
|
||||
* Text.
|
||||
* Text node.
|
||||
*
|
||||
* @type {Component}
|
||||
*/
|
||||
|
||||
class Text extends React.Component {
|
||||
/**
|
||||
* Property types.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
const Text = React.forwardRef((props, ref) => {
|
||||
const { annotations, block, decorations, node, parent, editor, style } = props
|
||||
const { key } = node
|
||||
const leaves = node.getLeaves(annotations, decorations)
|
||||
let at = 0
|
||||
|
||||
static propTypes = {
|
||||
block: SlateTypes.block,
|
||||
decorations: ImmutableTypes.list.isRequired,
|
||||
editor: Types.object.isRequired,
|
||||
node: SlateTypes.node.isRequired,
|
||||
parent: SlateTypes.node.isRequired,
|
||||
style: Types.object,
|
||||
}
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
style={style}
|
||||
{...{
|
||||
[DATA_ATTRS.OBJECT]: node.object,
|
||||
[DATA_ATTRS.KEY]: key,
|
||||
}}
|
||||
>
|
||||
{leaves.map((leaf, index) => {
|
||||
const { text } = leaf
|
||||
const offset = at
|
||||
at += text.length
|
||||
|
||||
/**
|
||||
* Default prop types.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
return (
|
||||
<Leaf
|
||||
key={`${node.key}-${index}`}
|
||||
block={block}
|
||||
editor={editor}
|
||||
index={index}
|
||||
annotations={leaf.annotations}
|
||||
decorations={leaf.decorations}
|
||||
marks={leaf.marks}
|
||||
node={node}
|
||||
offset={offset}
|
||||
parent={parent}
|
||||
leaves={leaves}
|
||||
text={text}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
|
||||
static defaultProps = {
|
||||
style: null,
|
||||
}
|
||||
/**
|
||||
* Prop types.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
*
|
||||
* @param {String} message
|
||||
* @param {Mixed} ...args
|
||||
*/
|
||||
Text.propTypes = {
|
||||
annotations: ImmutableTypes.map.isRequired,
|
||||
block: SlateTypes.block,
|
||||
decorations: ImmutableTypes.list.isRequired,
|
||||
editor: Types.object.isRequired,
|
||||
node: SlateTypes.node.isRequired,
|
||||
parent: SlateTypes.node.isRequired,
|
||||
style: Types.object,
|
||||
}
|
||||
|
||||
debug = (message, ...args) => {
|
||||
const { node } = this.props
|
||||
const { key } = node
|
||||
debug(message, `${key} (text)`, ...args)
|
||||
}
|
||||
/**
|
||||
* A memoized version of `Text` that updates less frequently.
|
||||
*
|
||||
* @type {Component}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Should the node update?
|
||||
*
|
||||
* @param {Object} nextProps
|
||||
* @param {Object} value
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
shouldComponentUpdate = nextProps => {
|
||||
const { props } = this
|
||||
const n = nextProps
|
||||
const p = props
|
||||
|
||||
// If the node has changed, update. PERF: There are cases where it will have
|
||||
const MemoizedText = React.memo(Text, (prev, next) => {
|
||||
return (
|
||||
// PERF: There are cases where it will have
|
||||
// changed, but it's properties will be exactly the same (eg. copy-paste)
|
||||
// which this won't catch. But that's rare and not a drag on performance, so
|
||||
// for simplicity we just let them through.
|
||||
if (n.node !== p.node) return true
|
||||
|
||||
next.node === prev.node &&
|
||||
// If the node parent is a block node, and it was the last child of the
|
||||
// block, re-render to cleanup extra `\n`.
|
||||
if (n.parent.object === 'block') {
|
||||
const pLast = p.parent.nodes.last()
|
||||
const nLast = n.parent.nodes.last()
|
||||
if (p.node === pLast && n.node !== nLast) return true
|
||||
}
|
||||
|
||||
// Re-render if the current decorations have changed.
|
||||
if (!n.decorations.equals(p.decorations)) return true
|
||||
|
||||
// Otherwise, don't update.
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Render.
|
||||
*
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
render() {
|
||||
this.debug('render', this)
|
||||
|
||||
const { decorations, editor, node, style } = this.props
|
||||
const { value } = editor
|
||||
const { document } = value
|
||||
const { key } = node
|
||||
|
||||
const decs = decorations.filter(d => {
|
||||
const { start, end } = d
|
||||
|
||||
// If either of the decoration's keys match, include it.
|
||||
if (start.key === key || end.key === key) return true
|
||||
|
||||
// Otherwise, if the decoration is in a single node, it's not ours.
|
||||
if (start.key === end.key) return false
|
||||
|
||||
const path = document.assertPath(key)
|
||||
const startPath = start.path || document.assertPath(start.key)
|
||||
const endPath = end.path || document.assertPath(end.key)
|
||||
|
||||
// If the node's path is before the start path, ignore it.
|
||||
if (PathUtils.compare(path, startPath) === -1) return false
|
||||
|
||||
// If the node's path is after the end path, ignore it.
|
||||
if (PathUtils.compare(path, endPath) === 1) return false
|
||||
|
||||
// Otherwise, include it.
|
||||
return true
|
||||
})
|
||||
|
||||
// PERF: Take advantage of cache by avoiding arguments
|
||||
const leaves = decs.size === 0 ? node.getLeaves() : node.getLeaves(decs)
|
||||
let offset = 0
|
||||
|
||||
const children = leaves.map((leaf, i) => {
|
||||
const child = this.renderLeaf(leaves, leaf, i, offset)
|
||||
offset += leaf.text.length
|
||||
return child
|
||||
})
|
||||
|
||||
return (
|
||||
<span data-key={key} style={style}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single leaf given a `leaf` and `offset`.
|
||||
*
|
||||
* @param {List<Leaf>} leaves
|
||||
* @param {Leaf} leaf
|
||||
* @param {Number} index
|
||||
* @param {Number} offset
|
||||
* @return {Element} leaf
|
||||
*/
|
||||
|
||||
renderLeaf = (leaves, leaf, index, offset) => {
|
||||
const { block, node, parent, editor } = this.props
|
||||
const { text, marks } = leaf
|
||||
|
||||
return (
|
||||
<Leaf
|
||||
key={`${node.key}-${index}`}
|
||||
block={block}
|
||||
editor={editor}
|
||||
index={index}
|
||||
marks={marks}
|
||||
node={node}
|
||||
offset={offset}
|
||||
parent={parent}
|
||||
leaves={leaves}
|
||||
text={text}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
(next.parent.object === 'block' &&
|
||||
prev.parent.nodes.last() === prev.node &&
|
||||
next.parent.nodes.last() !== next.node) &&
|
||||
// The formatting hasn't changed.
|
||||
next.annotations.equals(prev.annotations) &&
|
||||
next.decorations.equals(prev.decorations)
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Export.
|
||||
@@ -184,4 +99,4 @@ class Text extends React.Component {
|
||||
* @type {Component}
|
||||
*/
|
||||
|
||||
export default Text
|
||||
export default MemoizedText
|
||||
|
@@ -4,6 +4,7 @@ import SlateTypes from 'slate-prop-types'
|
||||
import Types from 'prop-types'
|
||||
|
||||
import Text from './text'
|
||||
import DATA_ATTRS from '../constants/data-attributes'
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
@@ -66,8 +67,12 @@ class Void extends React.Component {
|
||||
position: 'absolute',
|
||||
}
|
||||
|
||||
const spacerAttrs = {
|
||||
[DATA_ATTRS.SPACER]: true,
|
||||
}
|
||||
|
||||
const spacer = (
|
||||
<Tag data-slate-spacer style={style}>
|
||||
<Tag style={style} {...spacerAttrs}>
|
||||
{this.renderText()}
|
||||
</Tag>
|
||||
)
|
||||
@@ -78,11 +83,15 @@ class Void extends React.Component {
|
||||
|
||||
this.debug('render', { props })
|
||||
|
||||
const attrs = {
|
||||
[DATA_ATTRS.VOID]: true,
|
||||
[DATA_ATTRS.KEY]: node.key,
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag
|
||||
data-slate-void
|
||||
data-key={node.key}
|
||||
contentEditable={readOnly || node.object === 'block' ? null : false}
|
||||
{...attrs}
|
||||
>
|
||||
{readOnly ? null : spacer}
|
||||
{content}
|
||||
@@ -102,10 +111,20 @@ class Void extends React.Component {
|
||||
*/
|
||||
|
||||
renderText = () => {
|
||||
const { block, decorations, node, readOnly, editor } = this.props
|
||||
const {
|
||||
annotations,
|
||||
block,
|
||||
decorations,
|
||||
node,
|
||||
readOnly,
|
||||
editor,
|
||||
textRef,
|
||||
} = this.props
|
||||
const child = node.getFirstText()
|
||||
return (
|
||||
<Text
|
||||
ref={textRef}
|
||||
annotations={annotations}
|
||||
block={node.object === 'block' ? node : block}
|
||||
decorations={decorations}
|
||||
editor={editor}
|
||||
|
20
packages/slate-react/src/constants/data-attributes.js
Normal file
20
packages/slate-react/src/constants/data-attributes.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* DOM data attribute strings that refer to Slate concepts.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
|
||||
export default {
|
||||
EDITOR: 'data-slate-editor',
|
||||
FRAGMENT: 'data-slate-fragment',
|
||||
KEY: 'data-key',
|
||||
LEAF: 'data-slate-leaf',
|
||||
LENGTH: 'data-slate-length',
|
||||
OBJECT: 'data-slate-object',
|
||||
OFFSET_KEY: 'data-offset-key',
|
||||
SPACER: 'data-slate-spacer',
|
||||
STRING: 'data-slate-string',
|
||||
TEXT: 'data-slate-object',
|
||||
VOID: 'data-slate-void',
|
||||
ZERO_WIDTH: 'data-slate-zero-width',
|
||||
}
|
20
packages/slate-react/src/constants/selectors.js
Normal file
20
packages/slate-react/src/constants/selectors.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import DATA_ATTRS from './data-attributes'
|
||||
|
||||
/**
|
||||
* DOM selector strings that refer to Slate concepts.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
|
||||
export default {
|
||||
BLOCK: `[${DATA_ATTRS.OBJECT}="block"]`,
|
||||
EDITOR: `[${DATA_ATTRS.EDITOR}]`,
|
||||
INLINE: `[${DATA_ATTRS.OBJECT}="inline"]`,
|
||||
KEY: `[${DATA_ATTRS.KEY}]`,
|
||||
LEAF: `[${DATA_ATTRS.LEAF}]`,
|
||||
OBJECT: `[${DATA_ATTRS.OBJECT}]`,
|
||||
STRING: `[${DATA_ATTRS.STRING}]`,
|
||||
TEXT: `[${DATA_ATTRS.OBJECT}="text"]`,
|
||||
VOID: `[${DATA_ATTRS.VOID}]`,
|
||||
ZERO_WIDTH: `[${DATA_ATTRS.ZERO_WIDTH}]`,
|
||||
}
|
@@ -4,18 +4,10 @@
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
const TRANSFER_TYPES = {
|
||||
export default {
|
||||
FRAGMENT: 'application/x-slate-fragment',
|
||||
HTML: 'text/html',
|
||||
NODE: 'application/x-slate-node',
|
||||
RICH: 'text/rtf',
|
||||
TEXT: 'text/plain',
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
export default TRANSFER_TYPES
|
||||
|
@@ -1,8 +1,11 @@
|
||||
import Editor from './components/editor'
|
||||
import cloneFragment from './utils/clone-fragment'
|
||||
import findDOMNode from './utils/find-dom-node'
|
||||
import findDOMPoint from './utils/find-dom-point'
|
||||
import findDOMRange from './utils/find-dom-range'
|
||||
import findNode from './utils/find-node'
|
||||
import findPath from './utils/find-path'
|
||||
import findPoint from './utils/find-point'
|
||||
import findRange from './utils/find-range'
|
||||
import getEventRange from './utils/get-event-range'
|
||||
import getEventTransfer from './utils/get-event-transfer'
|
||||
@@ -19,8 +22,11 @@ export {
|
||||
Editor,
|
||||
cloneFragment,
|
||||
findDOMNode,
|
||||
findDOMPoint,
|
||||
findDOMRange,
|
||||
findNode,
|
||||
findPath,
|
||||
findPoint,
|
||||
findRange,
|
||||
getEventRange,
|
||||
getEventTransfer,
|
||||
@@ -32,8 +38,11 @@ export default {
|
||||
Editor,
|
||||
cloneFragment,
|
||||
findDOMNode,
|
||||
findDOMPoint,
|
||||
findDOMRange,
|
||||
findNode,
|
||||
findPath,
|
||||
findPoint,
|
||||
findRange,
|
||||
getEventRange,
|
||||
getEventTransfer,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import getSelectionFromDom from './get-selection-from-dom'
|
||||
import getSelectionFromDom from '../../utils/get-selection-from-dom'
|
||||
import ElementSnapshot from './element-snapshot'
|
||||
import SELECTORS from '../../constants/selectors'
|
||||
|
||||
/**
|
||||
* Returns the closest element that matches the selector.
|
||||
@@ -36,7 +37,7 @@ export default class DomSnapshot {
|
||||
constructor(window, editor, { before = false } = {}) {
|
||||
const domSelection = window.getSelection()
|
||||
const { anchorNode } = domSelection
|
||||
const subrootEl = closest(anchorNode, '[data-slate-editor] > *')
|
||||
const subrootEl = closest(anchorNode, `${SELECTORS.EDITOR} > *`)
|
||||
const elements = [subrootEl]
|
||||
|
||||
// The before option is for when we need to take a snapshot of the current
|
||||
@@ -62,6 +63,6 @@ export default class DomSnapshot {
|
||||
apply(editor) {
|
||||
const { snapshot, selection } = this
|
||||
snapshot.apply()
|
||||
editor.moveTo(selection.anchor.key, selection.anchor.offset)
|
||||
editor.moveTo(selection.anchor.path, selection.anchor.offset)
|
||||
}
|
||||
}
|
@@ -1,5 +1,7 @@
|
||||
import getWindow from 'get-window'
|
||||
|
||||
import DATA_ATTRS from '../../constants/data-attributes'
|
||||
|
||||
/**
|
||||
* Is the given node a text node?
|
||||
*
|
||||
@@ -91,7 +93,7 @@ function applyElementSnapshot(snapshot, window) {
|
||||
const key = dataset.key
|
||||
if (!key) return // if there's no `data-key`, don't remove it
|
||||
const dups = new window.Set(
|
||||
Array.from(window.document.querySelectorAll(`[data-key='${key}']`))
|
||||
Array.from(window.document.querySelectorAll(`[${DATA_ATTRS.KEY}="${key}"]`))
|
||||
)
|
||||
dups.delete(el)
|
||||
dups.forEach(dup => dup.parentElement.removeChild(dup))
|
@@ -3,14 +3,13 @@ import getWindow from 'get-window'
|
||||
import pick from 'lodash/pick'
|
||||
|
||||
import { ANDROID_API_VERSION } from 'slate-dev-environment'
|
||||
import fixSelectionInZeroWidthBlock from '../utils/fix-selection-in-zero-width-block'
|
||||
import getSelectionFromDom from '../utils/get-selection-from-dom'
|
||||
import setSelectionFromDom from '../utils/set-selection-from-dom'
|
||||
import setTextFromDomNode from '../utils/set-text-from-dom-node'
|
||||
import isInputDataEnter from '../utils/is-input-data-enter'
|
||||
import isInputDataLastChar from '../utils/is-input-data-last-char'
|
||||
import DomSnapshot from '../utils/dom-snapshot'
|
||||
import Executor from '../utils/executor'
|
||||
import fixSelectionInZeroWidthBlock from './fix-selection-in-zero-width-block'
|
||||
import getSelectionFromDom from '../../utils/get-selection-from-dom'
|
||||
import setTextFromDomNode from '../../utils/set-text-from-dom-node'
|
||||
import isInputDataEnter from './is-input-data-enter'
|
||||
import isInputDataLastChar from './is-input-data-last-char'
|
||||
import DomSnapshot from './dom-snapshot'
|
||||
import Executor from './executor'
|
||||
|
||||
const debug = Debug('slate:android')
|
||||
debug.reconcile = Debug('slate:reconcile')
|
||||
@@ -50,7 +49,7 @@ function AndroidPlugin() {
|
||||
* certain scenarios like hitting 'enter' at the end of a word.
|
||||
*
|
||||
* @type {DomSnapshot} [compositionEndSnapshot]
|
||||
|
||||
|
||||
*/
|
||||
|
||||
let compositionEndSnapshot = null
|
||||
@@ -134,12 +133,13 @@ function AndroidPlugin() {
|
||||
function reconcile(window, editor, { from }) {
|
||||
debug.reconcile({ from })
|
||||
const domSelection = window.getSelection()
|
||||
const selection = getSelectionFromDom(window, editor, domSelection)
|
||||
|
||||
nodes.forEach(node => {
|
||||
setTextFromDomNode(window, editor, node)
|
||||
})
|
||||
|
||||
setSelectionFromDom(window, editor, domSelection)
|
||||
editor.select(selection)
|
||||
nodes.clear()
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ function AndroidPlugin() {
|
||||
const selection = getSelectionFromDom(window, editor, domSelection)
|
||||
preventNextBeforeInput = true
|
||||
event.preventDefault()
|
||||
editor.moveTo(selection.anchor.key, selection.anchor.offset)
|
||||
editor.moveTo(selection.anchor.path, selection.anchor.offset)
|
||||
editor.splitBlock()
|
||||
}
|
||||
} else {
|
||||
@@ -516,7 +516,7 @@ function AndroidPlugin() {
|
||||
// have to grab the selection from the DOM.
|
||||
const domSelection = window.getSelection()
|
||||
const selection = getSelectionFromDom(window, editor, domSelection)
|
||||
editor.moveTo(selection.anchor.key, selection.anchor.offset)
|
||||
editor.moveTo(selection.anchor.path, selection.anchor.offset)
|
||||
editor.splitBlock()
|
||||
}
|
||||
return
|
@@ -5,15 +5,10 @@ import Plain from 'slate-plain-serializer'
|
||||
import getWindow from 'get-window'
|
||||
import { IS_IOS, IS_IE, IS_EDGE } from 'slate-dev-environment'
|
||||
|
||||
import cloneFragment from '../utils/clone-fragment'
|
||||
import findDOMNode from '../utils/find-dom-node'
|
||||
import findNode from '../utils/find-node'
|
||||
import findRange from '../utils/find-range'
|
||||
import getEventRange from '../utils/get-event-range'
|
||||
import getEventTransfer from '../utils/get-event-transfer'
|
||||
import setEventTransfer from '../utils/set-event-transfer'
|
||||
import setSelectionFromDom from '../utils/set-selection-from-dom'
|
||||
import setTextFromDomNode from '../utils/set-text-from-dom-node'
|
||||
import cloneFragment from '../../utils/clone-fragment'
|
||||
import getEventTransfer from '../../utils/get-event-transfer'
|
||||
import setEventTransfer from '../../utils/set-event-transfer'
|
||||
import setTextFromDomNode from '../../utils/set-text-from-dom-node'
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
@@ -65,7 +60,7 @@ function AfterPlugin(options = {}) {
|
||||
event.preventDefault()
|
||||
|
||||
const { document, selection } = value
|
||||
const range = findRange(targetRange, editor)
|
||||
const range = editor.findRange(targetRange)
|
||||
|
||||
switch (event.inputType) {
|
||||
case 'deleteByDrag':
|
||||
@@ -171,12 +166,13 @@ function AfterPlugin(options = {}) {
|
||||
|
||||
const { value } = editor
|
||||
const { document } = value
|
||||
const node = findNode(event.target, editor)
|
||||
if (!node) return next()
|
||||
const path = editor.findPath(event.target)
|
||||
if (!path) return next()
|
||||
|
||||
debug('onClick', { event })
|
||||
|
||||
const ancestors = document.getAncestors(node.key)
|
||||
const node = document.getNode(path)
|
||||
const ancestors = document.getAncestors(path)
|
||||
const isVoid =
|
||||
node && (editor.isVoid(node) || ancestors.some(a => editor.isVoid(a)))
|
||||
|
||||
@@ -222,15 +218,21 @@ function AfterPlugin(options = {}) {
|
||||
// If user cuts a void block node or a void inline node,
|
||||
// manually removes it since selection is collapsed in this case.
|
||||
const { value } = editor
|
||||
const { endBlock, endInline, selection } = value
|
||||
const { isCollapsed } = selection
|
||||
const isVoidBlock = endBlock && editor.isVoid(endBlock) && isCollapsed
|
||||
const isVoidInline = endInline && editor.isVoid(endInline) && isCollapsed
|
||||
const { document, selection } = value
|
||||
const { end, isCollapsed } = selection
|
||||
let voidPath
|
||||
|
||||
if (isVoidBlock) {
|
||||
editor.removeNodeByKey(endBlock.key)
|
||||
} else if (isVoidInline) {
|
||||
editor.removeNodeByKey(endInline.key)
|
||||
if (isCollapsed) {
|
||||
for (const [node, path] of document.ancestors(end.path)) {
|
||||
if (editor.isVoid(node)) {
|
||||
voidPath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (voidPath) {
|
||||
editor.removeNodeByKey(voidPath)
|
||||
} else {
|
||||
editor.delete()
|
||||
}
|
||||
@@ -268,13 +270,12 @@ function AfterPlugin(options = {}) {
|
||||
|
||||
const { value } = editor
|
||||
const { document } = value
|
||||
const node = findNode(event.target, editor)
|
||||
const ancestors = document.getAncestors(node.key)
|
||||
const path = editor.findPath(event.target)
|
||||
const node = document.getNode(path)
|
||||
const ancestors = document.getAncestors(path)
|
||||
const isVoid =
|
||||
node && (editor.isVoid(node) || ancestors.some(a => editor.isVoid(a)))
|
||||
const selectionIncludesNode = value.blocks.some(
|
||||
block => block.key === node.key
|
||||
)
|
||||
const selectionIncludesNode = value.blocks.some(block => block === node)
|
||||
|
||||
// If a void block is dragged and is not selected, select it (necessary for local drags).
|
||||
if (isVoid && !selectionIncludesNode) {
|
||||
@@ -299,8 +300,11 @@ function AfterPlugin(options = {}) {
|
||||
const { value } = editor
|
||||
const { document, selection } = value
|
||||
const window = getWindow(event.target)
|
||||
let target = getEventRange(event, editor)
|
||||
if (!target) return next()
|
||||
let target = editor.findEventRange(event)
|
||||
|
||||
if (!target) {
|
||||
return next()
|
||||
}
|
||||
|
||||
debug('onDrop', { event })
|
||||
|
||||
@@ -313,11 +317,11 @@ function AfterPlugin(options = {}) {
|
||||
// needs to account for the selection's content being deleted.
|
||||
if (
|
||||
isDraggingInternally &&
|
||||
selection.end.key === target.end.key &&
|
||||
selection.end.offset < target.end.offset
|
||||
selection.end.offset < target.end.offset &&
|
||||
selection.end.path.equals(target.end.path)
|
||||
) {
|
||||
target = target.moveForward(
|
||||
selection.start.key === selection.end.key
|
||||
selection.start.path.equals(selection.end.path)
|
||||
? 0 - selection.end.offset + selection.start.offset
|
||||
: 0 - selection.end.offset
|
||||
)
|
||||
@@ -331,15 +335,21 @@ function AfterPlugin(options = {}) {
|
||||
|
||||
if (type === 'text' || type === 'html') {
|
||||
const { anchor } = target
|
||||
let hasVoidParent = document.hasVoidParent(anchor.key, editor)
|
||||
let hasVoidParent = document.hasVoidParent(anchor.path, editor)
|
||||
|
||||
if (hasVoidParent) {
|
||||
let n = document.getNode(anchor.key)
|
||||
let p = anchor.path
|
||||
let n = document.getNode(anchor.path)
|
||||
|
||||
while (hasVoidParent) {
|
||||
n = document.getNextText(n.key)
|
||||
if (!n) break
|
||||
hasVoidParent = document.hasVoidParent(n.key, editor)
|
||||
const [nxt] = document.texts({ path: p })
|
||||
|
||||
if (!nxt) {
|
||||
break
|
||||
}
|
||||
|
||||
;[n, p] = nxt
|
||||
hasVoidParent = document.hasVoidParent(p, editor)
|
||||
}
|
||||
|
||||
if (n) editor.moveToStartOfNode(n)
|
||||
@@ -361,8 +371,7 @@ function AfterPlugin(options = {}) {
|
||||
// has fired in a node: https://github.com/facebook/react/issues/11379.
|
||||
// Until this is fixed in React, we dispatch a mouseup event on that
|
||||
// DOM node, since that will make it go back to normal.
|
||||
const focusNode = document.getNode(target.focus.key)
|
||||
const el = findDOMNode(focusNode, window)
|
||||
const el = editor.findDOMNode(target.focus.path)
|
||||
|
||||
if (el) {
|
||||
el.dispatchEvent(
|
||||
@@ -411,14 +420,20 @@ function AfterPlugin(options = {}) {
|
||||
|
||||
function onInput(event, editor, next) {
|
||||
debug('onInput')
|
||||
|
||||
const window = getWindow(event.target)
|
||||
const domSelection = window.getSelection()
|
||||
const selection = editor.findSelection(domSelection)
|
||||
|
||||
// Get the selection point.
|
||||
const selection = window.getSelection()
|
||||
const { anchorNode } = selection
|
||||
if (selection) {
|
||||
editor.select(selection)
|
||||
} else {
|
||||
editor.blur()
|
||||
}
|
||||
|
||||
const { anchorNode } = domSelection
|
||||
setTextFromDomNode(window, editor, anchorNode)
|
||||
setSelectionFromDom(window, editor, selection)
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
@@ -435,7 +450,8 @@ function AfterPlugin(options = {}) {
|
||||
|
||||
const { value } = editor
|
||||
const { document, selection } = value
|
||||
const hasVoidParent = document.hasVoidParent(selection.start.path, editor)
|
||||
const { start } = selection
|
||||
const hasVoidParent = document.hasVoidParent(start.path, editor)
|
||||
|
||||
// COMPAT: In iOS, some of these hotkeys are handled in the
|
||||
// `onNativeBeforeInput` handler of the `<Content>` component in order to
|
||||
@@ -535,20 +551,34 @@ function AfterPlugin(options = {}) {
|
||||
}
|
||||
|
||||
if (Hotkeys.isExtendBackward(event)) {
|
||||
const { previousText, startText } = value
|
||||
const isPreviousInVoid =
|
||||
previousText && document.hasVoidParent(previousText.key, editor)
|
||||
const startText = document.getNode(start.path)
|
||||
const prevEntry = document.texts({
|
||||
path: start.path,
|
||||
direction: 'backward',
|
||||
})
|
||||
|
||||
if (hasVoidParent || isPreviousInVoid || startText.text === '') {
|
||||
let isPrevInVoid = false
|
||||
|
||||
if (prevEntry) {
|
||||
const [, prevPath] = prevEntry
|
||||
isPrevInVoid = document.hasVoidParent(prevPath, editor)
|
||||
}
|
||||
|
||||
if (hasVoidParent || isPrevInVoid || startText.text === '') {
|
||||
event.preventDefault()
|
||||
return editor.moveFocusBackward()
|
||||
}
|
||||
}
|
||||
|
||||
if (Hotkeys.isExtendForward(event)) {
|
||||
const { nextText, startText } = value
|
||||
const isNextInVoid =
|
||||
nextText && document.hasVoidParent(nextText.key, editor)
|
||||
const startText = document.getNode(start.path)
|
||||
const [nextEntry] = document.texts({ path: start.path })
|
||||
let isNextInVoid = false
|
||||
|
||||
if (nextEntry) {
|
||||
const [, nextPath] = nextEntry
|
||||
isNextInVoid = document.hasVoidParent(nextPath, editor)
|
||||
}
|
||||
|
||||
if (hasVoidParent || isNextInVoid || startText.text === '') {
|
||||
event.preventDefault()
|
||||
@@ -632,8 +662,14 @@ function AfterPlugin(options = {}) {
|
||||
function onSelect(event, editor, next) {
|
||||
debug('onSelect', { event })
|
||||
const window = getWindow(event.target)
|
||||
const selection = window.getSelection()
|
||||
setSelectionFromDom(window, editor, selection)
|
||||
const domSelection = window.getSelection()
|
||||
const selection = editor.findSelection(domSelection)
|
||||
|
||||
if (selection) {
|
||||
editor.select(selection)
|
||||
} else {
|
||||
editor.blur()
|
||||
}
|
||||
|
||||
// COMPAT: reset the `isMouseDown` state here in case a `mouseup` event
|
||||
// happens outside the editor. This is needed for `onFocus` handling.
|
@@ -1,6 +1,5 @@
|
||||
import Debug from 'debug'
|
||||
import Hotkeys from 'slate-hotkeys'
|
||||
import ReactDOM from 'react-dom'
|
||||
import getWindow from 'get-window'
|
||||
import {
|
||||
IS_FIREFOX,
|
||||
@@ -9,7 +8,7 @@ import {
|
||||
HAS_INPUT_EVENTS_LEVEL_2,
|
||||
} from 'slate-dev-environment'
|
||||
|
||||
import findNode from '../utils/find-node'
|
||||
import DATA_ATTRS from '../../constants/data-attributes'
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
@@ -77,7 +76,7 @@ function BeforePlugin() {
|
||||
// COMPAT: The `relatedTarget` can be null when the new focus target is not
|
||||
// a "focusable" element (eg. a `<div>` without `tabindex` set).
|
||||
if (relatedTarget) {
|
||||
const el = ReactDOM.findDOMNode(editor)
|
||||
const el = editor.findDOMNode([])
|
||||
|
||||
// COMPAT: The event should be ignored if the focus is returning to the
|
||||
// editor from an embedded editable element (eg. an <input> element inside
|
||||
@@ -86,13 +85,16 @@ function BeforePlugin() {
|
||||
|
||||
// COMPAT: The event should be ignored if the focus is moving from the
|
||||
// editor to inside a void node's spacer element.
|
||||
if (relatedTarget.hasAttribute('data-slate-spacer')) return
|
||||
if (relatedTarget.hasAttribute(DATA_ATTRS.SPACER)) return
|
||||
|
||||
// COMPAT: The event should be ignored if the focus is moving to a non-
|
||||
// editable section of an element that isn't a void node (eg. a list item
|
||||
// of the check list example).
|
||||
const node = findNode(relatedTarget, editor)
|
||||
if (el.contains(relatedTarget) && node && !editor.isVoid(node)) return
|
||||
const node = editor.findNode(relatedTarget)
|
||||
|
||||
if (el.contains(relatedTarget) && node && !editor.isVoid(node)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
debug('onBlur', { event })
|
||||
@@ -267,8 +269,11 @@ function BeforePlugin() {
|
||||
// call `preventDefault` to signal that drops are allowed.
|
||||
// When the target is editable, dropping is already allowed by
|
||||
// default, and calling `preventDefault` hides the cursor.
|
||||
const node = findNode(event.target, editor)
|
||||
if (editor.isVoid(node)) event.preventDefault()
|
||||
const node = editor.findNode(event.target)
|
||||
|
||||
if (editor.isVoid(node)) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
// COMPAT: IE won't call onDrop on contentEditables unless the
|
||||
// default dragOver is prevented:
|
||||
@@ -337,7 +342,7 @@ function BeforePlugin() {
|
||||
if (isCopying) return
|
||||
if (editor.readOnly) return
|
||||
|
||||
const el = ReactDOM.findDOMNode(editor)
|
||||
const el = editor.findDOMNode([])
|
||||
|
||||
// Save the new `activeElement`.
|
||||
const window = getWindow(event.target)
|
@@ -1,5 +1,5 @@
|
||||
import { IS_ANDROID } from 'slate-dev-environment'
|
||||
import AndroidPlugin from './android'
|
||||
import AndroidPlugin from '../android'
|
||||
import AfterPlugin from './after'
|
||||
import BeforePlugin from './before'
|
||||
|
||||
@@ -12,12 +12,14 @@ import BeforePlugin from './before'
|
||||
|
||||
function DOMPlugin(options = {}) {
|
||||
const { plugins = [] } = options
|
||||
// Add Android specific handling separately before it gets to the other
|
||||
// plugins because it is specific (other browser don't need it) and finicky
|
||||
// (it has to come before other plugins to work).
|
||||
const beforeBeforePlugins = IS_ANDROID ? [AndroidPlugin()] : []
|
||||
const beforePlugin = BeforePlugin()
|
||||
const afterPlugin = AfterPlugin()
|
||||
|
||||
// COMPAT: Add Android specific handling separately before it gets to the
|
||||
// other plugins because it is specific (other browser don't need it) and
|
||||
// finicky (it has to come before other plugins to work).
|
||||
const beforeBeforePlugins = IS_ANDROID ? [AndroidPlugin()] : []
|
||||
|
||||
return [...beforeBeforePlugins, beforePlugin, ...plugins, afterPlugin]
|
||||
}
|
||||
|
144
packages/slate-react/src/plugins/react.js
vendored
144
packages/slate-react/src/plugins/react.js
vendored
@@ -1,144 +0,0 @@
|
||||
import PlaceholderPlugin from 'slate-react-placeholder'
|
||||
import React from 'react'
|
||||
|
||||
import DOMPlugin from './dom'
|
||||
import Content from '../components/content'
|
||||
import EVENT_HANDLERS from '../constants/event-handlers'
|
||||
|
||||
/**
|
||||
* Props that can be defined by plugins.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const PROPS = [
|
||||
...EVENT_HANDLERS,
|
||||
'commands',
|
||||
'decorateNode',
|
||||
'queries',
|
||||
'renderEditor',
|
||||
'renderMark',
|
||||
'renderNode',
|
||||
'schema',
|
||||
]
|
||||
|
||||
/**
|
||||
* A plugin that adds the React-specific rendering logic to the editor.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function ReactPlugin(options = {}) {
|
||||
const { placeholder, plugins = [] } = options
|
||||
|
||||
/**
|
||||
* Decorate node.
|
||||
*
|
||||
* @param {Object} node
|
||||
* @param {Editor} editor
|
||||
* @param {Function} next
|
||||
* @return {Array}
|
||||
*/
|
||||
|
||||
function decorateNode(node, editor, next) {
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Render editor.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Editor} editor
|
||||
* @param {Function} next
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
function renderEditor(props, editor, next) {
|
||||
return (
|
||||
<Content
|
||||
autoCorrect={props.autoCorrect}
|
||||
className={props.className}
|
||||
editor={editor}
|
||||
id={props.id}
|
||||
onEvent={(handler, event) => editor.run(handler, event)}
|
||||
readOnly={props.readOnly}
|
||||
role={props.role}
|
||||
spellCheck={props.spellCheck}
|
||||
style={props.style}
|
||||
tabIndex={props.tabIndex}
|
||||
tagName={props.tagName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render node.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Editor} editor
|
||||
* @param {Function} next
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
function renderNode(props, editor, next) {
|
||||
const { attributes, children, node } = props
|
||||
const { object } = node
|
||||
if (object !== 'block' && object !== 'inline') return null
|
||||
|
||||
const Tag = object === 'block' ? 'div' : 'span'
|
||||
const style = { position: 'relative' }
|
||||
return (
|
||||
<Tag {...attributes} style={style}>
|
||||
{children}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the plugins.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const ret = []
|
||||
const editorPlugin = PROPS.reduce((memo, prop) => {
|
||||
if (prop in options) memo[prop] = options[prop]
|
||||
return memo
|
||||
}, {})
|
||||
|
||||
ret.push(
|
||||
DOMPlugin({
|
||||
plugins: [editorPlugin, ...plugins],
|
||||
})
|
||||
)
|
||||
|
||||
if (placeholder) {
|
||||
ret.push(
|
||||
PlaceholderPlugin({
|
||||
placeholder,
|
||||
when: (editor, node) =>
|
||||
node.object === 'document' &&
|
||||
node.text === '' &&
|
||||
node.nodes.size === 1 &&
|
||||
node.getTexts().size === 1,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
ret.push({
|
||||
decorateNode,
|
||||
renderEditor,
|
||||
renderNode,
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default ReactPlugin
|
46
packages/slate-react/src/plugins/react/editor-props.js
Normal file
46
packages/slate-react/src/plugins/react/editor-props.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import EVENT_HANDLERS from '../../constants/event-handlers'
|
||||
|
||||
/**
|
||||
* Props that can be defined by plugins.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const PROPS = [
|
||||
...EVENT_HANDLERS,
|
||||
'commands',
|
||||
'decorateNode',
|
||||
'queries',
|
||||
'renderAnnotation',
|
||||
'renderBlock',
|
||||
'renderDecoration',
|
||||
'renderDocument',
|
||||
'renderEditor',
|
||||
'renderInline',
|
||||
'renderMark',
|
||||
'schema',
|
||||
]
|
||||
|
||||
/**
|
||||
* The top-level editor props in a plugin.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function EditorPropsPlugin(options = {}) {
|
||||
const plugin = PROPS.reduce((memo, prop) => {
|
||||
if (prop in options) memo[prop] = options[prop]
|
||||
return memo
|
||||
}, {})
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default EditorPropsPlugin
|
42
packages/slate-react/src/plugins/react/index.js
Normal file
42
packages/slate-react/src/plugins/react/index.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import PlaceholderPlugin from 'slate-react-placeholder'
|
||||
|
||||
import EditorPropsPlugin from './editor-props'
|
||||
import RenderingPlugin from './rendering'
|
||||
import QueriesPlugin from './queries'
|
||||
import DOMPlugin from '../dom'
|
||||
|
||||
/**
|
||||
* A plugin that adds the React-specific rendering logic to the editor.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function ReactPlugin(options = {}) {
|
||||
const { placeholder = '', plugins = [] } = options
|
||||
const renderingPlugin = RenderingPlugin(options)
|
||||
const queriesPlugin = QueriesPlugin(options)
|
||||
const editorPropsPlugin = EditorPropsPlugin(options)
|
||||
const domPlugin = DOMPlugin({
|
||||
plugins: [editorPropsPlugin, ...plugins],
|
||||
})
|
||||
|
||||
const placeholderPlugin = PlaceholderPlugin({
|
||||
placeholder,
|
||||
when: (editor, node) =>
|
||||
node.object === 'document' &&
|
||||
node.text === '' &&
|
||||
node.nodes.size === 1 &&
|
||||
Array.from(node.texts()).length === 1,
|
||||
})
|
||||
|
||||
return [domPlugin, placeholderPlugin, renderingPlugin, queriesPlugin]
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default ReactPlugin
|
623
packages/slate-react/src/plugins/react/queries.js
Normal file
623
packages/slate-react/src/plugins/react/queries.js
Normal file
@@ -0,0 +1,623 @@
|
||||
import getWindow from 'get-window'
|
||||
import { PathUtils } from 'slate'
|
||||
|
||||
import DATA_ATTRS from '../../constants/data-attributes'
|
||||
import SELECTORS from '../../constants/selectors'
|
||||
|
||||
/**
|
||||
* A set of queries for the React plugin.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function QueriesPlugin() {
|
||||
/**
|
||||
* Find the native DOM element for a node at `path`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
* @param {Array|List} path
|
||||
* @return {DOMNode|Null}
|
||||
*/
|
||||
|
||||
function findDOMNode(editor, path) {
|
||||
path = PathUtils.create(path)
|
||||
const content = editor.tmp.contentRef.current
|
||||
|
||||
if (!path.size) {
|
||||
return content.ref.current || null
|
||||
}
|
||||
|
||||
const search = (instance, p) => {
|
||||
if (!instance) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!p.size) {
|
||||
if (instance.ref) {
|
||||
return instance.ref.current || null
|
||||
} else {
|
||||
return instance || null
|
||||
}
|
||||
}
|
||||
|
||||
const index = p.first()
|
||||
const rest = p.rest()
|
||||
const ref = instance.tmp.nodeRefs[index]
|
||||
return search(ref, rest)
|
||||
}
|
||||
|
||||
const document = content.tmp.nodeRef.current
|
||||
const el = search(document, path)
|
||||
return el
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a native DOM selection point from a Slate `point`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
* @param {Point} point
|
||||
* @return {Object|Null}
|
||||
*/
|
||||
|
||||
function findDOMPoint(editor, point) {
|
||||
const el = editor.findDOMNode(point.path)
|
||||
let start = 0
|
||||
|
||||
if (!el) {
|
||||
return null
|
||||
}
|
||||
|
||||
// For each leaf, we need to isolate its content, which means filtering to its
|
||||
// direct text and zero-width spans. (We have to filter out any other siblings
|
||||
// that may have been rendered alongside them.)
|
||||
const texts = Array.from(
|
||||
el.querySelectorAll(`${SELECTORS.STRING}, ${SELECTORS.ZERO_WIDTH}`)
|
||||
)
|
||||
|
||||
for (const text of texts) {
|
||||
const node = text.childNodes[0]
|
||||
const domLength = node.textContent.length
|
||||
let slateLength = domLength
|
||||
|
||||
if (text.hasAttribute(DATA_ATTRS.LENGTH)) {
|
||||
slateLength = parseInt(text.getAttribute(DATA_ATTRS.LENGTH), 10)
|
||||
}
|
||||
|
||||
const end = start + slateLength
|
||||
|
||||
if (point.offset <= end) {
|
||||
const offset = Math.min(domLength, Math.max(0, point.offset - start))
|
||||
return { node, offset }
|
||||
}
|
||||
|
||||
start = end
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a native DOM range from a Slate `range`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
* @param {Range} range
|
||||
* @return {DOMRange|Null}
|
||||
*/
|
||||
|
||||
function findDOMRange(editor, range) {
|
||||
const { anchor, focus, isBackward, isCollapsed } = range
|
||||
const domAnchor = editor.findDOMPoint(anchor)
|
||||
const domFocus = isCollapsed ? domAnchor : editor.findDOMPoint(focus)
|
||||
|
||||
if (!domAnchor || !domFocus) {
|
||||
return null
|
||||
}
|
||||
|
||||
const window = getWindow(domAnchor.node)
|
||||
const r = window.document.createRange()
|
||||
const start = isBackward ? domFocus : domAnchor
|
||||
const end = isBackward ? domAnchor : domFocus
|
||||
r.setStart(start.node, start.offset)
|
||||
r.setEnd(end.node, end.offset)
|
||||
return r
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a Slate node from a native DOM `element`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
* @param {Element} element
|
||||
* @return {List|Null}
|
||||
*/
|
||||
|
||||
function findNode(editor, element) {
|
||||
const path = editor.findPath(element)
|
||||
|
||||
if (!path) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { value } = editor
|
||||
const { document } = value
|
||||
const node = document.getNode(path)
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target range from a DOM `event`.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Editor} editor
|
||||
* @return {Range}
|
||||
*/
|
||||
|
||||
function findEventRange(editor, event) {
|
||||
if (event.nativeEvent) {
|
||||
event = event.nativeEvent
|
||||
}
|
||||
|
||||
const { clientX: x, clientY: y, target } = event
|
||||
if (x == null || y == null) return null
|
||||
|
||||
const { value } = editor
|
||||
const { document } = value
|
||||
const path = editor.findPath(event.target)
|
||||
if (!path) return null
|
||||
|
||||
const node = document.getNode(path)
|
||||
|
||||
// If the drop target is inside a void node, move it into either the next or
|
||||
// previous node, depending on which side the `x` and `y` coordinates are
|
||||
// closest to.
|
||||
if (editor.isVoid(node)) {
|
||||
const rect = target.getBoundingClientRect()
|
||||
const isPrevious =
|
||||
node.object === 'inline'
|
||||
? x - rect.left < rect.left + rect.width - x
|
||||
: y - rect.top < rect.top + rect.height - y
|
||||
|
||||
const range = document.createRange()
|
||||
const iterable = isPrevious ? 'previousTexts' : 'nextTexts'
|
||||
const move = isPrevious ? 'moveToEndOfNode' : 'moveToStartOfNode'
|
||||
const entry = document[iterable](path)
|
||||
|
||||
if (entry) {
|
||||
const [n] = entry
|
||||
return range[move](n)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Else resolve a range from the caret position where the drop occured.
|
||||
const window = getWindow(target)
|
||||
let native
|
||||
|
||||
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
|
||||
if (window.document.caretRangeFromPoint) {
|
||||
native = window.document.caretRangeFromPoint(x, y)
|
||||
} else if (window.document.caretPositionFromPoint) {
|
||||
const position = window.document.caretPositionFromPoint(x, y)
|
||||
native = window.document.createRange()
|
||||
native.setStart(position.offsetNode, position.offset)
|
||||
native.setEnd(position.offsetNode, position.offset)
|
||||
} else if (window.document.body.createTextRange) {
|
||||
// COMPAT: In IE, `caretRangeFromPoint` and
|
||||
// `caretPositionFromPoint` don't exist. (2018/07/11)
|
||||
native = window.document.body.createTextRange()
|
||||
|
||||
try {
|
||||
native.moveToPoint(x, y)
|
||||
} catch (error) {
|
||||
// IE11 will raise an `unspecified error` if `moveToPoint` is
|
||||
// called during a dropEvent.
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve a Slate range from the DOM range.
|
||||
const range = editor.findRange(native)
|
||||
return range
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the path of a native DOM `element` by searching React refs.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
* @param {Element} element
|
||||
* @return {List|Null}
|
||||
*/
|
||||
|
||||
function findPath(editor, element) {
|
||||
const content = editor.tmp.contentRef.current
|
||||
|
||||
if (element === content.ref.current) {
|
||||
return PathUtils.create([])
|
||||
}
|
||||
|
||||
const search = (instance, p) => {
|
||||
if (element === instance) {
|
||||
return p
|
||||
}
|
||||
|
||||
if (!instance.ref) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (element === instance.ref.current) {
|
||||
return p
|
||||
}
|
||||
|
||||
// If there's no `tmp` then we're at a leaf node without success.
|
||||
if (!instance.tmp) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { nodeRefs } = instance.tmp
|
||||
const keys = Object.keys(nodeRefs)
|
||||
|
||||
for (const i of keys) {
|
||||
const ref = nodeRefs[i]
|
||||
const n = parseInt(i, 10)
|
||||
const path = search(ref, [...p, n])
|
||||
|
||||
if (path) {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const document = content.tmp.nodeRef.current
|
||||
const path = search(document, [])
|
||||
|
||||
if (!path) {
|
||||
return null
|
||||
}
|
||||
|
||||
return PathUtils.create(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a Slate point from a DOM selection's `nativeNode` and `nativeOffset`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
* @param {Element} nativeNode
|
||||
* @param {Number} nativeOffset
|
||||
* @return {Point}
|
||||
*/
|
||||
|
||||
function findPoint(editor, nativeNode, nativeOffset) {
|
||||
const { node: nearestNode, offset: nearestOffset } = normalizeNodeAndOffset(
|
||||
nativeNode,
|
||||
nativeOffset
|
||||
)
|
||||
|
||||
const window = getWindow(nativeNode)
|
||||
const { parentNode } = nearestNode
|
||||
let leafNode = parentNode.closest(SELECTORS.LEAF)
|
||||
let textNode
|
||||
let offset
|
||||
let node
|
||||
|
||||
// Calculate how far into the text node the `nearestNode` is, so that we can
|
||||
// determine what the offset relative to the text node is.
|
||||
if (leafNode) {
|
||||
textNode = leafNode.closest(SELECTORS.TEXT)
|
||||
const range = window.document.createRange()
|
||||
range.setStart(textNode, 0)
|
||||
range.setEnd(nearestNode, nearestOffset)
|
||||
const contents = range.cloneContents()
|
||||
const zeroWidths = contents.querySelectorAll(SELECTORS.ZERO_WIDTH)
|
||||
|
||||
Array.from(zeroWidths).forEach(el => {
|
||||
el.parentNode.removeChild(el)
|
||||
})
|
||||
|
||||
// COMPAT: Edge has a bug where Range.prototype.toString() will convert \n
|
||||
// into \r\n. The bug causes a loop when slate-react attempts to reposition
|
||||
// its cursor to match the native position. Use textContent.length instead.
|
||||
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10291116/
|
||||
offset = contents.textContent.length
|
||||
node = textNode
|
||||
} else {
|
||||
// For void nodes, the element with the offset key will be a cousin, not an
|
||||
// ancestor, so find it by going down from the nearest void parent.
|
||||
const voidNode = parentNode.closest(SELECTORS.VOID)
|
||||
|
||||
if (!voidNode) {
|
||||
return null
|
||||
}
|
||||
|
||||
leafNode = voidNode.querySelector(SELECTORS.LEAF)
|
||||
|
||||
if (!leafNode) {
|
||||
return null
|
||||
}
|
||||
|
||||
textNode = leafNode.closest(SELECTORS.TEXT)
|
||||
node = leafNode
|
||||
offset = node.textContent.length
|
||||
}
|
||||
|
||||
// COMPAT: If the parent node is a Slate zero-width space, this is because the
|
||||
// text node should have no characters. However, during IME composition the
|
||||
// ASCII characters will be prepended to the zero-width space, so subtract 1
|
||||
// from the offset to account for the zero-width space character.
|
||||
if (
|
||||
offset === node.textContent.length &&
|
||||
parentNode.hasAttribute(DATA_ATTRS.ZERO_WIDTH)
|
||||
) {
|
||||
offset--
|
||||
}
|
||||
|
||||
// COMPAT: If someone is clicking from one Slate editor into another, the
|
||||
// select event fires twice, once for the old editor's `element` first, and
|
||||
// then afterwards for the correct `element`. (2017/03/03)
|
||||
const path = editor.findPath(textNode)
|
||||
|
||||
if (!path) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { value } = editor
|
||||
const { document } = value
|
||||
const point = document.createPoint({ path, offset })
|
||||
return point
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a Slate range from a DOM range or selection.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
* @param {Selection} domRange
|
||||
* @return {Range}
|
||||
*/
|
||||
|
||||
function findRange(editor, domRange) {
|
||||
const el = domRange.anchorNode || domRange.startContainer
|
||||
|
||||
if (!el) {
|
||||
return null
|
||||
}
|
||||
|
||||
const window = getWindow(el)
|
||||
|
||||
// If the `domRange` object is a DOM `Range` or `StaticRange` object, change it
|
||||
// into something that looks like a DOM `Selection` instead.
|
||||
if (
|
||||
domRange instanceof window.Range ||
|
||||
(window.StaticRange && domRange instanceof window.StaticRange)
|
||||
) {
|
||||
domRange = {
|
||||
anchorNode: domRange.startContainer,
|
||||
anchorOffset: domRange.startOffset,
|
||||
focusNode: domRange.endContainer,
|
||||
focusOffset: domRange.endOffset,
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
anchorNode,
|
||||
anchorOffset,
|
||||
focusNode,
|
||||
focusOffset,
|
||||
isCollapsed,
|
||||
} = domRange
|
||||
const { value } = editor
|
||||
const anchor = editor.findPoint(anchorNode, anchorOffset)
|
||||
const focus = isCollapsed
|
||||
? anchor
|
||||
: editor.findPoint(focusNode, focusOffset)
|
||||
|
||||
if (!anchor || !focus) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { document } = value
|
||||
const range = document.createRange({
|
||||
anchor,
|
||||
focus,
|
||||
})
|
||||
|
||||
return range
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a Slate selection from a DOM selection.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
* @param {Selection} domSelection
|
||||
* @return {Range}
|
||||
*/
|
||||
|
||||
function findSelection(editor, domSelection) {
|
||||
const { value } = editor
|
||||
const { document } = value
|
||||
|
||||
// If there are no ranges, the editor was blurred natively.
|
||||
if (!domSelection.rangeCount) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Otherwise, determine the Slate selection from the native one.
|
||||
let range = editor.findRange(domSelection)
|
||||
|
||||
if (!range) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { anchor, focus } = range
|
||||
const anchorText = document.getNode(anchor.path)
|
||||
const focusText = document.getNode(focus.path)
|
||||
const anchorInline = document.getClosestInline(anchor.path)
|
||||
const focusInline = document.getClosestInline(focus.path)
|
||||
const focusBlock = document.getClosestBlock(focus.path)
|
||||
const anchorBlock = document.getClosestBlock(anchor.path)
|
||||
|
||||
// COMPAT: If the anchor point is at the start of a non-void, and the
|
||||
// focus point is inside a void node with an offset that isn't `0`, set
|
||||
// the focus offset to `0`. This is due to void nodes <span>'s being
|
||||
// positioned off screen, resulting in the offset always being greater
|
||||
// than `0`. Since we can't know what it really should be, and since an
|
||||
// offset of `0` is less destructive because it creates a hanging
|
||||
// selection, go with `0`. (2017/09/07)
|
||||
if (
|
||||
anchorBlock &&
|
||||
!editor.isVoid(anchorBlock) &&
|
||||
anchor.offset === 0 &&
|
||||
focusBlock &&
|
||||
editor.isVoid(focusBlock) &&
|
||||
focus.offset !== 0
|
||||
) {
|
||||
range = range.setFocus(focus.setOffset(0))
|
||||
}
|
||||
|
||||
// COMPAT: If the selection is at the end of a non-void inline node, and
|
||||
// there is a node after it, put it in the node after instead. This
|
||||
// standardizes the behavior, since it's indistinguishable to the user.
|
||||
if (
|
||||
anchorInline &&
|
||||
!editor.isVoid(anchorInline) &&
|
||||
anchor.offset === anchorText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(anchor.path)
|
||||
const [next] = block.texts({ path: anchor.path })
|
||||
|
||||
if (next) {
|
||||
const [, nextPath] = next
|
||||
range = range.moveAnchorTo(nextPath, 0)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
focusInline &&
|
||||
!editor.isVoid(focusInline) &&
|
||||
focus.offset === focusText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(focus.path)
|
||||
const [next] = block.texts({ path: focus.path })
|
||||
|
||||
if (next) {
|
||||
const [, nextPath] = next
|
||||
range = range.moveFocusTo(nextPath, 0)
|
||||
}
|
||||
}
|
||||
|
||||
let selection = document.createSelection(range)
|
||||
|
||||
// COMPAT: Ensure that the `isFocused` argument is set.
|
||||
selection = selection.setIsFocused(true)
|
||||
|
||||
// COMPAT: Preserve the marks, since we have no way of knowing what the DOM
|
||||
// selection's marks were. They will be cleared automatically by the
|
||||
// `select` command if the selection moves.
|
||||
selection = selection.set('marks', value.selection.marks)
|
||||
|
||||
return selection
|
||||
}
|
||||
|
||||
return {
|
||||
queries: {
|
||||
findDOMNode,
|
||||
findDOMPoint,
|
||||
findDOMRange,
|
||||
findEventRange,
|
||||
findNode,
|
||||
findPath,
|
||||
findPoint,
|
||||
findRange,
|
||||
findSelection,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* From a DOM selection's `node` and `offset`, normalize so that it always
|
||||
* refers to a text node.
|
||||
*
|
||||
* @param {Element} node
|
||||
* @param {Number} offset
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function normalizeNodeAndOffset(node, offset) {
|
||||
// If it's an element node, its offset refers to the index of its children
|
||||
// including comment nodes, so try to find the right text child node.
|
||||
if (node.nodeType === 1 && node.childNodes.length) {
|
||||
const isLast = offset === node.childNodes.length
|
||||
const direction = isLast ? 'backward' : 'forward'
|
||||
const index = isLast ? offset - 1 : offset
|
||||
node = getEditableChild(node, index, direction)
|
||||
|
||||
// If the node has children, traverse until we have a leaf node. Leaf nodes
|
||||
// can be either text nodes, or other void DOM nodes.
|
||||
while (node.nodeType === 1 && node.childNodes.length) {
|
||||
const i = isLast ? node.childNodes.length - 1 : 0
|
||||
node = getEditableChild(node, i, direction)
|
||||
}
|
||||
|
||||
// Determine the new offset inside the text node.
|
||||
offset = isLast ? node.textContent.length : 0
|
||||
}
|
||||
|
||||
// Return the node and offset.
|
||||
return { node, offset }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nearest editable child at `index` in a `parent`, preferring
|
||||
* `direction`.
|
||||
*
|
||||
* @param {Element} parent
|
||||
* @param {Number} index
|
||||
* @param {String} direction ('forward' or 'backward')
|
||||
* @return {Element|Null}
|
||||
*/
|
||||
|
||||
function getEditableChild(parent, index, direction) {
|
||||
const { childNodes } = parent
|
||||
let child = childNodes[index]
|
||||
let i = index
|
||||
let triedForward = false
|
||||
let triedBackward = false
|
||||
|
||||
// While the child is a comment node, or an element node with no children,
|
||||
// keep iterating to find a sibling non-void, non-comment node.
|
||||
while (
|
||||
child.nodeType === 8 ||
|
||||
(child.nodeType === 1 && child.childNodes.length === 0) ||
|
||||
(child.nodeType === 1 && child.getAttribute('contenteditable') === 'false')
|
||||
) {
|
||||
if (triedForward && triedBackward) break
|
||||
|
||||
if (i >= childNodes.length) {
|
||||
triedForward = true
|
||||
i = index - 1
|
||||
direction = 'backward'
|
||||
continue
|
||||
}
|
||||
|
||||
if (i < 0) {
|
||||
triedBackward = true
|
||||
i = index + 1
|
||||
direction = 'forward'
|
||||
continue
|
||||
}
|
||||
|
||||
child = childNodes[i]
|
||||
if (direction === 'forward') i++
|
||||
if (direction === 'backward') i--
|
||||
}
|
||||
|
||||
return child || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default QueriesPlugin
|
59
packages/slate-react/src/plugins/react/rendering.js
Normal file
59
packages/slate-react/src/plugins/react/rendering.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* The default rendering behavior for the React plugin.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function Rendering() {
|
||||
return {
|
||||
decorateNode() {
|
||||
return []
|
||||
},
|
||||
|
||||
renderAnnotation({ attributes, children }) {
|
||||
return <span {...attributes}>{children}</span>
|
||||
},
|
||||
|
||||
renderBlock({ attributes, children }) {
|
||||
return (
|
||||
<div {...attributes} style={{ position: 'relative' }}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
renderDecoration({ attributes, children }) {
|
||||
return <span {...attributes}>{children}</span>
|
||||
},
|
||||
|
||||
renderDocument({ children }) {
|
||||
return children
|
||||
},
|
||||
|
||||
renderEditor({ children }) {
|
||||
return children
|
||||
},
|
||||
|
||||
renderInline({ attributes, children }) {
|
||||
return (
|
||||
<span {...attributes} style={{ position: 'relative' }}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
|
||||
renderMark({ attributes, children }) {
|
||||
return <span {...attributes}>{children}</span>
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default Rendering
|
@@ -1,49 +0,0 @@
|
||||
import { IS_ANDROID } from 'slate-dev-environment'
|
||||
|
||||
/**
|
||||
* Array of regular expression matchers and their API version
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const ANDROID_API_VERSIONS = [
|
||||
[/^9([.]0|)/, 28],
|
||||
[/^8[.]1/, 27],
|
||||
[/^8([.]0|)/, 26],
|
||||
[/^7[.]1/, 25],
|
||||
[/^7([.]0|)/, 24],
|
||||
[/^6([.]0|)/, 23],
|
||||
[/^5[.]1/, 22],
|
||||
[/^5([.]0|)/, 21],
|
||||
[/^4[.]4/, 20],
|
||||
]
|
||||
|
||||
/**
|
||||
* get the Android API version from the userAgent
|
||||
*
|
||||
* @return {Number} version
|
||||
*/
|
||||
|
||||
function getApiVersion() {
|
||||
if (!IS_ANDROID) return null
|
||||
const { userAgent } = window.navigator
|
||||
const matchData = userAgent.match(/Android\s([0-9\.]+)/)
|
||||
if (matchData == null) return null
|
||||
const versionString = matchData[1]
|
||||
|
||||
for (const tuple of ANDROID_API_VERSIONS) {
|
||||
const [regex, version] = tuple
|
||||
if (versionString.match(regex)) return version
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const API_VERSION = getApiVersion()
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* type {number}
|
||||
*/
|
||||
|
||||
export default API_VERSION
|
@@ -1,13 +1,15 @@
|
||||
import Base64 from 'slate-base64-serializer'
|
||||
import Plain from 'slate-plain-serializer'
|
||||
import TRANSFER_TYPES from '../constants/transfer-types'
|
||||
import findDOMNode from './find-dom-node'
|
||||
import getWindow from 'get-window'
|
||||
import invariant from 'tiny-invariant'
|
||||
import removeAllRanges from './remove-all-ranges'
|
||||
import { IS_IE } from 'slate-dev-environment'
|
||||
import { Value } from 'slate'
|
||||
import { ZERO_WIDTH_SELECTOR, ZERO_WIDTH_ATTRIBUTE } from './find-point'
|
||||
|
||||
import TRANSFER_TYPES from '../constants/transfer-types'
|
||||
import removeAllRanges from './remove-all-ranges'
|
||||
import findDOMNode from './find-dom-node'
|
||||
import DATA_ATTRS from '../constants/data-attributes'
|
||||
import SELECTORS from '../constants/selectors'
|
||||
|
||||
const { FRAGMENT, HTML, TEXT } = TRANSFER_TYPES
|
||||
|
||||
@@ -29,8 +31,8 @@ function cloneFragment(event, editor, callback = () => undefined) {
|
||||
const { value } = editor
|
||||
const { document, fragment, selection } = value
|
||||
const { start, end } = selection
|
||||
const startVoid = document.getClosestVoid(start.key, editor)
|
||||
const endVoid = document.getClosestVoid(end.key, editor)
|
||||
const startVoid = document.getClosestVoid(start.path, editor)
|
||||
const endVoid = document.getClosestVoid(end.path, editor)
|
||||
|
||||
// If the selection is collapsed, and it isn't inside a void node, abort.
|
||||
if (native.isCollapsed && !startVoid) return
|
||||
@@ -69,10 +71,12 @@ function cloneFragment(event, editor, callback = () => undefined) {
|
||||
|
||||
// Remove any zero-width space spans from the cloned DOM so that they don't
|
||||
// show up elsewhere when pasted.
|
||||
;[].slice.call(contents.querySelectorAll(ZERO_WIDTH_SELECTOR)).forEach(zw => {
|
||||
const isNewline = zw.getAttribute(ZERO_WIDTH_ATTRIBUTE) === 'n'
|
||||
zw.textContent = isNewline ? '\n' : ''
|
||||
})
|
||||
;[].slice
|
||||
.call(contents.querySelectorAll(SELECTORS.ZERO_WIDTH))
|
||||
.forEach(zw => {
|
||||
const isNewline = zw.getAttribute(DATA_ATTRS.ZERO_WIDTH) === 'n'
|
||||
zw.textContent = isNewline ? '\n' : ''
|
||||
})
|
||||
|
||||
// Set a `data-slate-fragment` attribute on a non-empty node, so it shows up
|
||||
// in the HTML, and can be used for intra-Slate pasting. If it's a text
|
||||
@@ -89,7 +93,7 @@ function cloneFragment(event, editor, callback = () => undefined) {
|
||||
attach = span
|
||||
}
|
||||
|
||||
attach.setAttribute('data-slate-fragment', encoded)
|
||||
attach.setAttribute(DATA_ATTRS.FRAGMENT, encoded)
|
||||
|
||||
// Creates value from only the selected blocks
|
||||
// Then gets plaintext for clipboard with proper linebreaks for BLOCK elements
|
||||
@@ -120,7 +124,7 @@ function cloneFragment(event, editor, callback = () => undefined) {
|
||||
// COMPAT: For browser that don't support the Clipboard API's setData method,
|
||||
// we must rely on the browser to natively copy what's selected.
|
||||
// So we add the div (containing our content) to the DOM, and select it.
|
||||
const editorEl = event.target.closest('[data-slate-editor]')
|
||||
const editorEl = event.target.closest(SELECTORS.EDITOR)
|
||||
div.setAttribute('contenteditable', true)
|
||||
div.style.position = 'absolute'
|
||||
div.style.left = '-9999px'
|
||||
|
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Find the deepest descendant of a DOM `element`.
|
||||
*
|
||||
* @param {Element} node
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
function findDeepestNode(element) {
|
||||
return element.firstChild ? findDeepestNode(element.firstChild) : element
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default findDeepestNode
|
@@ -1,4 +1,7 @@
|
||||
import { Node } from 'slate'
|
||||
import warning from 'tiny-warning'
|
||||
|
||||
import DATA_ATTRS from '../constants/data-attributes'
|
||||
|
||||
/**
|
||||
* Find the DOM node for a `key`.
|
||||
@@ -9,11 +12,16 @@ import { Node } from 'slate'
|
||||
*/
|
||||
|
||||
function findDOMNode(key, win = window) {
|
||||
warning(
|
||||
false,
|
||||
'As of slate-react@0.22 the `findDOMNode(key)` helper is deprecated in favor of `editor.findDOMNode(path)`.'
|
||||
)
|
||||
|
||||
if (Node.isNode(key)) {
|
||||
key = key.key
|
||||
}
|
||||
|
||||
const el = win.document.querySelector(`[data-key="${key}"]`)
|
||||
const el = win.document.querySelector(`[${DATA_ATTRS.KEY}="${key}"]`)
|
||||
|
||||
if (!el) {
|
||||
throw new Error(
|
||||
|
@@ -1,4 +1,8 @@
|
||||
import findDOMNode from './find-dom-node'
|
||||
import warning from 'tiny-warning'
|
||||
|
||||
import DATA_ATTRS from '../constants/data-attributes'
|
||||
import SELECTORS from '../constants/selectors'
|
||||
|
||||
/**
|
||||
* Find a native DOM selection point from a Slate `point`.
|
||||
@@ -9,6 +13,11 @@ import findDOMNode from './find-dom-node'
|
||||
*/
|
||||
|
||||
function findDOMPoint(point, win = window) {
|
||||
warning(
|
||||
false,
|
||||
'As of slate-react@0.22 the `findDOMPoint(point)` helper is deprecated in favor of `editor.findDOMPoint(point)`.'
|
||||
)
|
||||
|
||||
const el = findDOMNode(point.key, win)
|
||||
let start = 0
|
||||
|
||||
@@ -16,7 +25,7 @@ function findDOMPoint(point, win = window) {
|
||||
// direct text and zero-width spans. (We have to filter out any other siblings
|
||||
// that may have been rendered alongside them.)
|
||||
const texts = Array.from(
|
||||
el.querySelectorAll('[data-slate-content], [data-slate-zero-width]')
|
||||
el.querySelectorAll(`${SELECTORS.STRING}, ${SELECTORS.ZERO_WIDTH}`)
|
||||
)
|
||||
|
||||
for (const text of texts) {
|
||||
@@ -24,8 +33,8 @@ function findDOMPoint(point, win = window) {
|
||||
const domLength = node.textContent.length
|
||||
let slateLength = domLength
|
||||
|
||||
if (text.hasAttribute('data-slate-length')) {
|
||||
slateLength = parseInt(text.getAttribute('data-slate-length'), 10)
|
||||
if (text.hasAttribute(DATA_ATTRS.LENGTH)) {
|
||||
slateLength = parseInt(text.getAttribute(DATA_ATTRS.LENGTH), 10)
|
||||
}
|
||||
|
||||
const end = start + slateLength
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import findDOMPoint from './find-dom-point'
|
||||
import warning from 'tiny-warning'
|
||||
|
||||
/**
|
||||
* Find a native DOM range Slate `range`.
|
||||
@@ -9,6 +10,11 @@ import findDOMPoint from './find-dom-point'
|
||||
*/
|
||||
|
||||
function findDOMRange(range, win = window) {
|
||||
warning(
|
||||
false,
|
||||
'As of slate-react@0.22 the `findDOMRange(range)` helper is deprecated in favor of `editor.findDOMRange(range)`.'
|
||||
)
|
||||
|
||||
const { anchor, focus, isBackward, isCollapsed } = range
|
||||
const domAnchor = findDOMPoint(anchor, win)
|
||||
const domFocus = isCollapsed ? domAnchor : findDOMPoint(focus, win)
|
||||
|
@@ -1,6 +1,10 @@
|
||||
import invariant from 'tiny-invariant'
|
||||
import warning from 'tiny-warning'
|
||||
import { Value } from 'slate'
|
||||
|
||||
import DATA_ATTRS from '../constants/data-attributes'
|
||||
import SELECTORS from '../constants/selectors'
|
||||
|
||||
/**
|
||||
* Find a Slate node from a DOM `element`.
|
||||
*
|
||||
@@ -10,15 +14,20 @@ import { Value } from 'slate'
|
||||
*/
|
||||
|
||||
function findNode(element, editor) {
|
||||
warning(
|
||||
false,
|
||||
'As of slate-react@0.22 the `findNode(element)` helper is deprecated in favor of `editor.findNode(element)`.'
|
||||
)
|
||||
|
||||
invariant(
|
||||
!Value.isValue(editor),
|
||||
'As of Slate 0.42.0, the `findNode` utility takes an `editor` instead of a `value`.'
|
||||
)
|
||||
|
||||
const closest = element.closest('[data-key]')
|
||||
const closest = element.closest(SELECTORS.KEY)
|
||||
if (!closest) return null
|
||||
|
||||
const key = closest.getAttribute('data-key')
|
||||
const key = closest.getAttribute(DATA_ATTRS.KEY)
|
||||
if (!key) return null
|
||||
|
||||
const { value } = editor
|
||||
|
36
packages/slate-react/src/utils/find-path.js
Normal file
36
packages/slate-react/src/utils/find-path.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import findNode from './find-node'
|
||||
import warning from 'tiny-warning'
|
||||
|
||||
/**
|
||||
* Find a Slate path from a DOM `element`.
|
||||
*
|
||||
* @param {Element} element
|
||||
* @param {Editor} editor
|
||||
* @return {List|Null}
|
||||
*/
|
||||
|
||||
function findPath(element, editor) {
|
||||
warning(
|
||||
false,
|
||||
'As of slate-react@0.22 the `findPath(element)` helper is deprecated in favor of `editor.findPath(element)`.'
|
||||
)
|
||||
|
||||
const node = findNode(element, editor)
|
||||
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { value } = editor
|
||||
const { document } = value
|
||||
const path = document.getPath(node)
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default findPath
|
@@ -1,21 +1,11 @@
|
||||
import getWindow from 'get-window'
|
||||
import invariant from 'tiny-invariant'
|
||||
import warning from 'tiny-warning'
|
||||
import { Value } from 'slate'
|
||||
|
||||
import OffsetKey from './offset-key'
|
||||
|
||||
/**
|
||||
* Constants.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
|
||||
export const ZERO_WIDTH_ATTRIBUTE = 'data-slate-zero-width'
|
||||
export const ZERO_WIDTH_SELECTOR = `[${ZERO_WIDTH_ATTRIBUTE}]`
|
||||
const OFFSET_KEY_ATTRIBUTE = 'data-offset-key'
|
||||
const RANGE_SELECTOR = `[${OFFSET_KEY_ATTRIBUTE}]`
|
||||
const TEXT_SELECTOR = `[data-key]`
|
||||
const VOID_SELECTOR = '[data-slate-void]'
|
||||
import DATA_ATTRS from '../constants/data-attributes'
|
||||
import SELECTORS from '../constants/selectors'
|
||||
|
||||
/**
|
||||
* Find a Slate point from a DOM selection's `nativeNode` and `nativeOffset`.
|
||||
@@ -27,6 +17,11 @@ const VOID_SELECTOR = '[data-slate-void]'
|
||||
*/
|
||||
|
||||
function findPoint(nativeNode, nativeOffset, editor) {
|
||||
warning(
|
||||
false,
|
||||
'As of slate-react@0.22 the `findPoint(node, offset)` helper is deprecated in favor of `editor.findPoint(node, offset)`.'
|
||||
)
|
||||
|
||||
invariant(
|
||||
!Value.isValue(editor),
|
||||
'As of Slate 0.42.0, the `findPoint` utility takes an `editor` instead of a `value`.'
|
||||
@@ -39,7 +34,7 @@ function findPoint(nativeNode, nativeOffset, editor) {
|
||||
|
||||
const window = getWindow(nativeNode)
|
||||
const { parentNode } = nearestNode
|
||||
let rangeNode = parentNode.closest(RANGE_SELECTOR)
|
||||
let rangeNode = parentNode.closest(SELECTORS.LEAF)
|
||||
let offset
|
||||
let node
|
||||
|
||||
@@ -47,7 +42,7 @@ function findPoint(nativeNode, nativeOffset, editor) {
|
||||
// determine what the offset relative to the text node is.
|
||||
if (rangeNode) {
|
||||
const range = window.document.createRange()
|
||||
const textNode = rangeNode.closest(TEXT_SELECTOR)
|
||||
const textNode = rangeNode.closest(SELECTORS.TEXT)
|
||||
range.setStart(textNode, 0)
|
||||
range.setEnd(nearestNode, nearestOffset)
|
||||
node = textNode
|
||||
@@ -60,9 +55,9 @@ function findPoint(nativeNode, nativeOffset, editor) {
|
||||
} else {
|
||||
// For void nodes, the element with the offset key will be a cousin, not an
|
||||
// ancestor, so find it by going down from the nearest void parent.
|
||||
const voidNode = parentNode.closest(VOID_SELECTOR)
|
||||
const voidNode = parentNode.closest(SELECTORS.VOID)
|
||||
if (!voidNode) return null
|
||||
rangeNode = voidNode.querySelector(RANGE_SELECTOR)
|
||||
rangeNode = voidNode.querySelector(SELECTORS.LEAF)
|
||||
if (!rangeNode) return null
|
||||
node = rangeNode
|
||||
offset = node.textContent.length
|
||||
@@ -74,13 +69,13 @@ function findPoint(nativeNode, nativeOffset, editor) {
|
||||
// from the offset to account for the zero-width space character.
|
||||
if (
|
||||
offset === node.textContent.length &&
|
||||
parentNode.hasAttribute(ZERO_WIDTH_ATTRIBUTE)
|
||||
parentNode.hasAttribute(DATA_ATTRS.ZERO_WIDTH)
|
||||
) {
|
||||
offset--
|
||||
}
|
||||
|
||||
// Get the string value of the offset key attribute.
|
||||
const offsetKey = rangeNode.getAttribute(OFFSET_KEY_ATTRIBUTE)
|
||||
const offsetKey = rangeNode.getAttribute(DATA_ATTRS.OFFSET_KEY)
|
||||
if (!offsetKey) return null
|
||||
|
||||
const { key } = OffsetKey.parse(offsetKey)
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import getWindow from 'get-window'
|
||||
import invariant from 'tiny-invariant'
|
||||
import warning from 'tiny-warning'
|
||||
import { Value } from 'slate'
|
||||
|
||||
import findPoint from './find-point'
|
||||
@@ -13,6 +14,11 @@ import findPoint from './find-point'
|
||||
*/
|
||||
|
||||
function findRange(native, editor) {
|
||||
warning(
|
||||
false,
|
||||
'As of slate-react@0.22 the `findRange(selection)` helper is deprecated in favor of `editor.findRange(selection)`.'
|
||||
)
|
||||
|
||||
invariant(
|
||||
!Value.isValue(editor),
|
||||
'As of Slate 0.42.0, the `findNode` utility takes an `editor` instead of a `value`.'
|
||||
|
@@ -1,132 +0,0 @@
|
||||
import { Set } from 'immutable'
|
||||
|
||||
/**
|
||||
* Split the decorations in lists of relevant decorations for each child.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @param {List} decorations
|
||||
* @return {Array<List<Decoration>>}
|
||||
*/
|
||||
|
||||
function getChildrenDecorations(node, decorations) {
|
||||
const activeDecorations = Set().asMutable()
|
||||
const childrenDecorations = []
|
||||
|
||||
orderChildDecorations(node, decorations).forEach(item => {
|
||||
if (item.isRangeStart) {
|
||||
// Item is a decoration start
|
||||
activeDecorations.add(item.decoration)
|
||||
} else if (item.isRangeEnd) {
|
||||
// item is a decoration end
|
||||
activeDecorations.remove(item.decoration)
|
||||
} else {
|
||||
// Item is a child node
|
||||
childrenDecorations.push(activeDecorations.toList())
|
||||
}
|
||||
})
|
||||
|
||||
return childrenDecorations
|
||||
}
|
||||
|
||||
/**
|
||||
* Orders the children of provided node and its decoration endpoints (start, end)
|
||||
* so that decorations can be passed only to relevant children (see use in Node.render())
|
||||
*
|
||||
* @param {Node} node
|
||||
* @param {List} decorations
|
||||
* @return {Array<Item>}
|
||||
*
|
||||
* where type Item =
|
||||
* {
|
||||
* child: Node,
|
||||
* // Index of the child in its parent
|
||||
* index: number
|
||||
* }
|
||||
* or {
|
||||
* // True if this represents the start of the given decoration
|
||||
* isRangeStart: boolean,
|
||||
* // True if this represents the end of the given decoration
|
||||
* isRangeEnd: boolean,
|
||||
* decoration: Range
|
||||
* }
|
||||
*/
|
||||
|
||||
function orderChildDecorations(node, decorations) {
|
||||
if (decorations.isEmpty()) {
|
||||
return node.nodes.toArray().map((child, index) => ({
|
||||
child,
|
||||
index,
|
||||
}))
|
||||
}
|
||||
|
||||
// Map each key to its global order
|
||||
const keyOrders = { [node.key]: 0 }
|
||||
let globalOrder = 1
|
||||
|
||||
node.forEachDescendant(child => {
|
||||
keyOrders[child.key] = globalOrder
|
||||
globalOrder = globalOrder + 1
|
||||
})
|
||||
|
||||
const childNodes = node.nodes.toArray()
|
||||
|
||||
const endPoints = childNodes.map((child, index) => ({
|
||||
child,
|
||||
index,
|
||||
order: keyOrders[child.key],
|
||||
}))
|
||||
|
||||
decorations.forEach(decoration => {
|
||||
// Range start.
|
||||
// A rangeStart should be before the child containing its startKey, in order
|
||||
// to consider it active before going down the child.
|
||||
const startKeyOrder = keyOrders[decoration.start.key]
|
||||
const containingChildOrder =
|
||||
startKeyOrder === undefined
|
||||
? 0
|
||||
: getContainingChildOrder(childNodes, keyOrders, startKeyOrder)
|
||||
|
||||
endPoints.push({
|
||||
isRangeStart: true,
|
||||
order: containingChildOrder - 0.5,
|
||||
decoration,
|
||||
})
|
||||
|
||||
// Range end.
|
||||
const endKeyOrder = (keyOrders[decoration.end.key] || globalOrder) + 0.5
|
||||
|
||||
endPoints.push({
|
||||
isRangeEnd: true,
|
||||
order: endKeyOrder,
|
||||
decoration,
|
||||
})
|
||||
})
|
||||
|
||||
return endPoints.sort((a, b) => (a.order > b.order ? 1 : -1))
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the key order of the child right before the given order.
|
||||
*/
|
||||
|
||||
function getContainingChildOrder(children, keyOrders, order) {
|
||||
// Find the first child that is after the given key
|
||||
const nextChildIndex = children.findIndex(
|
||||
child => order < keyOrders[child.key]
|
||||
)
|
||||
|
||||
if (nextChildIndex <= 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const containingChild = children[nextChildIndex - 1]
|
||||
return keyOrders[containingChild.key]
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default getChildrenDecorations
|
@@ -1,8 +1,9 @@
|
||||
import getWindow from 'get-window'
|
||||
import invariant from 'tiny-invariant'
|
||||
import warning from 'tiny-warning'
|
||||
import { Value } from 'slate'
|
||||
|
||||
import findNode from './find-node'
|
||||
import findPath from './find-node'
|
||||
import findRange from './find-range'
|
||||
|
||||
/**
|
||||
@@ -14,6 +15,11 @@ import findRange from './find-range'
|
||||
*/
|
||||
|
||||
function getEventRange(event, editor) {
|
||||
warning(
|
||||
false,
|
||||
'As of slate-react@0.22 the `getEventRange(event, editor)` helper is deprecated in favor of `editor.findEventRange(event)`.'
|
||||
)
|
||||
|
||||
invariant(
|
||||
!Value.isValue(editor),
|
||||
'As of Slate 0.42.0, the `findNode` utility takes an `editor` instead of a `value`.'
|
||||
@@ -28,32 +34,32 @@ function getEventRange(event, editor) {
|
||||
|
||||
const { value } = editor
|
||||
const { document } = value
|
||||
const node = findNode(target, editor)
|
||||
if (!node) return null
|
||||
const path = findPath(event.target, editor)
|
||||
if (!path) return null
|
||||
|
||||
const node = document.getNode(path)
|
||||
|
||||
// If the drop target is inside a void node, move it into either the next or
|
||||
// previous node, depending on which side the `x` and `y` coordinates are
|
||||
// closest to.
|
||||
if (editor.query('isVoid', node)) {
|
||||
if (editor.isVoid(node)) {
|
||||
const rect = target.getBoundingClientRect()
|
||||
const isPrevious =
|
||||
node.object === 'inline'
|
||||
? x - rect.left < rect.left + rect.width - x
|
||||
: y - rect.top < rect.top + rect.height - y
|
||||
|
||||
const text = node.getFirstText()
|
||||
const range = document.createRange()
|
||||
const iterable = isPrevious ? 'previousTexts' : 'nextTexts'
|
||||
const move = isPrevious ? 'moveToEndOfNode' : 'moveToStartOfNode'
|
||||
const entry = document[iterable](path)
|
||||
|
||||
if (isPrevious) {
|
||||
const previousText = document.getPreviousText(text.key)
|
||||
|
||||
if (previousText) {
|
||||
return range.moveToEndOfNode(previousText)
|
||||
}
|
||||
if (entry) {
|
||||
const [n] = entry
|
||||
return range[move](n)
|
||||
}
|
||||
|
||||
const nextText = document.getNextText(text.key)
|
||||
return nextText ? range.moveToStartOfNode(nextText) : null
|
||||
return null
|
||||
}
|
||||
|
||||
// Else resolve a range from the caret position where the drop occured.
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import Base64 from 'slate-base64-serializer'
|
||||
import { IS_IE } from 'slate-dev-environment'
|
||||
|
||||
import TRANSFER_TYPES from '../constants/transfer-types'
|
||||
import DATA_ATTRS from '../constants/data-attributes'
|
||||
|
||||
/**
|
||||
* Transfer types.
|
||||
@@ -43,7 +45,7 @@ function getEventTransfer(event) {
|
||||
|
||||
// If there isn't a fragment, but there is HTML, check to see if the HTML is
|
||||
// actually an encoded fragment.
|
||||
if (!fragment && html && ~html.indexOf(' data-slate-fragment="')) {
|
||||
if (!fragment && html && ~html.indexOf(` ${DATA_ATTRS.FRAGMENT}="`)) {
|
||||
const matches = FRAGMENT_MATCHER.exec(html)
|
||||
const [full, encoded] = matches // eslint-disable-line no-unused-vars
|
||||
if (encoded) fragment = encoded
|
||||
|
@@ -1,44 +0,0 @@
|
||||
import { findDOMNode } from 'react-dom'
|
||||
|
||||
/**
|
||||
* Get clipboard HTML data by capturing the HTML inserted by the browser's
|
||||
* native paste action. To make this work, `preventDefault()` may not be
|
||||
* called on the `onPaste` event. As this method is asynchronous, a callback
|
||||
* is needed to return the HTML content. This solution was adapted from
|
||||
* http://stackoverflow.com/a/6804718.
|
||||
*
|
||||
* @param {Component} component
|
||||
* @param {Function} callback
|
||||
*/
|
||||
|
||||
function getHtmlFromNativePaste(component, callback) {
|
||||
// Create an off-screen clone of the element and give it focus.
|
||||
const el = findDOMNode(component)
|
||||
const clone = el.cloneNode()
|
||||
clone.setAttribute('class', '')
|
||||
clone.setAttribute('style', 'position: fixed; left: -9999px')
|
||||
el.parentNode.insertBefore(clone, el)
|
||||
clone.focus()
|
||||
|
||||
// Tick forward so the native paste behaviour occurs in cloned element and we
|
||||
// can get what was pasted from the DOM.
|
||||
setTimeout(() => {
|
||||
if (clone.childElementCount > 0) {
|
||||
// If the node contains any child nodes, that is the HTML content.
|
||||
const html = clone.innerHTML
|
||||
clone.parentNode.removeChild(clone)
|
||||
callback(html)
|
||||
} else {
|
||||
// Only plain text, no HTML.
|
||||
callback()
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default getHtmlFromNativePaste
|
@@ -1,6 +1,13 @@
|
||||
import warning from 'tiny-warning'
|
||||
|
||||
import findRange from './find-range'
|
||||
|
||||
export default function getSelectionFromDOM(window, editor, domSelection) {
|
||||
warning(
|
||||
false,
|
||||
'As of slate-react@0.22 the `getSelectionFromDOM(window, editor, domSelection)` helper is deprecated in favor of `editor.findSelection(domSelection)`.'
|
||||
)
|
||||
|
||||
const { value } = editor
|
||||
const { document } = value
|
||||
|
||||
@@ -18,12 +25,12 @@ export default function getSelectionFromDOM(window, editor, domSelection) {
|
||||
}
|
||||
|
||||
const { anchor, focus } = range
|
||||
const anchorText = document.getNode(anchor.key)
|
||||
const focusText = document.getNode(focus.key)
|
||||
const anchorInline = document.getClosestInline(anchor.key)
|
||||
const focusInline = document.getClosestInline(focus.key)
|
||||
const focusBlock = document.getClosestBlock(focus.key)
|
||||
const anchorBlock = document.getClosestBlock(anchor.key)
|
||||
const anchorText = document.getNode(anchor.path)
|
||||
const focusText = document.getNode(focus.path)
|
||||
const anchorInline = document.getClosestInline(anchor.path)
|
||||
const focusInline = document.getClosestInline(focus.path)
|
||||
const focusBlock = document.getClosestBlock(focus.path)
|
||||
const anchorBlock = document.getClosestBlock(anchor.path)
|
||||
|
||||
// COMPAT: If the anchor point is at the start of a non-void, and the
|
||||
// focus point is inside a void node with an offset that isn't `0`, set
|
||||
@@ -51,9 +58,13 @@ export default function getSelectionFromDOM(window, editor, domSelection) {
|
||||
!editor.isVoid(anchorInline) &&
|
||||
anchor.offset === anchorText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(anchor.key)
|
||||
const nextText = block.getNextText(anchor.key)
|
||||
if (nextText) range = range.moveAnchorTo(nextText.key, 0)
|
||||
const block = document.getClosestBlock(anchor.path)
|
||||
const [next] = block.texts({ path: anchor.path })
|
||||
|
||||
if (next) {
|
||||
const [, nextPath] = next
|
||||
range = range.moveAnchorTo(nextPath, 0)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -61,9 +72,13 @@ export default function getSelectionFromDOM(window, editor, domSelection) {
|
||||
!editor.isVoid(focusInline) &&
|
||||
focus.offset === focusText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(focus.key)
|
||||
const nextText = block.getNextText(focus.key)
|
||||
if (nextText) range = range.moveFocusTo(nextText.key, 0)
|
||||
const block = document.getClosestBlock(focus.path)
|
||||
const [next] = block.texts({ path: focus.path })
|
||||
|
||||
if (next) {
|
||||
const [, nextPath] = next
|
||||
range = range.moveFocusTo(nextPath, 0)
|
||||
}
|
||||
}
|
||||
|
||||
let selection = document.createSelection(range)
|
||||
|
@@ -1,21 +1,20 @@
|
||||
import { IS_IE } from 'slate-dev-environment'
|
||||
|
||||
/**
|
||||
* COMPAT: if we are in <= IE11 and the selection contains
|
||||
* tables, `removeAllRanges()` will throw
|
||||
* "unable to complete the operation due to error 800a025e"
|
||||
* Cross-browser remove all ranges from a `domSelection`.
|
||||
*
|
||||
* @param {Selection} selection document selection
|
||||
* @param {Selection} domSelection
|
||||
*/
|
||||
|
||||
function removeAllRanges(selection) {
|
||||
const doc = window.document
|
||||
|
||||
if (doc && doc.body.createTextRange) {
|
||||
// All IE but Edge
|
||||
const range = doc.body.createTextRange()
|
||||
function removeAllRanges(domSelection) {
|
||||
// COMPAT: In IE 11, if the selection contains nested tables, then
|
||||
// `removeAllRanges` will throw an error.
|
||||
if (IS_IE) {
|
||||
const range = window.document.body.createTextRange()
|
||||
range.collapse()
|
||||
range.select()
|
||||
} else {
|
||||
selection.removeAllRanges()
|
||||
domSelection.removeAllRanges()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,14 +0,0 @@
|
||||
import getSelectionFromDOM from './get-selection-from-dom'
|
||||
|
||||
/**
|
||||
* Looks at the DOM and generates the equivalent Slate Selection.
|
||||
*
|
||||
* @param {Window} window
|
||||
* @param {Editor} editor
|
||||
* @param {Selection} domSelection - The DOM's selection Object
|
||||
*/
|
||||
|
||||
export default function setSelectionFromDOM(window, editor, domSelection) {
|
||||
const selection = getSelectionFromDOM(window, editor, domSelection)
|
||||
editor.select(selection)
|
||||
}
|
@@ -21,8 +21,8 @@ export default function setTextFromDomNode(window, editor, domNode) {
|
||||
// Get the text node and leaf in question.
|
||||
const { value } = editor
|
||||
const { document, selection } = value
|
||||
const node = document.getDescendant(point.key)
|
||||
const block = document.getClosestBlock(node.key)
|
||||
const node = document.getDescendant(point.path)
|
||||
const block = document.getClosestBlock(point.path)
|
||||
const leaves = node.getLeaves()
|
||||
const lastText = block.getLastText()
|
||||
const lastLeaf = leaves.last()
|
||||
@@ -57,8 +57,8 @@ export default function setTextFromDomNode(window, editor, domNode) {
|
||||
// const delta = textContent.length - text.length
|
||||
// const corrected = selection.moveToEnd().moveForward(delta)
|
||||
let entire = selection
|
||||
.moveAnchorTo(point.key, start)
|
||||
.moveFocusTo(point.key, end)
|
||||
.moveAnchorTo(point.path, start)
|
||||
.moveFocusTo(point.path, end)
|
||||
|
||||
entire = document.resolveRange(entire)
|
||||
|
||||
|
@@ -1,12 +1,24 @@
|
||||
import { JSDOM } from 'jsdom' // eslint-disable-line import/no-extraneous-dependencies
|
||||
|
||||
const UNWANTED_ATTRS = ['data-key', 'data-offset-key']
|
||||
const UNWANTED_ATTRS = [
|
||||
'data-key',
|
||||
'data-offset-key',
|
||||
'data-slate-object',
|
||||
'data-slate-leaf',
|
||||
'data-slate-zero-width',
|
||||
'data-slate-editor',
|
||||
'style',
|
||||
'data-slate-void',
|
||||
'data-slate-spacer',
|
||||
'data-slate-length',
|
||||
]
|
||||
|
||||
const UNWANTED_TOP_LEVEL_ATTRS = [
|
||||
'autocorrect',
|
||||
'spellcheck',
|
||||
'style',
|
||||
'data-gramm',
|
||||
'role',
|
||||
]
|
||||
|
||||
/**
|
||||
|
@@ -21,7 +21,7 @@ describe('slate-react', () => {
|
||||
assert.equal(actual, expected)
|
||||
})
|
||||
|
||||
fixtures(__dirname, 'rendering/fixtures', ({ module }) => {
|
||||
fixtures.skip(__dirname, 'rendering/fixtures', ({ module }) => {
|
||||
const { value, output, props } = module
|
||||
const p = {
|
||||
value,
|
||||
|
@@ -3,25 +3,21 @@
|
||||
import React from 'react'
|
||||
import h from '../../helpers/h'
|
||||
|
||||
function Image(props) {
|
||||
return React.createElement('img', {
|
||||
className: props.isFocused ? 'focused' : '',
|
||||
src: props.node.data.get('src'),
|
||||
...props.attributes,
|
||||
})
|
||||
}
|
||||
|
||||
function renderNode(props, editor, next) {
|
||||
function renderBlock(props, editor, next) {
|
||||
switch (props.node.type) {
|
||||
case 'image':
|
||||
return Image(props)
|
||||
return React.createElement('img', {
|
||||
className: props.isFocused ? 'focused' : '',
|
||||
src: props.node.data.get('src'),
|
||||
...props.attributes,
|
||||
})
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
export const props = {
|
||||
renderNode,
|
||||
renderBlock,
|
||||
schema: {
|
||||
blocks: {
|
||||
image: {
|
||||
@@ -55,19 +51,19 @@ export const value = (
|
||||
)
|
||||
|
||||
export const output = `
|
||||
<div data-slate-editor="true" contenteditable="true" role="textbox">
|
||||
<div style="position:relative">
|
||||
<div contenteditable="true">
|
||||
<div>
|
||||
<span>
|
||||
<span data-slate-leaf="true">
|
||||
<span data-slate-zero-width="n" data-slate-length="0"><br /></span>
|
||||
<span>
|
||||
<span><br /></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div data-slate-void="true">
|
||||
<div data-slate-spacer="true" style="height:0;color:transparent;outline:none;position:absolute">
|
||||
<div>
|
||||
<div>
|
||||
<span>
|
||||
<span data-slate-leaf="true">
|
||||
<span data-slate-zero-width="z" data-slate-length="0"></span>
|
||||
<span>
|
||||
<span></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -77,16 +73,16 @@ export const output = `
|
||||
</div>
|
||||
<div style="position:relative">
|
||||
<span>
|
||||
<span data-slate-leaf="true">
|
||||
<span data-slate-zero-width="n" data-slate-length="0"><br /></span>
|
||||
<span>
|
||||
<span><br /></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div data-slate-void="true">
|
||||
<div data-slate-spacer="true" style="height:0;color:transparent;outline:none;position:absolute">
|
||||
<div>
|
||||
<div>
|
||||
<span>
|
||||
<span data-slate-leaf="true">
|
||||
<span data-slate-zero-width="z" data-slate-length="0"></span>
|
||||
<span>
|
||||
<span></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user