mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-15 11:44:05 +02:00
Merge branch 'master' of github.com:ianstormtaylor/slate
This commit is contained in:
@@ -53,6 +53,7 @@ class Content extends React.Component {
|
||||
static propTypes = {
|
||||
autoCorrect: Types.bool.isRequired,
|
||||
className: Types.string,
|
||||
contentKey: Types.number,
|
||||
editor: Types.object.isRequired,
|
||||
id: Types.string,
|
||||
readOnly: Types.bool.isRequired,
|
||||
@@ -74,6 +75,22 @@ class Content extends React.Component {
|
||||
tagName: 'div',
|
||||
}
|
||||
|
||||
/**
|
||||
* An error boundary. If there is a render error, we increment `errorKey`
|
||||
* which is part of the container `key` which forces a re-render from
|
||||
* scratch.
|
||||
*
|
||||
* @param {Error} error
|
||||
* @param {String} info
|
||||
*/
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
debug('componentDidCatch', { error, info })
|
||||
// The call to `setState` is required despite not setting a value.
|
||||
// Without this call, React will not try to recreate the component tree.
|
||||
this.setState({})
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary values.
|
||||
*
|
||||
@@ -486,6 +503,7 @@ class Content extends React.Component {
|
||||
|
||||
return (
|
||||
<Container
|
||||
key={this.props.contentKey}
|
||||
{...handlers}
|
||||
{...data}
|
||||
ref={this.ref}
|
||||
|
@@ -79,7 +79,7 @@ class Editor extends React.Component {
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
state = { value: this.props.defaultValue }
|
||||
state = { value: this.props.defaultValue, contentKey: 0 }
|
||||
|
||||
/**
|
||||
* Temporary values.
|
||||
@@ -151,6 +151,7 @@ class Editor extends React.Component {
|
||||
const { options, readOnly, value: valueFromProps } = this.props
|
||||
const { value: valueFromState } = this.state
|
||||
const value = valueFromProps || valueFromState
|
||||
const { contentKey } = this.state
|
||||
this.controller.setReadOnly(readOnly)
|
||||
this.controller.setValue(value, options)
|
||||
|
||||
@@ -170,6 +171,7 @@ class Editor extends React.Component {
|
||||
ref={this.tmp.contentRef}
|
||||
autoCorrect={autoCorrect}
|
||||
className={className}
|
||||
contentKey={contentKey}
|
||||
editor={this}
|
||||
id={id}
|
||||
onEvent={(handler, event) => this.run(handler, event)}
|
||||
|
@@ -201,10 +201,11 @@ Leaf.propTypes = {
|
||||
|
||||
const MemoizedLeaf = React.memo(Leaf, (prev, next) => {
|
||||
return (
|
||||
next.block === prev.block &&
|
||||
next.index === prev.index &&
|
||||
next.marks === prev.marks &&
|
||||
next.parent === prev.parent &&
|
||||
next.block === prev.block &&
|
||||
next.text === prev.text &&
|
||||
next.annotations.equals(prev.annotations) &&
|
||||
next.decorations.equals(prev.decorations)
|
||||
)
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import getSelectionFromDom from '../../utils/get-selection-from-dom'
|
||||
import ElementSnapshot from './element-snapshot'
|
||||
import SELECTORS from '../../constants/selectors'
|
||||
|
||||
@@ -51,7 +50,7 @@ export default class DomSnapshot {
|
||||
}
|
||||
|
||||
this.snapshot = new ElementSnapshot(elements)
|
||||
this.selection = getSelectionFromDom(window, editor, domSelection)
|
||||
this.selection = editor.findSelection(domSelection)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -5,7 +5,6 @@ import pick from 'lodash/pick'
|
||||
import { ANDROID_API_VERSION } from 'slate-dev-environment'
|
||||
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'
|
||||
@@ -133,10 +132,10 @@ function AndroidPlugin() {
|
||||
function reconcile(window, editor, { from }) {
|
||||
debug.reconcile({ from })
|
||||
const domSelection = window.getSelection()
|
||||
const selection = getSelectionFromDom(window, editor, domSelection)
|
||||
const selection = editor.findSelection(domSelection)
|
||||
|
||||
nodes.forEach(node => {
|
||||
setTextFromDomNode(window, editor, node)
|
||||
editor.reconcileDOMNode(node)
|
||||
})
|
||||
|
||||
editor.select(selection)
|
||||
|
111
packages/slate-react/src/plugins/debug/debug-batch-events.js
Normal file
111
packages/slate-react/src/plugins/debug/debug-batch-events.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import Debug from 'debug'
|
||||
import EVENT_HANDLERS from '../../constants/event-handlers'
|
||||
import stringifyEvent from './stringify-event'
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const INTERVAL = 2000
|
||||
|
||||
/**
|
||||
* Debug events function.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
const debug = Debug('slate:batch-events')
|
||||
|
||||
/**
|
||||
* A plugin that sends short easy to digest debug info about each event to
|
||||
* browser.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function DebugBatchEventsPlugin() {
|
||||
/**
|
||||
* When the batch started
|
||||
*
|
||||
* @type {Date}
|
||||
*/
|
||||
|
||||
let startDate = null
|
||||
|
||||
/**
|
||||
* The timeoutId used to cancel the timeout
|
||||
*
|
||||
* @type {Any}
|
||||
*/
|
||||
|
||||
let timeoutId = null
|
||||
|
||||
/**
|
||||
* An array of events not yet dumped with `debug`
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const events = []
|
||||
|
||||
/**
|
||||
* Send all events to debug
|
||||
*
|
||||
* Note: Formatted so it can easily be cut and pasted as text for analysis or
|
||||
* documentation.
|
||||
*/
|
||||
|
||||
function dumpEvents() {
|
||||
debug(`\n${events.join('\n')}`)
|
||||
events.length = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Push an event on to the Array of events for debugging in a batch
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
function pushEvent(event) {
|
||||
if (events.length === 0) {
|
||||
startDate = new Date()
|
||||
}
|
||||
|
||||
const s = stringifyEvent(event)
|
||||
const now = new Date()
|
||||
events.push(`- ${now - startDate} - ${s}`)
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(dumpEvents, INTERVAL)
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin Object
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
const plugin = {}
|
||||
|
||||
for (const eventName of EVENT_HANDLERS) {
|
||||
plugin[eventName] = function(event, editor, next) {
|
||||
pushEvent(event)
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the plugin.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default DebugBatchEventsPlugin
|
52
packages/slate-react/src/plugins/debug/debug-events.js
Normal file
52
packages/slate-react/src/plugins/debug/debug-events.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import Debug from 'debug'
|
||||
import EVENT_HANDLERS from '../../constants/event-handlers'
|
||||
import stringifyEvent from './stringify-event'
|
||||
|
||||
/**
|
||||
* Debug events function.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
const debug = Debug('slate:events')
|
||||
|
||||
/**
|
||||
* A plugin that sends short easy to digest debug info about each event to
|
||||
* browser.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function DebugEventsPlugin() {
|
||||
/**
|
||||
* Plugin Object
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
const plugin = {}
|
||||
|
||||
for (const eventName of EVENT_HANDLERS) {
|
||||
plugin[eventName] = function(event, editor, next) {
|
||||
const s = stringifyEvent(event)
|
||||
debug(s)
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the plugin.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default DebugEventsPlugin
|
@@ -1,64 +0,0 @@
|
||||
import Debug from 'debug'
|
||||
|
||||
/**
|
||||
* A plugin that adds the "before" browser-specific logic to the editor.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function DebugPlugin(namespace) {
|
||||
/**
|
||||
* Debug.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
const debug = Debug(namespace)
|
||||
|
||||
const events = [
|
||||
'onBeforeInput',
|
||||
'onBlur',
|
||||
'onClick',
|
||||
'onCompositionEnd',
|
||||
'onCompositionStart',
|
||||
'onCopy',
|
||||
'onCut',
|
||||
'onDragEnd',
|
||||
'onDragEnter',
|
||||
'onDragExit',
|
||||
'onDragLeave',
|
||||
'onDragOver',
|
||||
'onDragStart',
|
||||
'onDrop',
|
||||
'onFocus',
|
||||
'onInput',
|
||||
'onKeyDown',
|
||||
'onPaste',
|
||||
'onSelect',
|
||||
]
|
||||
|
||||
const plugin = {}
|
||||
|
||||
for (const eventName of events) {
|
||||
plugin[eventName] = function(event, editor, next) {
|
||||
debug(eventName, { event })
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the plugin.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default DebugPlugin
|
21
packages/slate-react/src/plugins/debug/stringify-event.js
Normal file
21
packages/slate-react/src/plugins/debug/stringify-event.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Takes a React Synthetic Event or a DOM Event and turns it into a String that
|
||||
* is easy to log. It's succinct and keeps info to a bare minimum.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
export default function stringifyEvent(event) {
|
||||
const e = event.nativeEvent || event
|
||||
|
||||
switch (e.type) {
|
||||
case 'keydown':
|
||||
return `${e.type} ${JSON.stringify(e.key)}`
|
||||
case 'input':
|
||||
case 'beforeinput':
|
||||
case 'textInput':
|
||||
return `${e.type}:${e.inputType} ${JSON.stringify(e.data)}`
|
||||
default:
|
||||
return e.type
|
||||
}
|
||||
}
|
@@ -8,7 +8,6 @@ import { IS_IOS, IS_IE, IS_EDGE } from 'slate-dev-environment'
|
||||
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.
|
||||
@@ -432,7 +431,7 @@ function AfterPlugin(options = {}) {
|
||||
}
|
||||
|
||||
const { anchorNode } = domSelection
|
||||
setTextFromDomNode(window, editor, anchorNode)
|
||||
editor.reconcileDOMNode(anchorNode)
|
||||
|
||||
next()
|
||||
}
|
||||
@@ -552,7 +551,7 @@ function AfterPlugin(options = {}) {
|
||||
|
||||
if (Hotkeys.isExtendBackward(event)) {
|
||||
const startText = document.getNode(start.path)
|
||||
const prevEntry = document.texts({
|
||||
const [prevEntry] = document.texts({
|
||||
path: start.path,
|
||||
direction: 'backward',
|
||||
})
|
||||
|
@@ -271,7 +271,7 @@ function BeforePlugin() {
|
||||
// default, and calling `preventDefault` hides the cursor.
|
||||
const node = editor.findNode(event.target)
|
||||
|
||||
if (editor.isVoid(node)) {
|
||||
if (!node || editor.isVoid(node)) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
|
72
packages/slate-react/src/plugins/react/commands.js
Normal file
72
packages/slate-react/src/plugins/react/commands.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* A set of commands for the React plugin.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function CommandsPlugin() {
|
||||
/**
|
||||
* Takes a `node`, find the matching `domNode` and uses it to set the text
|
||||
* in the `node`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
* @param {Node} node
|
||||
*/
|
||||
|
||||
function reconcileNode(editor, node) {
|
||||
const { value } = editor
|
||||
const { document, selection } = value
|
||||
const path = document.getPath(node.key)
|
||||
|
||||
const domElement = editor.findDOMNode(path)
|
||||
const block = document.getClosestBlock(path)
|
||||
|
||||
// Get text information
|
||||
const { text } = node
|
||||
let { textContent: domText } = domElement
|
||||
|
||||
const isLastNode = block.nodes.last() === node
|
||||
const lastChar = domText.charAt(domText.length - 1)
|
||||
|
||||
// COMPAT: If this is the last leaf, 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 (isLastNode && lastChar === '\n') {
|
||||
domText = domText.slice(0, -1)
|
||||
}
|
||||
|
||||
// If the text is no different, abort.
|
||||
if (text === domText) return
|
||||
|
||||
let entire = selection.moveAnchorTo(path, 0).moveFocusTo(path, text.length)
|
||||
|
||||
entire = document.resolveRange(entire)
|
||||
|
||||
// Change the current value to have the leaf's text replaced.
|
||||
editor.insertTextAtRange(entire, domText, node.marks)
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes text from the `domNode` and uses it to set the text in the matching
|
||||
* `node` in Slate.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
* @param {DOMNode} domNode
|
||||
*/
|
||||
|
||||
function reconcileDOMNode(editor, domNode) {
|
||||
const domElement = domNode.parentElement.closest('[data-key]')
|
||||
const node = editor.findNode(domElement)
|
||||
editor.reconcileNode(node)
|
||||
}
|
||||
|
||||
return {
|
||||
commands: {
|
||||
reconcileNode,
|
||||
reconcileDOMNode,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default CommandsPlugin
|
@@ -1,9 +1,14 @@
|
||||
import Debug from 'debug'
|
||||
import PlaceholderPlugin from 'slate-react-placeholder'
|
||||
|
||||
import EditorPropsPlugin from './editor-props'
|
||||
import RenderingPlugin from './rendering'
|
||||
import CommandsPlugin from './commands'
|
||||
import QueriesPlugin from './queries'
|
||||
import DOMPlugin from '../dom'
|
||||
import RestoreDOMPlugin from './restore-dom'
|
||||
import DebugEventsPlugin from '../debug/debug-events'
|
||||
import DebugBatchEventsPlugin from '../debug/debug-batch-events'
|
||||
|
||||
/**
|
||||
* A plugin that adds the React-specific rendering logic to the editor.
|
||||
@@ -14,13 +19,20 @@ import DOMPlugin from '../dom'
|
||||
|
||||
function ReactPlugin(options = {}) {
|
||||
const { placeholder = '', plugins = [] } = options
|
||||
const debugEventsPlugin = Debug.enabled('slate:events')
|
||||
? DebugEventsPlugin(options)
|
||||
: null
|
||||
const debugBatchEventsPlugin = Debug.enabled('slate:batch-events')
|
||||
? DebugBatchEventsPlugin(options)
|
||||
: null
|
||||
const renderingPlugin = RenderingPlugin(options)
|
||||
const commandsPlugin = CommandsPlugin(options)
|
||||
const queriesPlugin = QueriesPlugin(options)
|
||||
const editorPropsPlugin = EditorPropsPlugin(options)
|
||||
const domPlugin = DOMPlugin({
|
||||
plugins: [editorPropsPlugin, ...plugins],
|
||||
})
|
||||
|
||||
const restoreDomPlugin = RestoreDOMPlugin()
|
||||
const placeholderPlugin = PlaceholderPlugin({
|
||||
placeholder,
|
||||
when: (editor, node) =>
|
||||
@@ -30,7 +42,16 @@ function ReactPlugin(options = {}) {
|
||||
Array.from(node.texts()).length === 1,
|
||||
})
|
||||
|
||||
return [domPlugin, placeholderPlugin, renderingPlugin, queriesPlugin]
|
||||
return [
|
||||
debugEventsPlugin,
|
||||
debugBatchEventsPlugin,
|
||||
domPlugin,
|
||||
restoreDomPlugin,
|
||||
placeholderPlugin,
|
||||
renderingPlugin,
|
||||
commandsPlugin,
|
||||
queriesPlugin,
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -23,6 +23,10 @@ function QueriesPlugin() {
|
||||
path = PathUtils.create(path)
|
||||
const content = editor.tmp.contentRef.current
|
||||
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!path.size) {
|
||||
return content.ref.current || null
|
||||
}
|
||||
@@ -177,13 +181,13 @@ function QueriesPlugin() {
|
||||
: 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)
|
||||
const entry = document[isPrevious ? 'getPreviousText' : 'getNextText'](
|
||||
path
|
||||
)
|
||||
|
||||
if (entry) {
|
||||
const [n] = entry
|
||||
return range[move](n)
|
||||
return range[move](entry)
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -230,13 +234,24 @@ function QueriesPlugin() {
|
||||
|
||||
function findPath(editor, element) {
|
||||
const content = editor.tmp.contentRef.current
|
||||
let nodeElement = element
|
||||
|
||||
if (element === content.ref.current) {
|
||||
// If element does not have a key, it is likely a string or
|
||||
// mark, return the closest parent Node that can be looked up.
|
||||
if (!nodeElement.hasAttribute(DATA_ATTRS.KEY)) {
|
||||
nodeElement = nodeElement.closest(SELECTORS.KEY)
|
||||
}
|
||||
|
||||
if (!nodeElement || !nodeElement.getAttribute(DATA_ATTRS.KEY)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (nodeElement === content.ref.current) {
|
||||
return PathUtils.create([])
|
||||
}
|
||||
|
||||
const search = (instance, p) => {
|
||||
if (element === instance) {
|
||||
if (nodeElement === instance) {
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -244,7 +259,7 @@ function QueriesPlugin() {
|
||||
return null
|
||||
}
|
||||
|
||||
if (element === instance.ref.current) {
|
||||
if (nodeElement === instance.ref.current) {
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -483,11 +498,14 @@ function QueriesPlugin() {
|
||||
anchor.offset === anchorText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(anchor.path)
|
||||
const [next] = block.texts({ path: anchor.path })
|
||||
const depth = document.getDepth(block.key)
|
||||
const relativePath = PathUtils.drop(anchor.path, depth)
|
||||
const [next] = block.texts({ path: relativePath })
|
||||
|
||||
if (next) {
|
||||
const [, nextPath] = next
|
||||
range = range.moveAnchorTo(nextPath, 0)
|
||||
const absolutePath = anchor.path.slice(0, depth).concat(nextPath)
|
||||
range = range.moveAnchorTo(absolutePath, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,11 +515,14 @@ function QueriesPlugin() {
|
||||
focus.offset === focusText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(focus.path)
|
||||
const [next] = block.texts({ path: focus.path })
|
||||
const depth = document.getDepth(block.key)
|
||||
const relativePath = PathUtils.drop(focus.path, depth)
|
||||
const [next] = block.texts({ path: relativePath })
|
||||
|
||||
if (next) {
|
||||
const [, nextPath] = next
|
||||
range = range.moveFocusTo(nextPath, 0)
|
||||
const absolutePath = focus.path.slice(0, depth).concat(nextPath)
|
||||
range = range.moveFocusTo(absolutePath, 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
21
packages/slate-react/src/plugins/react/restore-dom.js
Normal file
21
packages/slate-react/src/plugins/react/restore-dom.js
Normal file
@@ -0,0 +1,21 @@
|
||||
function RestoreDOMPlugin() {
|
||||
/**
|
||||
* Makes sure that on the next Content `render` the DOM is restored.
|
||||
* This gets us around issues where the DOM is in a different state than
|
||||
* React's virtual DOM and would crash.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function restoreDOM(editor) {
|
||||
editor.setState({ contentKey: editor.state.contentKey + 1 })
|
||||
}
|
||||
|
||||
return {
|
||||
commands: {
|
||||
restoreDOM,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default RestoreDOMPlugin
|
@@ -1,4 +1,5 @@
|
||||
import warning from 'tiny-warning'
|
||||
import { PathUtils } from 'slate'
|
||||
|
||||
import findRange from './find-range'
|
||||
|
||||
@@ -59,11 +60,14 @@ export default function getSelectionFromDOM(window, editor, domSelection) {
|
||||
anchor.offset === anchorText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(anchor.path)
|
||||
const [next] = block.texts({ path: anchor.path })
|
||||
const depth = document.getDepth(block.key)
|
||||
const relativePath = PathUtils.drop(anchor.path, depth)
|
||||
const [next] = block.texts({ path: relativePath })
|
||||
|
||||
if (next) {
|
||||
const [, nextPath] = next
|
||||
range = range.moveAnchorTo(nextPath, 0)
|
||||
const absolutePath = anchor.path.slice(0, depth).concat(nextPath)
|
||||
range = range.moveAnchorTo(absolutePath, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,11 +77,14 @@ export default function getSelectionFromDOM(window, editor, domSelection) {
|
||||
focus.offset === focusText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(focus.path)
|
||||
const [next] = block.texts({ path: focus.path })
|
||||
const depth = document.getDepth(block.key)
|
||||
const relativePath = PathUtils.drop(focus.path, depth)
|
||||
const [next] = block.texts({ path: relativePath })
|
||||
|
||||
if (next) {
|
||||
const [, nextPath] = next
|
||||
range = range.moveFocusTo(nextPath, 0)
|
||||
const absolutePath = focus.path.slice(0, depth).concat(nextPath)
|
||||
range = range.moveFocusTo(absolutePath, 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,67 +0,0 @@
|
||||
import findPoint from './find-point'
|
||||
|
||||
/**
|
||||
* setTextFromDomNode lets us take a domNode and reconcile the text in the
|
||||
* editor's Document such that it reflects the text in the DOM. This is the
|
||||
* opposite of what the Editor usually does which takes the Editor's Document
|
||||
* and React modifies the DOM to match. The purpose of this method is for
|
||||
* composition changes where we don't know what changes the user made by
|
||||
* looking at events. Instead we wait until the DOM is in a safe state, we
|
||||
* read from it, and update the Editor's Document.
|
||||
*
|
||||
* @param {Window} window
|
||||
* @param {Editor} editor
|
||||
* @param {Node} domNode
|
||||
*/
|
||||
|
||||
export default function setTextFromDomNode(window, editor, domNode) {
|
||||
const point = findPoint(domNode, 0, editor)
|
||||
if (!point) return
|
||||
|
||||
// Get the text node and leaf in question.
|
||||
const { value } = editor
|
||||
const { document, selection } = value
|
||||
const node = document.getDescendant(point.path)
|
||||
const block = document.getClosestBlock(point.path)
|
||||
const leaves = node.getLeaves()
|
||||
const lastText = block.getLastText()
|
||||
const lastLeaf = leaves.last()
|
||||
let start = 0
|
||||
let end = 0
|
||||
|
||||
const leaf =
|
||||
leaves.find(r => {
|
||||
start = end
|
||||
end += r.text.length
|
||||
if (end > point.offset) return true
|
||||
}) || lastLeaf
|
||||
|
||||
// Get the text information.
|
||||
const { text } = leaf
|
||||
let { textContent } = domNode
|
||||
const isLastText = node === lastText
|
||||
const isLastLeaf = leaf === lastLeaf
|
||||
const lastChar = textContent.charAt(textContent.length - 1)
|
||||
|
||||
// COMPAT: If this is the last leaf, 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 && isLastLeaf && lastChar === '\n') {
|
||||
textContent = textContent.slice(0, -1)
|
||||
}
|
||||
|
||||
// If the text is no different, abort.
|
||||
if (textContent === text) return
|
||||
|
||||
// Determine what the selection should be after changing the text.
|
||||
// const delta = textContent.length - text.length
|
||||
// const corrected = selection.moveToEnd().moveForward(delta)
|
||||
let entire = selection
|
||||
.moveAnchorTo(point.path, start)
|
||||
.moveFocusTo(point.path, end)
|
||||
|
||||
entire = document.resolveRange(entire)
|
||||
|
||||
// Change the current value to have the leaf's text replaced.
|
||||
editor.insertTextAtRange(entire, textContent, leaf.marks)
|
||||
}
|
Reference in New Issue
Block a user