mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-11 09:43:58 +02:00
Operations update decorations (#1778)
* initial simple decorations (mark-like), many tests added * allow decorators to be set by focus, anchor tags - add tests * handle one more edge case with decorations in hyperscript * apply prettier cleanup * apply linting rules * update changelog * ensure always normalize decoration ranges * reapply prettier after latest adjustments * all operations apply now update decorations with selection * ranges can now be 'atomic', will invalidate if contents change * lint, prettier cleanups * add atomic invalidation tests, update hyperscript usage * fix linter errors * minor cleanup * slight refactor for simplicity * remove a couple superfluous lines * update in response to review * drop unnecessarily committed add'l file * remove the need for explicit anchor, focus prop on decoration tags * update hyperscript use to match latest syntax in #1777 * atomic -> isAtomic
This commit is contained in:
committed by
Ian Storm Taylor
parent
63eae6b48b
commit
c500becf81
@@ -56,6 +56,7 @@ class SearchHighlighting extends React.Component {
|
|||||||
focusKey: key,
|
focusKey: key,
|
||||||
focusOffset: offset,
|
focusOffset: offset,
|
||||||
marks: [{ type: 'highlight' }],
|
marks: [{ type: 'highlight' }],
|
||||||
|
atomic: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -19,9 +19,12 @@ const FOCUS = {}
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class DecoratorPoint {
|
class DecoratorPoint {
|
||||||
constructor(key, marks) {
|
constructor({ key, data }, marks) {
|
||||||
this._key = key
|
this._key = key
|
||||||
this.marks = marks
|
this.marks = marks
|
||||||
|
this.attribs = data || {}
|
||||||
|
this.isAtomic = !!this.attribs.atomic
|
||||||
|
delete this.attribs.atomic
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
withPosition = offset => {
|
withPosition = offset => {
|
||||||
@@ -45,6 +48,8 @@ class DecoratorPoint {
|
|||||||
anchorOffset: this.offset,
|
anchorOffset: this.offset,
|
||||||
focusOffset: focus.offset,
|
focusOffset: focus.offset,
|
||||||
marks: this.marks,
|
marks: this.marks,
|
||||||
|
isAtomic: this.isAtomic,
|
||||||
|
...this.attribs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +102,7 @@ const CREATORS = {
|
|||||||
|
|
||||||
decoration(tagName, attributes, children) {
|
decoration(tagName, attributes, children) {
|
||||||
if (attributes.key) {
|
if (attributes.key) {
|
||||||
return new DecoratorPoint(attributes.key, [{ type: tagName }])
|
return new DecoratorPoint(attributes, [{ type: tagName }])
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodes = createChildren(children, { key: attributes.key })
|
const nodes = createChildren(children, { key: attributes.key })
|
||||||
@@ -106,6 +111,7 @@ const CREATORS = {
|
|||||||
anchorOffset: 0,
|
anchorOffset: 0,
|
||||||
focusOffset: nodes.reduce((len, n) => len + n.text.length, 0),
|
focusOffset: nodes.reduce((len, n) => len + n.text.length, 0),
|
||||||
marks: [{ type: tagName }],
|
marks: [{ type: tagName }],
|
||||||
|
isAtomic: !!attributes.data.atomic,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
return nodes
|
return nodes
|
||||||
|
@@ -19,6 +19,7 @@ const DEFAULTS = {
|
|||||||
isBackward: null,
|
isBackward: null,
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
marks: null,
|
marks: null,
|
||||||
|
isAtomic: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,6 +85,7 @@ class Range extends Record(DEFAULTS) {
|
|||||||
isBackward: attrs.isBackward,
|
isBackward: attrs.isBackward,
|
||||||
isFocused: attrs.isFocused,
|
isFocused: attrs.isFocused,
|
||||||
marks: attrs.marks,
|
marks: attrs.marks,
|
||||||
|
isAtomic: attrs.isAtomic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +101,7 @@ class Range extends Record(DEFAULTS) {
|
|||||||
if ('isFocused' in attrs) props.isFocused = attrs.isFocused
|
if ('isFocused' in attrs) props.isFocused = attrs.isFocused
|
||||||
if ('marks' in attrs)
|
if ('marks' in attrs)
|
||||||
props.marks = attrs.marks == null ? null : Mark.createSet(attrs.marks)
|
props.marks = attrs.marks == null ? null : Mark.createSet(attrs.marks)
|
||||||
|
if ('isAtomic' in attrs) props.isAtomic = attrs.isAtomic
|
||||||
return props
|
return props
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +126,7 @@ class Range extends Record(DEFAULTS) {
|
|||||||
isBackward = null,
|
isBackward = null,
|
||||||
isFocused = false,
|
isFocused = false,
|
||||||
marks = null,
|
marks = null,
|
||||||
|
isAtomic = false,
|
||||||
} = object
|
} = object
|
||||||
|
|
||||||
const range = new Range({
|
const range = new Range({
|
||||||
@@ -133,6 +137,7 @@ class Range extends Record(DEFAULTS) {
|
|||||||
isBackward,
|
isBackward,
|
||||||
isFocused,
|
isFocused,
|
||||||
marks: marks == null ? null : new Set(marks.map(Mark.fromJSON)),
|
marks: marks == null ? null : new Set(marks.map(Mark.fromJSON)),
|
||||||
|
isAtomic,
|
||||||
})
|
})
|
||||||
|
|
||||||
return range
|
return range
|
||||||
@@ -779,6 +784,7 @@ class Range extends Record(DEFAULTS) {
|
|||||||
isFocused: this.isFocused,
|
isFocused: this.isFocused,
|
||||||
marks:
|
marks:
|
||||||
this.marks == null ? null : this.marks.toArray().map(m => m.toJSON()),
|
this.marks == null ? null : this.marks.toArray().map(m => m.toJSON()),
|
||||||
|
isAtomic: this.isAtomic,
|
||||||
}
|
}
|
||||||
|
|
||||||
return object
|
return object
|
||||||
|
@@ -675,6 +675,24 @@ class Value extends Record(DEFAULTS) {
|
|||||||
delete object.selection.focusKey
|
delete object.selection.focusKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
options.preserveDecorations &&
|
||||||
|
object.decorations &&
|
||||||
|
!options.preserveKeys
|
||||||
|
) {
|
||||||
|
const { document } = this
|
||||||
|
object.decorations = object.decorations.map(decoration => {
|
||||||
|
const withPath = {
|
||||||
|
...decoration,
|
||||||
|
anchorPath: document.getPath(decoration.anchorKey),
|
||||||
|
focusPath: document.getPath(decoration.focusKey),
|
||||||
|
}
|
||||||
|
delete withPath.anchorKey
|
||||||
|
delete withPath.focusKey
|
||||||
|
return withPath
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return object
|
return object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,6 +10,65 @@ import Operation from '../models/operation'
|
|||||||
|
|
||||||
const debug = Debug('slate:operation:apply')
|
const debug = Debug('slate:operation:apply')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply adjustments to affected ranges (selections, decorations);
|
||||||
|
* accepts (value, checking function(range) -> bool, applying function(range) -> range)
|
||||||
|
* returns value with affected ranges updated
|
||||||
|
*
|
||||||
|
* @param {Value} value
|
||||||
|
* @param {Function} checkAffected
|
||||||
|
* @param {Function} adjustRange
|
||||||
|
* @return {Value}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function applyRangeAdjustments(value, checkAffected, adjustRange) {
|
||||||
|
// check selection, apply adjustment if affected
|
||||||
|
if (value.selection && checkAffected(value.selection)) {
|
||||||
|
value = value.set('selection', adjustRange(value.selection))
|
||||||
|
}
|
||||||
|
if (!value.decorations) return value
|
||||||
|
|
||||||
|
// check all ranges, apply adjustment if affected
|
||||||
|
const decorations = value.decorations
|
||||||
|
.map(
|
||||||
|
decoration =>
|
||||||
|
checkAffected(decoration) ? adjustRange(decoration) : decoration
|
||||||
|
)
|
||||||
|
.filter(decoration => decoration.anchorKey !== null)
|
||||||
|
return value.set('decorations', decorations)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clear any atomic ranges (in decorations) if they contain the point (key, offset, offset-end?)
|
||||||
|
* specified
|
||||||
|
*
|
||||||
|
* @param {Value} value
|
||||||
|
* @param {String} key
|
||||||
|
* @param {Number} offset
|
||||||
|
* @param {Number?} offsetEnd
|
||||||
|
* @return {Value}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function clearAtomicRangesIfContains(value, key, offset, offsetEnd = null) {
|
||||||
|
return applyRangeAdjustments(
|
||||||
|
value,
|
||||||
|
range => {
|
||||||
|
if (!range.isAtomic) return false
|
||||||
|
const { startKey, startOffset, endKey, endOffset } = range
|
||||||
|
return (
|
||||||
|
(startKey == key &&
|
||||||
|
startOffset < offset &&
|
||||||
|
(endKey != key || endOffset > offset)) ||
|
||||||
|
(offsetEnd &&
|
||||||
|
startKey == key &&
|
||||||
|
startOffset < offsetEnd &&
|
||||||
|
(endKey != key || endOffset > offsetEnd))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
range => range.deselect()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applying functions.
|
* Applying functions.
|
||||||
*
|
*
|
||||||
@@ -65,23 +124,37 @@ const APPLIERS = {
|
|||||||
|
|
||||||
insert_text(value, operation) {
|
insert_text(value, operation) {
|
||||||
const { path, offset, text, marks } = operation
|
const { path, offset, text, marks } = operation
|
||||||
let { document, selection } = value
|
let { document } = value
|
||||||
const { anchorKey, focusKey, anchorOffset, focusOffset } = selection
|
|
||||||
let node = document.assertPath(path)
|
let node = document.assertPath(path)
|
||||||
|
|
||||||
// Update the document
|
// Update the document
|
||||||
node = node.insertText(offset, text, marks)
|
node = node.insertText(offset, text, marks)
|
||||||
document = document.updateNode(node)
|
document = document.updateNode(node)
|
||||||
|
|
||||||
// Update the selection
|
value = value.set('document', document)
|
||||||
if (anchorKey == node.key && anchorOffset >= offset) {
|
|
||||||
selection = selection.moveAnchor(text.length)
|
// if insert happens within atomic ranges, clear
|
||||||
}
|
value = clearAtomicRangesIfContains(value, node.key, offset)
|
||||||
if (focusKey == node.key && focusOffset >= offset) {
|
|
||||||
selection = selection.moveFocus(text.length)
|
// Update the selection, decorations
|
||||||
}
|
value = applyRangeAdjustments(
|
||||||
|
value,
|
||||||
|
({ anchorKey, anchorOffset, isBackward, isAtomic }) =>
|
||||||
|
anchorKey == node.key &&
|
||||||
|
(anchorOffset > offset ||
|
||||||
|
(anchorOffset == offset && (!isAtomic || !isBackward))),
|
||||||
|
range => range.moveAnchor(text.length)
|
||||||
|
)
|
||||||
|
|
||||||
|
value = applyRangeAdjustments(
|
||||||
|
value,
|
||||||
|
({ focusKey, focusOffset, isBackward, isAtomic }) =>
|
||||||
|
focusKey == node.key &&
|
||||||
|
(focusOffset > offset ||
|
||||||
|
(focusOffset == offset && (!isAtomic || isBackward))),
|
||||||
|
range => range.moveFocus(text.length)
|
||||||
|
)
|
||||||
|
|
||||||
value = value.set('document', document).set('selection', selection)
|
|
||||||
return value
|
return value
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -98,7 +171,7 @@ const APPLIERS = {
|
|||||||
const withPath = path
|
const withPath = path
|
||||||
.slice(0, path.length - 1)
|
.slice(0, path.length - 1)
|
||||||
.concat([path[path.length - 1] - 1])
|
.concat([path[path.length - 1] - 1])
|
||||||
let { document, selection } = value
|
let { document } = value
|
||||||
const one = document.assertPath(withPath)
|
const one = document.assertPath(withPath)
|
||||||
const two = document.assertPath(path)
|
const two = document.assertPath(path)
|
||||||
let parent = document.getParent(one.key)
|
let parent = document.getParent(one.key)
|
||||||
@@ -108,36 +181,31 @@ const APPLIERS = {
|
|||||||
// Perform the merge in the document.
|
// Perform the merge in the document.
|
||||||
parent = parent.mergeNode(oneIndex, twoIndex)
|
parent = parent.mergeNode(oneIndex, twoIndex)
|
||||||
document = document.updateNode(parent)
|
document = document.updateNode(parent)
|
||||||
|
value = value.set('document', document)
|
||||||
|
|
||||||
// If the nodes are text nodes and the selection is inside the second node
|
|
||||||
// update it to refer to the first node instead.
|
|
||||||
if (one.object == 'text') {
|
if (one.object == 'text') {
|
||||||
const { anchorKey, anchorOffset, focusKey, focusOffset } = selection
|
value = applyRangeAdjustments(
|
||||||
let normalize = false
|
value,
|
||||||
|
// If the nodes are text nodes and the range is inside the second node:
|
||||||
if (anchorKey == two.key) {
|
({ anchorKey, focusKey }) =>
|
||||||
selection = selection.moveAnchorTo(
|
anchorKey == two.key || focusKey == two.key,
|
||||||
one.key,
|
// update it to refer to the first node instead:
|
||||||
one.text.length + anchorOffset
|
range => {
|
||||||
)
|
if (range.anchorKey == two.key)
|
||||||
normalize = true
|
range = range.moveAnchorTo(
|
||||||
}
|
one.key,
|
||||||
|
one.text.length + range.anchorOffset
|
||||||
if (focusKey == two.key) {
|
)
|
||||||
selection = selection.moveFocusTo(
|
if (range.focusKey == two.key)
|
||||||
one.key,
|
range = range.moveFocusTo(
|
||||||
one.text.length + focusOffset
|
one.key,
|
||||||
)
|
one.text.length + range.focusOffset
|
||||||
normalize = true
|
)
|
||||||
}
|
return range.normalize(document)
|
||||||
|
}
|
||||||
if (normalize) {
|
)
|
||||||
selection = selection.normalize(document)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the document and selection.
|
|
||||||
value = value.set('document', document).set('selection', selection)
|
|
||||||
return value
|
return value
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -222,44 +290,38 @@ const APPLIERS = {
|
|||||||
remove_node(value, operation) {
|
remove_node(value, operation) {
|
||||||
const { path } = operation
|
const { path } = operation
|
||||||
let { document, selection } = value
|
let { document, selection } = value
|
||||||
const { startKey, endKey } = selection
|
|
||||||
const node = document.assertPath(path)
|
const node = document.assertPath(path)
|
||||||
|
|
||||||
// If the selection is set, check to see if it needs to be updated.
|
if (selection.isSet || value.decorations !== null) {
|
||||||
if (selection.isSet) {
|
|
||||||
const hasStartNode = node.hasNode(startKey)
|
|
||||||
const hasEndNode = node.hasNode(endKey)
|
|
||||||
const first = node.object == 'text' ? node : node.getFirstText() || node
|
const first = node.object == 'text' ? node : node.getFirstText() || node
|
||||||
const last = node.object == 'text' ? node : node.getLastText() || node
|
const last = node.object == 'text' ? node : node.getLastText() || node
|
||||||
const prev = document.getPreviousText(first.key)
|
const prev = document.getPreviousText(first.key)
|
||||||
const next = document.getNextText(last.key)
|
const next = document.getNextText(last.key)
|
||||||
|
|
||||||
// If the start point was in this node, update it to be just before/after.
|
value = applyRangeAdjustments(
|
||||||
if (hasStartNode) {
|
value,
|
||||||
if (prev) {
|
// If the start or end point was in this node
|
||||||
selection = selection.moveStartTo(prev.key, prev.text.length)
|
({ startKey, endKey }) =>
|
||||||
} else if (next) {
|
node.hasNode(startKey) || node.hasNode(endKey),
|
||||||
selection = selection.moveStartTo(next.key, 0)
|
// update it to be just before/after
|
||||||
} else {
|
range => {
|
||||||
selection = selection.deselect()
|
const { startKey, endKey } = range
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the end point was in this node, update it to be just before/after.
|
if (node.hasNode(startKey)) {
|
||||||
if (selection.isSet && hasEndNode) {
|
range = prev
|
||||||
if (prev) {
|
? range.moveStartTo(prev.key, prev.text.length)
|
||||||
selection = selection.moveEndTo(prev.key, prev.text.length)
|
: next ? range.moveStartTo(next.key, 0) : range.deselect()
|
||||||
} else if (next) {
|
}
|
||||||
selection = selection.moveEndTo(next.key, 0)
|
if (node.hasNode(endKey)) {
|
||||||
} else {
|
range = prev
|
||||||
selection = selection.deselect()
|
? range.moveEndTo(prev.key, prev.text.length)
|
||||||
|
: next ? range.moveEndTo(next.key, 0) : range.deselect()
|
||||||
|
}
|
||||||
|
// If the range wasn't deselected, normalize it.
|
||||||
|
if (range.isSet) return range.normalize(document)
|
||||||
|
return range
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
// If the selection wasn't deselected, normalize it.
|
|
||||||
if (selection.isSet) {
|
|
||||||
selection = selection.normalize(document)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the node from the document.
|
// Remove the node from the document.
|
||||||
@@ -268,8 +330,8 @@ const APPLIERS = {
|
|||||||
parent = parent.removeNode(index)
|
parent = parent.removeNode(index)
|
||||||
document = document.updateNode(parent)
|
document = document.updateNode(parent)
|
||||||
|
|
||||||
// Update the document and selection.
|
// Update the document and range.
|
||||||
value = value.set('document', document).set('selection', selection)
|
value = value.set('document', document)
|
||||||
return value
|
return value
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -285,29 +347,47 @@ const APPLIERS = {
|
|||||||
const { path, offset, text } = operation
|
const { path, offset, text } = operation
|
||||||
const { length } = text
|
const { length } = text
|
||||||
const rangeOffset = offset + length
|
const rangeOffset = offset + length
|
||||||
let { document, selection } = value
|
let { document } = value
|
||||||
const { anchorKey, focusKey, anchorOffset, focusOffset } = selection
|
|
||||||
let node = document.assertPath(path)
|
let node = document.assertPath(path)
|
||||||
|
|
||||||
if (anchorKey == node.key) {
|
// if insert happens within atomic ranges, clear
|
||||||
if (anchorOffset >= rangeOffset) {
|
value = clearAtomicRangesIfContains(
|
||||||
selection = selection.moveAnchor(-length)
|
value,
|
||||||
} else if (anchorOffset > offset) {
|
node.key,
|
||||||
selection = selection.moveAnchorTo(anchorKey, offset)
|
offset,
|
||||||
}
|
offset + length
|
||||||
}
|
)
|
||||||
|
|
||||||
if (focusKey == node.key) {
|
value = applyRangeAdjustments(
|
||||||
if (focusOffset >= rangeOffset) {
|
value,
|
||||||
selection = selection.moveFocus(-length)
|
// if anchor of range is here
|
||||||
} else if (focusOffset > offset) {
|
({ anchorKey }) => anchorKey == node.key,
|
||||||
selection = selection.moveFocusTo(focusKey, offset)
|
// adjust if it is in or past the removal range
|
||||||
}
|
range =>
|
||||||
}
|
range.anchorOffset >= rangeOffset
|
||||||
|
? range.moveAnchor(-length)
|
||||||
|
: range.anchorOffset > offset
|
||||||
|
? range.moveAnchorTo(range.anchorKey, offset)
|
||||||
|
: range
|
||||||
|
)
|
||||||
|
|
||||||
|
value = applyRangeAdjustments(
|
||||||
|
value,
|
||||||
|
// if focus of range is here
|
||||||
|
({ focusKey }) => focusKey == node.key,
|
||||||
|
// adjust if it is in or past the removal range
|
||||||
|
range =>
|
||||||
|
range.focusOffset >= rangeOffset
|
||||||
|
? range.moveFocus(-length)
|
||||||
|
: range.focusOffset > offset
|
||||||
|
? range.moveFocusTo(range.focusKey, offset)
|
||||||
|
: range
|
||||||
|
)
|
||||||
|
|
||||||
node = node.removeText(offset, length)
|
node = node.removeText(offset, length)
|
||||||
document = document.updateNode(node)
|
document = document.updateNode(node)
|
||||||
value = value.set('document', document).set('selection', selection)
|
value = value.set('document', document)
|
||||||
return value
|
return value
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -400,7 +480,7 @@ const APPLIERS = {
|
|||||||
|
|
||||||
split_node(value, operation) {
|
split_node(value, operation) {
|
||||||
const { path, position, properties } = operation
|
const { path, position, properties } = operation
|
||||||
let { document, selection } = value
|
let { document } = value
|
||||||
|
|
||||||
// Calculate a few things...
|
// Calculate a few things...
|
||||||
const node = document.assertPath(path)
|
const node = document.assertPath(path)
|
||||||
@@ -416,32 +496,37 @@ const APPLIERS = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
document = document.updateNode(parent)
|
document = document.updateNode(parent)
|
||||||
|
|
||||||
// Determine whether we need to update the selection...
|
|
||||||
const { startKey, endKey, startOffset, endOffset } = selection
|
|
||||||
const next = document.getNextText(node.key)
|
const next = document.getNextText(node.key)
|
||||||
let normalize = false
|
|
||||||
|
|
||||||
// If the start point is after or equal to the split, update it.
|
value = applyRangeAdjustments(
|
||||||
if (node.key == startKey && position <= startOffset) {
|
value,
|
||||||
selection = selection.moveStartTo(next.key, startOffset - position)
|
// check if range is affected
|
||||||
normalize = true
|
({ startKey, startOffset, endKey, endOffset }) =>
|
||||||
}
|
(node.key == startKey && position <= startOffset) ||
|
||||||
|
(node.key == endKey && position <= endOffset),
|
||||||
|
// update its start / end as needed
|
||||||
|
range => {
|
||||||
|
const { startKey, startOffset, endKey, endOffset } = range
|
||||||
|
let normalize = false
|
||||||
|
|
||||||
// If the end point is after or equal to the split, update it.
|
if (node.key == startKey && position <= startOffset) {
|
||||||
if (node.key == endKey && position <= endOffset) {
|
range = range.moveStartTo(next.key, startOffset - position)
|
||||||
selection = selection.moveEndTo(next.key, endOffset - position)
|
normalize = true
|
||||||
normalize = true
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize the selection if we changed it, since the methods we use might
|
if (node.key == endKey && position <= endOffset) {
|
||||||
// leave it in a non-normalized value.
|
range = range.moveEndTo(next.key, endOffset - position)
|
||||||
if (normalize) {
|
normalize = true
|
||||||
selection = selection.normalize(document)
|
}
|
||||||
}
|
|
||||||
|
// Normalize the selection if we changed it
|
||||||
|
if (normalize) return range.normalize(document)
|
||||||
|
return range
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Return the updated value.
|
// Return the updated value.
|
||||||
value = value.set('document', document).set('selection', selection)
|
value = value.set('document', document)
|
||||||
return value
|
return value
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@@ -34,6 +34,9 @@ const h = createHyperscript({
|
|||||||
u: 'underline',
|
u: 'underline',
|
||||||
fontSize: 'font-size',
|
fontSize: 'font-size',
|
||||||
},
|
},
|
||||||
|
decorators: {
|
||||||
|
highlight: 'highlight',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -0,0 +1,61 @@
|
|||||||
|
/** @jsx h */
|
||||||
|
|
||||||
|
import h from '../../../helpers/h'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
type: 'remove_text',
|
||||||
|
path: [0, 0],
|
||||||
|
offset: 13,
|
||||||
|
text: 'on',
|
||||||
|
marks: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'remove_text',
|
||||||
|
path: [1, 0],
|
||||||
|
offset: 0,
|
||||||
|
text: 'This ',
|
||||||
|
marks: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'remove_text',
|
||||||
|
path: [2, 0],
|
||||||
|
offset: 10,
|
||||||
|
text: 'ation',
|
||||||
|
marks: [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const input = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
This <highlight atomic>decoration</highlight> should be invalid,{' '}
|
||||||
|
<highlight atomic>this</highlight> one shouldn't.
|
||||||
|
</paragraph>
|
||||||
|
<paragraph>
|
||||||
|
This <highlight atomic>decoration</highlight> will be fine.
|
||||||
|
</paragraph>
|
||||||
|
<paragraph>
|
||||||
|
This <highlight>decoration</highlight> can be altered, since non-atomic.
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const output = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
This decorati should be invalid, <highlight atomic>this</highlight> one
|
||||||
|
shouldn't.
|
||||||
|
</paragraph>
|
||||||
|
<paragraph>
|
||||||
|
<highlight atomic>decoration</highlight> will be fine.
|
||||||
|
</paragraph>
|
||||||
|
<paragraph>
|
||||||
|
This <highlight>decor</highlight> can be altered, since non-atomic.
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
@@ -0,0 +1,61 @@
|
|||||||
|
/** @jsx h */
|
||||||
|
|
||||||
|
import h from '../../../helpers/h'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
type: 'insert_text',
|
||||||
|
path: [0, 0],
|
||||||
|
offset: 6,
|
||||||
|
text: 'x',
|
||||||
|
marks: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'insert_text',
|
||||||
|
path: [1, 0],
|
||||||
|
offset: 5,
|
||||||
|
text: 'small ',
|
||||||
|
marks: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'remove_text',
|
||||||
|
path: [2, 0],
|
||||||
|
offset: 10,
|
||||||
|
text: 'ation',
|
||||||
|
marks: [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const input = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
This <highlight atomic>decoration</highlight> should be invalid,{' '}
|
||||||
|
<highlight atomic>this</highlight> one shouldn't.
|
||||||
|
</paragraph>
|
||||||
|
<paragraph>
|
||||||
|
This <highlight atomic>decoration</highlight> will be fine.
|
||||||
|
</paragraph>
|
||||||
|
<paragraph>
|
||||||
|
This <highlight>decoration</highlight> can be altered, since non-atomic.
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const output = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
This dxecoration should be invalid, <highlight atomic>this</highlight>{' '}
|
||||||
|
one shouldn't.
|
||||||
|
</paragraph>
|
||||||
|
<paragraph>
|
||||||
|
This small <highlight atomic>decoration</highlight> will be fine.
|
||||||
|
</paragraph>
|
||||||
|
<paragraph>
|
||||||
|
This <highlight>decor</highlight> can be altered, since non-atomic.
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
@@ -0,0 +1,33 @@
|
|||||||
|
/** @jsx h */
|
||||||
|
|
||||||
|
import h from '../../../helpers/h'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
type: 'remove_text',
|
||||||
|
path: [0, 0],
|
||||||
|
offset: 2,
|
||||||
|
text: ' there',
|
||||||
|
marks: [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const input = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
Hi<cursor /> there <highlight>you</highlight> person
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const output = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
Hi<cursor /> <highlight>you</highlight> person
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
@@ -0,0 +1,33 @@
|
|||||||
|
/** @jsx h */
|
||||||
|
|
||||||
|
import h from '../../../helpers/h'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
type: 'insert_text',
|
||||||
|
path: [0, 0],
|
||||||
|
offset: 2,
|
||||||
|
text: ' added',
|
||||||
|
marks: [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const input = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
Hi<cursor /> there <highlight>you</highlight>
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const output = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
Hi added<cursor /> there <highlight>you</highlight>
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
@@ -0,0 +1,45 @@
|
|||||||
|
/** @jsx h */
|
||||||
|
|
||||||
|
import h from '../../../helpers/h'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
type: 'merge_node',
|
||||||
|
path: [1],
|
||||||
|
position: 1,
|
||||||
|
properties: {},
|
||||||
|
target: null,
|
||||||
|
},
|
||||||
|
// also merge the resulting leaves
|
||||||
|
{
|
||||||
|
type: 'merge_node',
|
||||||
|
path: [0, 1],
|
||||||
|
position: 1,
|
||||||
|
properties: {},
|
||||||
|
target: null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const input = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
The decoration begins<highlight key="1" /> in this paragraph
|
||||||
|
</paragraph>
|
||||||
|
<paragraph>
|
||||||
|
And ends in this soon-to-be merged<highlight key="1" /> one.
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const output = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
The decoration begins<highlight key="1" /> in this paragraphAnd ends in
|
||||||
|
this soon-to-be merged<highlight key="1" /> one.
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
@@ -0,0 +1,34 @@
|
|||||||
|
/** @jsx h */
|
||||||
|
|
||||||
|
import h from '../../../helpers/h'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
type: 'remove_node',
|
||||||
|
path: [0],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const input = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
The decoration begins<highlight key="1" /> in this soon deleted
|
||||||
|
paragraph
|
||||||
|
</paragraph>
|
||||||
|
<paragraph>
|
||||||
|
And ends in this <highlight key="1" />one.
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const output = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
<highlight key="1" />And ends in this <highlight key="1" />one.
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
@@ -33,9 +33,14 @@ describe('operations', async () => {
|
|||||||
const operations = module.default
|
const operations = module.default
|
||||||
const change = input.change()
|
const change = input.change()
|
||||||
change.applyOperations(operations)
|
change.applyOperations(operations)
|
||||||
const opts = { preserveSelection: true, preserveData: true }
|
const opts = {
|
||||||
|
preserveSelection: true,
|
||||||
|
preserveDecorations: true,
|
||||||
|
preserveData: true,
|
||||||
|
}
|
||||||
const actual = change.value.toJSON(opts)
|
const actual = change.value.toJSON(opts)
|
||||||
const expected = output.toJSON(opts)
|
const expected = output.toJSON(opts)
|
||||||
|
|
||||||
assert.deepEqual(actual, expected)
|
assert.deepEqual(actual, expected)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -48,6 +48,7 @@ export const output = {
|
|||||||
isBackward: false,
|
isBackward: false,
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
marks: null,
|
marks: null,
|
||||||
|
isAtomic: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -45,6 +45,7 @@ export const output = {
|
|||||||
isBackward: false,
|
isBackward: false,
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
marks: null,
|
marks: null,
|
||||||
|
isAtomic: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user