1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-31 02:49:56 +02:00

refactor decorations to use selections (#1221)

* refactor decorations to use selections

* update docs

* cleanup

* add Selection.createList

* fix tests

* fix for nested blocks

* fix lint

* actually merge

* revert small change

* add state.decorations, with search example
This commit is contained in:
Ian Storm Taylor
2017-10-13 12:04:22 -07:00
committed by GitHub
parent 65ab5681d9
commit e53cee3942
32 changed files with 881 additions and 468 deletions

View File

@@ -13,6 +13,7 @@
"keycode": "^2.1.2",
"prop-types": "^15.5.8",
"react-portal": "^3.1.0",
"react-immutable-proptypes": "^2.1.0",
"selection-is-backward": "^1.0.0",
"slate-base64-serializer": "^0.1.11",
"slate-dev-logger": "^0.1.12",

View File

@@ -12,13 +12,13 @@ import TRANSFER_TYPES from '../constants/transfer-types'
import Node from './node'
import extendSelection from '../utils/extend-selection'
import findClosestNode from '../utils/find-closest-node'
import getCaretPosition from '../utils/get-caret-position'
import findDropPoint from '../utils/find-drop-point'
import findNativePoint from '../utils/find-native-point'
import findPoint from '../utils/find-point'
import getHtmlFromNativePaste from '../utils/get-html-from-native-paste'
import getPoint from '../utils/get-point'
import getDropPoint from '../utils/get-drop-point'
import getTransferData from '../utils/get-transfer-data'
import setTransferData from '../utils/set-transfer-data'
import scrollToSelection from '../utils/scroll-to-selection'
import setTransferData from '../utils/set-transfer-data'
import { IS_FIREFOX, IS_MAC, IS_IE } from '../constants/environment'
/**
@@ -121,7 +121,7 @@ class Content extends React.Component {
*/
updateSelection = () => {
const { editor, state } = this.props
const { state } = this.props
const { selection } = state
const window = getWindow(this.element)
const native = window.getSelection()
@@ -144,10 +144,8 @@ class Content extends React.Component {
// Otherwise, figure out which DOM nodes should be selected...
const { anchorKey, anchorOffset, focusKey, focusOffset, isCollapsed } = selection
const anchor = getCaretPosition(anchorKey, anchorOffset, state, editor, this.element)
const focus = isCollapsed
? anchor
: getCaretPosition(focusKey, focusOffset, state, editor, this.element)
const anchor = findNativePoint(anchorKey, anchorOffset)
const focus = isCollapsed ? anchor : findNativePoint(focusKey, focusOffset)
// If they are already selected, do nothing.
if (
@@ -432,12 +430,11 @@ class Content extends React.Component {
if (this.props.readOnly) return
const { editor, state } = this.props
const { state } = this.props
const { nativeEvent } = event
const { dataTransfer } = nativeEvent
const data = getTransferData(dataTransfer)
const point = getDropPoint(event, state, editor)
const point = findDropPoint(event, state)
if (!point) return
// Add drop-specific information to the data.
@@ -484,26 +481,33 @@ class Content extends React.Component {
// Get the selection point.
const native = window.getSelection()
const { anchorNode, anchorOffset } = native
const point = getPoint(anchorNode, anchorOffset, state, editor)
const point = findPoint(anchorNode, anchorOffset, state)
if (!point) return
// Get the range in question.
const { key, index, start, end } = point
// Get the text node and range in question.
const { document, selection } = state
const schema = editor.getSchema()
const decorators = document.getDescendantDecorators(key, schema)
const node = document.getDescendant(key)
const block = document.getClosestBlock(node.key)
const ranges = node.getRanges(decorators)
const lastText = block.getLastText()
const node = document.getDescendant(point.key)
const ranges = node.getRanges()
let start = 0
let end = 0
const range = ranges.find((r) => {
end += r.text.length
if (end >= point.offset) return true
start = end
})
// Get the text information.
const { text } = range
let { textContent } = anchorNode
const block = document.getClosestBlock(node.key)
const lastText = block.getLastText()
const lastRange = ranges.last()
const lastChar = textContent.charAt(textContent.length - 1)
const isLastText = node == lastText
const isLastRange = index == ranges.size - 1
const isLastRange = range == lastRange
// If we're dealing with the last leaf, and the DOM text ends in a new line,
// COMPAT: If this is the last range, and the DOM text ends in a new line,
// we will have added another new line in <Leaf>'s render method to account
// for browsers collapsing a single trailing new lines, so remove it.
if (isLastText && isLastRange && lastChar == '\n') {
@@ -511,26 +515,20 @@ class Content extends React.Component {
}
// If the text is no different, abort.
const range = ranges.get(index)
const { text, marks } = range
if (textContent == text) return
// Determine what the selection should be after changing the text.
const delta = textContent.length - text.length
const after = selection.collapseToEnd().move(delta)
const corrected = selection.collapseToEnd().move(delta)
const entire = selection.moveAnchorTo(point.key, start).moveFocusTo(point.key, end)
// Change the current state to have the text replaced.
// Change the current state to have the range's text replaced.
editor.change((change) => {
change
.select({
anchorKey: key,
anchorOffset: start,
focusKey: key,
focusOffset: end
})
.select(entire)
.delete()
.insertText(textContent, marks)
.select(after)
.insertText(textContent, range.marks)
.select(corrected)
})
}
@@ -677,7 +675,7 @@ class Content extends React.Component {
if (!this.isInEditor(event.target)) return
const window = getWindow(event.target)
const { state, editor } = this.props
const { state } = this.props
const { document, selection } = state
const native = window.getSelection()
const data = {}
@@ -690,8 +688,8 @@ class Content extends React.Component {
// Otherwise, determine the Slate selection from the native one.
else {
const { anchorNode, anchorOffset, focusNode, focusOffset } = native
const anchor = getPoint(anchorNode, anchorOffset, state, editor)
const focus = getPoint(focusNode, focusOffset, state, editor)
const anchor = findPoint(anchorNode, anchorOffset, state)
const focus = findPoint(focusNode, focusOffset, state)
if (!anchor || !focus) return
// There are situations where a select event will fire with a new native
@@ -872,11 +870,14 @@ class Content extends React.Component {
renderNode = (child, isSelected) => {
const { editor, readOnly, schema, state } = this.props
const { document } = state
const { document, decorations } = state
let decs = document.getDecorations(schema)
if (decorations) decs = decorations.concat(decs)
return (
<Node
block={null}
editor={editor}
decorations={decs}
isSelected={isSelected}
key={child.key}
node={child}

View File

@@ -100,6 +100,37 @@ class Leaf extends React.Component {
)
}
/**
* Render all of the leaf's mark components.
*
* @param {Object} props
* @return {Element}
*/
renderMarks(props) {
const { marks, schema, node, offset, text, state, editor } = props
const children = this.renderText(props)
return marks.reduce((memo, mark) => {
const Component = mark.getComponent(schema)
if (!Component) return memo
return (
<Component
editor={editor}
mark={mark}
marks={marks}
node={node}
offset={offset}
schema={schema}
state={state}
text={text}
>
{memo}
</Component>
)
}, children)
}
/**
* Render the text content of the leaf, accounting for browsers.
*
@@ -136,37 +167,6 @@ class Leaf extends React.Component {
return text
}
/**
* Render all of the leaf's mark components.
*
* @param {Object} props
* @return {Element}
*/
renderMarks(props) {
const { marks, schema, node, offset, text, state, editor } = props
const children = this.renderText(props)
return marks.reduce((memo, mark) => {
const Component = mark.getComponent(schema)
if (!Component) return memo
return (
<Component
editor={editor}
mark={mark}
marks={marks}
node={node}
offset={offset}
schema={schema}
state={state}
text={text}
>
{memo}
</Component>
)
}, children)
}
}
/**

View File

@@ -1,6 +1,7 @@
import Base64 from 'slate-base64-serializer'
import Debug from 'debug'
import ImmutableTypes from 'react-immutable-proptypes'
import React from 'react'
import SlateTypes from 'slate-prop-types'
import logger from 'slate-dev-logger'
@@ -35,6 +36,7 @@ class Node extends React.Component {
static propTypes = {
block: SlateTypes.block,
decorations: ImmutableTypes.list.isRequired,
editor: Types.object.isRequired,
isSelected: Types.bool.isRequired,
node: SlateTypes.node.isRequired,
@@ -136,6 +138,9 @@ class Node extends React.Component {
// need to be rendered again.
if (n.isSelected || p.isSelected) return true
// If the decorations have changed, update.
if (!n.decorations.equals(p.decorations)) return true
// Otherwise, don't update.
return false
}
@@ -225,11 +230,13 @@ class Node extends React.Component {
*/
renderNode = (child, isSelected) => {
const { block, editor, node, readOnly, schema, state } = this.props
const { block, decorations, editor, node, readOnly, schema, state } = this.props
const Component = child.kind === 'text' ? Text : Node
const decs = decorations.concat(node.getDecorations(schema))
return (
<Component
block={node.kind == 'block' ? node : block}
decorations={decs}
editor={editor}
isSelected={isSelected}
key={child.key}

View File

@@ -1,5 +1,6 @@
import Debug from 'debug'
import ImmutableTypes from 'react-immutable-proptypes'
import React from 'react'
import SlateTypes from 'slate-prop-types'
import Types from 'prop-types'
@@ -24,6 +25,7 @@ class Text extends React.Component {
static propTypes = {
block: SlateTypes.block,
decorations: ImmutableTypes.list.isRequired,
editor: Types.object.isRequired,
node: SlateTypes.node.isRequired,
parent: SlateTypes.node.isRequired,
@@ -63,16 +65,6 @@ class Text extends React.Component {
// for simplicity we just let them through.
if (n.node != p.node) return true
// Re-render if the current decorations have changed, even if the content of
// the text node itself hasn't.
if (n.schema.hasDecorators) {
const nDecorators = n.state.document.getDescendantDecorators(n.node.key, n.schema)
const pDecorators = p.state.document.getDescendantDecorators(p.node.key, p.schema)
const nRanges = n.node.getRanges(nDecorators)
const pRanges = p.node.getRanges(pDecorators)
if (!nRanges.equals(pRanges)) return true
}
// If the node parent is a block node, and it was the last child of the
// block, re-render to cleanup extra `<br/>` or `\n`.
if (n.parent.kind == 'block') {
@@ -81,6 +73,9 @@ class Text extends React.Component {
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
}
@@ -95,10 +90,19 @@ class Text extends React.Component {
const { props } = this
this.debug('render', { props })
const { node, schema, state } = props
const { decorations, node, state } = props
const { document } = state
const decorators = schema.hasDecorators ? document.getDescendantDecorators(node.key, schema) : []
const ranges = node.getRanges(decorators)
const { key } = node
const decs = decorations.filter((d) => {
const { startKey, endKey } = d
if (startKey == key || endKey == key) return true
const startsBefore = document.areDescendantsSorted(startKey, key)
const endsAfter = document.areDescendantsSorted(key, endKey)
return startsBefore && endsAfter
})
const ranges = node.getRanges(decs)
let offset = 0
const leaves = ranges.map((range, i) => {
@@ -108,7 +112,7 @@ class Text extends React.Component {
})
return (
<span data-key={node.key}>
<span data-key={key}>
{leaves}
</span>
)

View File

@@ -8,8 +8,8 @@ import { Block, Inline, coreSchema } from 'slate'
import Content from '../components/content'
import Placeholder from '../components/placeholder'
import getPoint from '../utils/get-point'
import findDOMNode from '../utils/find-dom-node'
import findPoint from '../utils/find-point'
import { IS_CHROME, IS_MAC, IS_SAFARI } from '../constants/environment'
/**
@@ -64,10 +64,9 @@ function Plugin(options = {}) {
* @param {Event} e
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onBeforeInput(e, data, change, editor) {
function onBeforeInput(e, data, change) {
debug('onBeforeInput', { data })
e.preventDefault()
@@ -82,8 +81,8 @@ function Plugin(options = {}) {
// the selection has gotten out of sync, and adjust it if so. (03/18/2017)
const window = getWindow(e.target)
const native = window.getSelection()
const a = getPoint(native.anchorNode, native.anchorOffset, state, editor)
const f = getPoint(native.focusNode, native.focusOffset, state, editor)
const a = findPoint(native.anchorNode, native.anchorOffset, state)
const f = findPoint(native.focusNode, native.focusOffset, state)
const hasMismatch = a && f && (
anchorKey != a.key ||
anchorOffset != a.offset ||

View File

@@ -1,16 +1,22 @@
import { Node } from 'slate'
/**
* Find the DOM node for a `node`.
* Find the DOM node for a `key`.
*
* @param {Node} node
* @param {String|Node} key
* @return {Element}
*/
function findDOMNode(node) {
const el = window.document.querySelector(`[data-key="${node.key}"]`)
function findDOMNode(key) {
if (Node.isNode(key)) {
key = key.key
}
const el = window.document.querySelector(`[data-key="${key}"]`)
if (!el) {
throw new Error(`Unable to find a DOM node for "${node.key}". This is often because of forgetting to add \`props.attributes\` to a component returned from \`renderNode\`.`)
throw new Error(`Unable to find a DOM node for "${key}". This is often because of forgetting to add \`props.attributes\` to a component returned from \`renderNode\`.`)
}
return el

View File

@@ -2,18 +2,17 @@
import getWindow from 'get-window'
import findClosestNode from './find-closest-node'
import getPoint from './get-point'
import findPoint from './find-point'
/**
* Get the target point for a drop event.
* Find the target point for a drop `event`.
*
* @param {Event} event
* @param {State} state
* @param {Editor} editor
* @return {Object}
*/
function getDropPoint(event, state, editor) {
function findDropPoint(event, state) {
const { document } = state
const { nativeEvent, target } = event
const { x, y } = nativeEvent
@@ -48,7 +47,6 @@ function getDropPoint(event, state, editor) {
document.getNextSibling(nodeKey)
const key = text.key
const offset = previous ? text.characters.size : 0
return { key, offset }
}
@@ -71,12 +69,10 @@ function getDropPoint(event, state, editor) {
const text = block.getLastText()
const { key } = text
const offset = 0
return { key, offset }
}
const point = getPoint(n, o, state, editor)
const point = findPoint(n, o, state)
return point
}
@@ -86,4 +82,4 @@ function getDropPoint(event, state, editor) {
* @type {Function}
*/
export default getDropPoint
export default findDropPoint

View File

@@ -0,0 +1,45 @@
import getWindow from 'get-window'
import findDOMNode from './find-dom-node'
/**
* Find a native DOM selection point from a Slate `key` and `offset`.
*
* @param {Element} root
* @param {String} key
* @param {Number} offset
* @return {Object}
*/
function findNativePoint(key, offset) {
const el = findDOMNode(key)
if (!el) return null
const window = getWindow(el)
const iterator = window.document.createNodeIterator(el, NodeFilter.SHOW_TEXT)
let start = 0
let n
while (n = iterator.nextNode()) {
const { length } = n.textContent
const end = start + length
if (offset <= end) {
const o = offset - start
return { node: n, offset: o }
}
start = end
}
return null
}
/**
* Export.
*
* @type {Function}
*/
export default findNativePoint

View File

@@ -0,0 +1,84 @@
import getWindow from 'get-window'
import OffsetKey from './offset-key'
import normalizeNodeAndOffset from './normalize-node-and-offset'
import findClosestNode from './find-closest-node'
/**
* Constants.
*
* @type {String}
*/
const OFFSET_KEY_ATTRIBUTE = 'data-offset-key'
const RANGE_SELECTOR = `[${OFFSET_KEY_ATTRIBUTE}]`
const TEXT_SELECTOR = `[data-key]`
const VOID_SELECTOR = '[data-slate-void]'
/**
* Find a Slate point from a DOM selection's `nativeNode` and `nativeOffset`.
*
* @param {Element} nativeNode
* @param {Number} nativeOffset
* @param {State} state
* @return {Object}
*/
function findPoint(nativeNode, nativeOffset, state) {
const {
node: nearestNode,
offset: nearestOffset,
} = normalizeNodeAndOffset(nativeNode, nativeOffset)
const window = getWindow(nativeNode)
const { parentNode } = nearestNode
let rangeNode = findClosestNode(parentNode, RANGE_SELECTOR)
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 (rangeNode) {
const range = window.document.createRange()
const textNode = findClosestNode(rangeNode, TEXT_SELECTOR)
range.setStart(textNode, 0)
range.setEnd(nearestNode, nearestOffset)
node = textNode
offset = range.toString().length
}
// 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.
else {
const voidNode = findClosestNode(parentNode, VOID_SELECTOR)
if (!voidNode) return null
rangeNode = voidNode.querySelector(RANGE_SELECTOR)
node = rangeNode
offset = node.textContent.length
}
// Get the string value of the offset key attribute.
const offsetKey = rangeNode.getAttribute(OFFSET_KEY_ATTRIBUTE)
if (!offsetKey) return null
const { key } = OffsetKey.parse(offsetKey)
// 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)
if (!state.document.hasDescendant(key)) return null
return {
key,
offset,
}
}
/**
* Export.
*
* @type {Function}
*/
export default findPoint

View File

@@ -1,46 +0,0 @@
import findDeepestNode from './find-deepest-node'
/**
* Get caret position from selection point.
*
* @param {String} key
* @param {Number} offset
* @param {State} state
* @param {Editor} editor
* @param {Element} el
* @return {Object}
*/
function getCaretPosition(key, offset, state, editor, el) {
const { document } = state
const text = document.getDescendant(key)
const schema = editor.getSchema()
const decorators = document.getDescendantDecorators(key, schema)
const ranges = text.getRanges(decorators)
let a = 0
let index
let off
ranges.forEach((range, i) => {
const { length } = range.text
a += length
if (a < offset) return
index = i
off = offset - (a - length)
return false
})
const span = el.querySelector(`[data-offset-key="${key}-${index}"]`)
const node = findDeepestNode(span)
return { node, offset: off }
}
/**
* Export.
*
* @type {Function}
*/
export default getCaretPosition

View File

@@ -1,41 +0,0 @@
import OffsetKey from './offset-key'
/**
* Get a point from a native selection's DOM `element` and `offset`.
*
* @param {Element} element
* @param {Number} offset
* @param {State} state
* @param {Editor} editor
* @return {Object}
*/
function getPoint(element, offset, state, editor) {
const { document } = state
const schema = editor.getSchema()
// If we can't find an offset key, we can't get a point.
const offsetKey = OffsetKey.findKey(element, offset)
if (!offsetKey) return null
// COMPAT: If someone is clicking from one Slate editor into another, the
// select event fires two, once for the old editor's `element` first, and
// then afterwards for the correct `element`. (2017/03/03)
const { key } = offsetKey
const node = document.getDescendant(key)
if (!node) return null
const decorators = document.getDescendantDecorators(key, schema)
const ranges = node.getRanges(decorators)
const point = OffsetKey.findPoint(offsetKey, ranges)
return point
}
/**
* Export.
*
* @type {Function}
*/
export default getPoint

View File

@@ -1,7 +1,4 @@
import normalizeNodeAndOffset from './normalize-node-and-offset'
import findClosestNode from './find-closest-node'
/**
* Offset key parser regex.
*
@@ -10,117 +7,6 @@ import findClosestNode from './find-closest-node'
const PARSER = /^(\w+)(?:-(\d+))?$/
/**
* Offset key attribute name.
*
* @type {String}
*/
const ATTRIBUTE = 'data-offset-key'
/**
* Offset key attribute selector.
*
* @type {String}
*/
const SELECTOR = `[${ATTRIBUTE}]`
/**
* Void node selection.
*
* @type {String}
*/
const VOID_SELECTOR = '[data-slate-void]'
/**
* Find the start and end bounds from an `offsetKey` and `ranges`.
*
* @param {Number} index
* @param {List<Range>} ranges
* @return {Object}
*/
function findBounds(index, ranges) {
const range = ranges.get(index)
const start = ranges
.slice(0, index)
.reduce((memo, r) => {
return memo += r.text.length
}, 0)
return {
start,
end: start + range.text.length
}
}
/**
* From a DOM node, find the closest parent's offset key.
*
* @param {Element} rawNode
* @param {Number} rawOffset
* @return {Object}
*/
function findKey(rawNode, rawOffset) {
let { node, offset } = normalizeNodeAndOffset(rawNode, rawOffset)
const { parentNode } = node
// Find the closest parent with an offset key attribute.
let closest = findClosestNode(parentNode, SELECTOR)
// 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.
if (!closest) {
const closestVoid = findClosestNode(parentNode, VOID_SELECTOR)
if (!closestVoid) return null
closest = closestVoid.querySelector(SELECTOR)
offset = closest.textContent.length
}
// Get the string value of the offset key attribute.
const offsetKey = closest.getAttribute(ATTRIBUTE)
// If we still didn't find an offset key, abort.
if (!offsetKey) return null
// Return the parsed the offset key.
const parsed = parse(offsetKey)
return {
key: parsed.key,
index: parsed.index,
offset
}
}
/**
* Find the selection point from an `offsetKey` and `ranges`.
*
* @param {Object} offsetKey
* @param {List<Range>} ranges
* @return {Object}
*/
function findPoint(offsetKey, ranges) {
let { key, index, offset } = offsetKey
const { start, end } = findBounds(index, ranges)
// Don't let the offset be outside of the start and end bounds.
offset = start + offset
offset = Math.max(offset, start)
offset = Math.min(offset, end)
return {
key,
index,
start,
end,
offset
}
}
/**
* Parse an offset key `string`.
*
@@ -158,9 +44,6 @@ function stringify(object) {
*/
export default {
findBounds,
findKey,
findPoint,
parse,
stringify
}

View File

@@ -1,19 +1,19 @@
/** @jsx h */
import h from '../../helpers/h'
import { Mark } from 'slate'
export const schema = {
nodes: {
paragraph: {
decorate(text, block) {
let { characters } = text
let second = characters.get(1)
const mark = Mark.create({ type: 'bold' })
const marks = second.marks.add(mark)
second = second.merge({ marks })
characters = characters.set(1, second)
return characters
decorate(block) {
const text = block.getFirstText()
return [{
anchorKey: text.key,
anchorOffset: 1,
focusKey: text.key,
focusOffset: 2,
marks: [{ type: 'bold' }]
}]
}
}
},

View File

@@ -1,4 +1,6 @@
import State from '../models/state'
/**
* Changes.
*
@@ -8,20 +10,20 @@
const Changes = {}
/**
* Set `properties` on the top-level state's data.
* Set `properties` on the state.
*
* @param {Change} change
* @param {Object} properties
* @param {Object|State} properties
*/
Changes.setData = (change, properties) => {
Changes.setState = (change, properties) => {
properties = State.createProperties(properties)
const { state } = change
const { data } = state
change.applyOperation({
type: 'set_data',
type: 'set_state',
properties,
data,
state,
})
}

View File

@@ -165,19 +165,12 @@ class Node {
first = normalizeKey(first)
second = normalizeKey(second)
let sorted
const keys = this.getKeysAsArray()
const firstIndex = keys.indexOf(first)
const secondIndex = keys.indexOf(second)
if (firstIndex == -1 || secondIndex == -1) return null
this.forEachDescendant((n) => {
if (n.key === first) {
sorted = true
return false
} else if (n.key === second) {
sorted = false
return false
}
})
return sorted
return firstIndex < secondIndex
}
/**
@@ -609,8 +602,8 @@ class Node {
* @return {Array}
*/
getDecorators(schema) {
return schema.__getDecorators(this)
getDecorations(schema) {
return schema.__getDecorations(this)
}
/**
@@ -674,32 +667,6 @@ class Node {
return descendant
}
/**
* Get the decorators for a descendant by `key` given a `schema`.
*
* @param {String} key
* @param {Schema} schema
* @return {Array}
*/
getDescendantDecorators(key, schema) {
if (!schema.hasDecorators) {
return []
}
const descendant = this.assertDescendant(key)
let child = this.getFurthestAncestor(key)
let decorators = []
while (child != descendant) {
decorators = decorators.concat(child.getDecorators(schema))
child = child.getFurthestAncestor(key)
}
decorators = decorators.concat(descendant.getDecorators(schema))
return decorators
}
/**
* Get the first child text node.
*
@@ -958,18 +925,29 @@ class Node {
}
/**
* Return a set of all keys in the node.
* Return a set of all keys in the node as an array.
*
* @return {Set<String>}
* @return {Array<String>}
*/
getKeys() {
getKeysAsArray() {
const keys = []
this.forEachDescendant((desc) => {
keys.push(desc.key)
})
return keys
}
/**
* Return a set of all keys in the node.
*
* @return {Set<String>}
*/
getKeys() {
const keys = this.getKeysAsArray()
return new Set(keys)
}
@@ -2102,6 +2080,7 @@ memoize(Node.prototype, [
'getInlines',
'getInlinesAsArray',
'getKeys',
'getKeysAsArray',
'getLastText',
'getMarks',
'getOrderedMarks',
@@ -2135,11 +2114,10 @@ memoize(Node.prototype, [
'getClosestVoid',
'getCommonAncestor',
'getComponent',
'getDecorators',
'getDecorations',
'getDepth',
'getDescendant',
'getDescendantAtPath',
'getDescendantDecorators',
'getFragmentAtRange',
'getFurthestBlock',
'getFurthestInline',

View File

@@ -7,6 +7,7 @@ import typeOf from 'type-of'
import { Record } from 'immutable'
import MODEL_TYPES from '../constants/model-types'
import Selection from '../models/selection'
import isReactComponent from '../utils/is-react-component'
/**
@@ -126,24 +127,33 @@ class Schema extends Record(DEFAULTS) {
}
/**
* Return the decorators for an `object`.
* Return the decorations for an `object`.
*
* This method is private, because it should always be called on one of the
* often-changing immutable objects instead, since it will be memoized for
* much better performance.
*
* @param {Mixed} object
* @return {Array}
* @return {List<Selection>}
*/
__getDecorators(object) {
return this.rules
.filter(rule => rule.decorate && rule.match(object))
.map((rule) => {
return (text) => {
return rule.decorate(text, object)
}
__getDecorations(object) {
const array = []
this.rules.forEach((rule) => {
if (!rule.decorate) return
if (!rule.match(object)) return
const decorations = rule.decorate(object)
if (!decorations.length) return
decorations.forEach((dec) => {
array.push(dec)
})
})
const list = Selection.createList(array)
return list
}
/**

View File

@@ -1,9 +1,10 @@
import isPlainObject from 'is-plain-object'
import logger from 'slate-dev-logger'
import { Record } from 'immutable'
import { List, Record, Set } from 'immutable'
import MODEL_TYPES from '../constants/model-types'
import Mark from './mark'
/**
* Default properties.
@@ -48,6 +49,22 @@ class Selection extends Record(DEFAULTS) {
throw new Error(`\`Selection.create\` only accepts objects or selections, but you passed it: ${attrs}`)
}
/**
* Create a list of `Selections` from a `value`.
*
* @param {Array<Selection|Object>|List<Selection|Object>} value
* @return {List<Selection>}
*/
static createList(value = []) {
if (List.isList(value) || Array.isArray(value)) {
const list = new List(value.map(Selection.create))
return list
}
throw new Error(`\`Selection.createList\` only accepts arrays or lists, but you passed it: ${value}`)
}
/**
* Create a dictionary of settable selection properties from `attrs`.
*
@@ -108,7 +125,7 @@ class Selection extends Record(DEFAULTS) {
focusOffset,
isBackward,
isFocused,
marks,
marks: marks == null ? null : new Set(marks.map(Mark.fromJSON)),
})
return selection

View File

@@ -5,7 +5,7 @@ import { Record, Set, List, Map } from 'immutable'
import MODEL_TYPES from '../constants/model-types'
import SCHEMA from '../schemas/core'
import Change from './change'
import Data from './data'
import Document from './document'
import History from './history'
import Selection from './selection'
@@ -21,6 +21,7 @@ const DEFAULTS = {
selection: Selection.create(),
history: History.create(),
data: new Map(),
decorations: null,
}
/**
@@ -51,6 +52,31 @@ class State extends Record(DEFAULTS) {
throw new Error(`\`State.create\` only accepts objects or states, but you passed it: ${attrs}`)
}
/**
* Create a dictionary of settable state properties from `attrs`.
*
* @param {Object|State} attrs
* @return {Object}
*/
static createProperties(attrs = {}) {
if (State.isState(attrs)) {
return {
data: attrs.data,
decorations: attrs.decorations,
}
}
if (isPlainObject(attrs)) {
const props = {}
if ('data' in attrs) props.data = Data.create(attrs.data)
if ('decorations' in attrs) props.decorations = Selection.createList(attrs.decorations)
return props
}
throw new Error(`\`State.createProperties\` only accepts objects or states, but you passed it: ${attrs}`)
}
/**
* Create a `State` from a JSON `object`.
*
@@ -549,6 +575,7 @@ class State extends Record(DEFAULTS) {
*/
change(attrs = {}) {
const Change = require('./change').default
return new Change({ ...attrs, state: this })
}
@@ -572,11 +599,20 @@ class State extends Record(DEFAULTS) {
toJSON(options = {}) {
const object = {
kind: this.kind,
data: this.data.toJSON(),
document: this.document.toJSON(options),
kind: this.kind,
history: this.history.toJSON(),
selection: this.selection.toJSON(),
decorations: this.decorations ? this.decorations.toArray().map(d => d.toJSON()) : null,
history: this.history.toJSON(),
}
if (!options.preserveData) {
delete object.data
}
if (!options.preserveDecorations) {
delete object.decorations
}
if (!options.preserveHistory) {
@@ -587,10 +623,6 @@ class State extends Record(DEFAULTS) {
delete object.selection
}
if (!options.preserveStateData) {
delete object.data
}
if (options.preserveSelection && !options.preserveKeys) {
const { document, selection } = this
object.selection.anchorPath = selection.isSet ? document.getPath(selection.anchorKey) : null

View File

@@ -1,7 +1,7 @@
import isPlainObject from 'is-plain-object'
import logger from 'slate-dev-logger'
import { List, Record, OrderedSet, is } from 'immutable'
import { List, OrderedSet, Record, Set, is } from 'immutable'
import Character from './character'
import Mark from './mark'
@@ -193,11 +193,25 @@ class Text extends Record(DEFAULTS) {
*/
addMark(index, length, mark) {
const marks = new Set([mark])
return this.addMarks(index, length, marks)
}
/**
* Add a `set` of marks at `index` and `length`.
*
* @param {Number} index
* @param {Number} length
* @param {Set<Mark>} set
* @return {Text}
*/
addMarks(index, length, set) {
const characters = this.characters.map((char, i) => {
if (i < index) return char
if (i >= index + length) return char
let { marks } = char
marks = marks.add(mark)
marks = marks.union(set)
char = char.set('marks', marks)
return char
})
@@ -206,24 +220,29 @@ class Text extends Record(DEFAULTS) {
}
/**
* Derive a set of decorated characters with `decorators`.
* Derive a set of decorated characters with `decorations`.
*
* @param {Array} decorators
* @param {List<Decoration>} decorations
* @return {List<Character>}
*/
getDecorations(decorators) {
const node = this
let { characters } = node
getDecoratedCharacters(decorations) {
let node = this
const { key, characters } = node
// PERF: Exit early if there are no characters to be decorated.
if (characters.size == 0) return characters
for (let i = 0; i < decorators.length; i++) {
const decorator = decorators[i]
const decorateds = decorator(node)
characters = characters.merge(decorateds)
}
decorations.forEach((range) => {
const { startKey, endKey, startOffset, endOffset, marks } = range
const hasStart = startKey == key
const hasEnd = endKey == key
const index = hasStart ? startOffset : 0
const length = hasEnd ? endOffset - index : characters.size
node = node.addMarks(index, length, marks)
})
return characters
return node.characters
}
/**
@@ -233,8 +252,8 @@ class Text extends Record(DEFAULTS) {
* @return {Array}
*/
getDecorators(schema) {
return schema.__getDecorators(this)
getDecorations(schema) {
return schema.__getDecorations(this)
}
/**
@@ -291,12 +310,12 @@ class Text extends Record(DEFAULTS) {
/**
* Derive the ranges for a list of `characters`.
*
* @param {Array|Void} decorators (optional)
* @param {Array|Void} decorations (optional)
* @return {List<Range>}
*/
getRanges(decorators = []) {
const characters = this.getDecorations(decorators)
getRanges(decorations = []) {
const characters = this.getDecoratedCharacters(decorations)
let ranges = []
// PERF: cache previous values for faster lookup.
@@ -513,8 +532,8 @@ memoize(Text.prototype, [
})
memoize(Text.prototype, [
'getDecoratedCharacters',
'getDecorations',
'getDecorators',
'getMarksAtIndex',
'getRanges',
'validate'

View File

@@ -310,23 +310,6 @@ const APPLIERS = {
return state
},
/**
* Set `data` on `state`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
*/
set_data(state, operation) {
const { properties } = operation
let { data } = state
data = data.merge(properties)
state = state.set('data', data)
return state
},
/**
* Set `properties` on mark on text at `offset` and `length` in node by `path`.
*
@@ -359,15 +342,13 @@ const APPLIERS = {
let { document } = state
let node = document.assertPath(path)
// Warn when trying to overwite a node's children.
if (properties.nodes && properties.nodes != node.nodes) {
logger.warn('Updating a Node\'s `nodes` property via `setNode()` is not allowed. Use the appropriate insertion and removal operations instead. The opeartion in question was:', operation)
if ('nodes' in properties) {
logger.warn('Updating a Node\'s `nodes` property via `setNode()` is not allowed. Use the appropriate insertion and removal methods instead. The operation in question was:', operation)
delete properties.nodes
}
// Warn when trying to change a node's key.
if (properties.key && properties.key != node.key) {
logger.warn('Updating a Node\'s `key` property via `setNode()` is not allowed. There should be no reason to do this. The opeartion in question was:', operation)
if ('key' in properties) {
logger.warn('Updating a Node\'s `key` property via `setNode()` is not allowed. There should be no reason to do this. The operation in question was:', operation)
delete properties.key
}
@@ -413,6 +394,36 @@ const APPLIERS = {
return state
},
/**
* Set `properties` on `state`.
*
* @param {State} state
* @param {Object} operation
* @return {State}
*/
set_state(state, operation) {
const { properties } = operation
if ('document' in properties) {
logger.warn('Updating `state.document` property via `setState()` is not allowed. Use the appropriate document updating methods instead. The operation in question was:', operation)
delete properties.document
}
if ('selection' in properties) {
logger.warn('Updating `state.selection` property via `setState()` is not allowed. Use the appropriate selection updating methods instead. The operation in question was:', operation)
delete properties.selection
}
if ('history' in properties) {
logger.warn('Updating `state.history` property via `setState()` is not allowed. Use the appropriate history updating methods instead. The operation in question was:', operation)
delete properties.history
}
state = state.merge(properties)
return state
},
/**
* Split a node by `path` at `offset`.
*

View File

@@ -3,7 +3,7 @@
import h from '../../../helpers/h'
export default function (change) {
change.setData({ thing: 'value' })
change.setState({ data: { thing: 'value' }})
}
export const input = (

View File

@@ -42,5 +42,5 @@ export const output = {
}
export const options = {
preserveStateData: true,
preserveData: true,
}