slate#Operation TypeScript Examples
The following examples show how to use
slate#Operation.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: utils.ts From slate-ot with MIT License | 7 votes |
checkOp = (snapshot: Editor, op: Operation) => {
switch (op.type) {
case 'remove_text': {
const leaf = Node.leaf(snapshot, op.path);
const textToRemove = leaf.text.slice(
op.offset,
op.offset + op.text.length
);
expect(textToRemove).toBe(op.text);
break;
}
case 'merge_node': {
const prev = Node.get(snapshot, Path.previous(op.path));
const prevLen = Text.isText(prev)
? prev.text.length
: prev.children.length;
expect(prevLen).toBe(op.position);
break;
}
case 'remove_node': {
// op.node needs to be checked
break;
}
default:
return;
}
}
Example #2
Source File: utils.ts From slate-ot with MIT License | 7 votes |
applyOp = (
snapshot: Editor,
ops: Operation[] | Operation
): Editor => {
slateType.normalize(ops).forEach((op) => {
checkOp(snapshot, op);
slateType.apply(snapshot, op);
});
return snapshot;
}
Example #3
Source File: SlateType.ts From slate-ot with MIT License | 6 votes |
doTransform = (
leftOp: Operation,
rightOp: Operation,
side: 'left' | 'right'
): Operation[] => {
// return side === 'left' ? leftOp : rightOp;
switch (leftOp.type) {
case 'insert_text':
return OT.transInsertText(leftOp, rightOp, side);
case 'remove_text':
return OT.transRemoveText(leftOp, rightOp, side);
case 'insert_node':
return OT.transInsertNode(leftOp, rightOp, side);
case 'remove_node':
return OT.transRemoveNode(leftOp, rightOp, side);
case 'split_node':
return OT.transSplitNode(leftOp, rightOp, side);
case 'merge_node':
return OT.transMergeNode(leftOp, rightOp, side);
case 'move_node':
return OT.transMoveNode(leftOp, rightOp, side);
case 'set_node':
return OT.transSetNode(leftOp, rightOp, side);
default:
throw new Error('Unsupported OP');
}
}
Example #4
Source File: op-generator.ts From slate-ot with MIT License | 6 votes |
generateRandomRemoveTextOp = (snapshot): Operation | null => {
const randomLeaf = getRandomLeafWithPath(snapshot);
if (!randomLeaf) return null;
const offset = fuzzer.randomInt(randomLeaf.text.length);
const textLength = fuzzer.randomInt(randomLeaf.text.length - offset);
return {
type: 'remove_text',
path: randomLeaf.path,
offset,
text: randomLeaf.text.slice(offset, offset + textLength),
};
}
Example #5
Source File: op-generator.ts From slate-ot with MIT License | 6 votes |
generateRandomInsertTextOp = (snapshot): Operation | null => {
const randomLeaf = getRandomLeafWithPath(snapshot);
return randomLeaf
? {
type: 'insert_text',
path: randomLeaf.path,
offset: fuzzer.randomInt(randomLeaf.text.length),
text: fuzzer.randomWord(),
}
: null;
}
Example #6
Source File: op-generator.ts From slate-ot with MIT License | 6 votes |
generateAndApplyRandomOp = function (snapshot) {
const result = _.cloneDeep(snapshot);
let op: Operation | null = null;
while (!op) {
let index = fuzzer.randomInt(genRandOp.length);
op = genRandOp[index](snapshot);
}
Transforms.transform(result, op);
return [[op], result];
}
Example #7
Source File: op-generator.ts From slate-ot with MIT License | 6 votes |
generateRandomInsertNodeOp = (snapshot): Operation => {
const randomPath = getRandomPathTo(snapshot);
const parent = <Ancestor>Node.get(snapshot, Path.parent(randomPath));
let node;
if (parent.children[0] && Text.isText(parent.children[0])) {
node = { text: fuzzer.randomWord() };
} else if (!parent.children[0] && fuzzer.randomInt(3) === 0) {
node = { text: fuzzer.randomWord() };
} else if (fuzzer.randomInt(2) === 0) {
node = {
type: BLOCKS[fuzzer.randomInt(BLOCKS.length)],
children: [{ text: fuzzer.randomWord() }, { text: fuzzer.randomWord() }],
};
} else {
node = {
type: BLOCKS[fuzzer.randomInt(BLOCKS.length)],
children: [
{
type: BLOCKS[fuzzer.randomInt(BLOCKS.length)],
children: [
{ text: fuzzer.randomWord() },
{ text: fuzzer.randomWord() },
],
},
],
};
}
return {
type: 'insert_node',
path: randomPath,
node,
};
}
Example #8
Source File: op-generator.ts From slate-ot with MIT License | 6 votes |
generateRandomRemoveNodeOp = (snapshot): Operation | null => {
const randomPath = getRandomPathFrom(snapshot);
return randomPath.length
? {
type: 'remove_node',
path: randomPath,
node: Node.get(snapshot, randomPath),
}
: null;
}
Example #9
Source File: op-generator.ts From slate-ot with MIT License | 6 votes |
generateRandomSplitNodeOp = (snapshot): Operation | null => {
const randomPath = getRandomPathFrom(snapshot);
const node = Node.get(snapshot, randomPath);
const position = Text.isText(node)
? <number>fuzzer.randomInt(node.text.length + 1)
: <number>fuzzer.randomInt(node.children.length + 1);
return randomPath.length
? {
type: 'split_node',
path: randomPath,
position,
target: null,
properties: {},
}
: null;
}
Example #10
Source File: op-generator.ts From slate-ot with MIT License | 6 votes |
generateRandomMoveNodeOp = (snapshot): Operation | null => {
let count = 0;
while (count < 10) {
count++;
const path = getRandomPathFrom(snapshot);
const newPath = getRandomPathTo(snapshot);
if (Path.isSibling(path, newPath)) {
const parent = <Ancestor>Node.get(snapshot, Path.parent(newPath));
if (newPath[newPath.length - 1] == parent.children.length) {
newPath[newPath.length - 1]--;
}
}
if (!Path.isAncestor(path, newPath)) {
return {
type: 'move_node',
path,
newPath,
};
}
}
return null;
}
Example #11
Source File: op-generator.ts From slate-ot with MIT License | 6 votes |
generateRandomSetNodeOp = (snapshot): Operation | null => {
const path = getRandomPathFrom(snapshot);
if (path.length === 0) return null;
const newProperties = {};
KEYS.forEach((key) => {
if (fuzzer.randomInt(2) === 0) {
newProperties[key] = VALUES[fuzzer.randomInt(VALUES.length)];
}
});
return {
type: 'set_node',
path,
properties: {},
newProperties,
};
}
Example #12
Source File: SlateType.ts From slate-ot with MIT License | 6 votes |
xTransform1x1 = (
leftOp: Operation,
rightOp: Operation,
side: 'left' | 'right'
): [Operation[], Operation[]] => {
const other = side === 'left' ? 'right' : 'left';
return [
doTransform(leftOp, rightOp, side),
doTransform(rightOp, leftOp, other),
];
}
Example #13
Source File: SlateType.ts From slate-ot with MIT License | 6 votes |
xTransform1xN = (
leftOp: Operation,
rightOps: Operation[],
side: 'left' | 'right'
): [Operation[], Operation[]] => {
let rRes: Operation[] = [];
for (let n = 0; n < rightOps.length; n++) {
let rightOp: Operation = rightOps[n];
let [l, r] = xTransform1x1(leftOp, rightOp, side);
rRes = rRes.concat(r);
if (l.length === 0) {
rRes = rRes.concat(rightOps.slice(n + 1));
return [[], rRes];
}
if (l.length > 1) {
[l, r] = xTransformMxN(l, rightOps.slice(n + 1), side);
rRes = rRes.concat(r);
return [l, rRes];
}
// l.length == 1
leftOp = l[0];
}
return [[leftOp], rRes];
}
Example #14
Source File: SlateType.ts From slate-ot with MIT License | 6 votes |
xTransformMxN = (
leftOps: Operation[],
rightOps: Operation[],
side: 'left' | 'right'
): [Operation[], Operation[]] => {
let leftRes: Operation[] = [];
for (let m = 0; m < leftOps.length; m++) {
let leftOp: Operation = leftOps[m];
let [lRes, rRes] = xTransform1xN(leftOp, rightOps, side);
leftRes = leftRes.concat(lRes);
rightOps = rRes;
}
return [leftRes, rightOps];
}
Example #15
Source File: OT.ts From slate-ot with MIT License | 6 votes |
pathTransform = (
leftOp: NodeOperation | TextOperation,
rightOp: Operation
): (NodeOperation | TextOperation)[] => {
let path = Path.transform(leftOp.path, rightOp);
return path
? [
{
...leftOp,
path,
},
]
: [];
}
Example #16
Source File: client.ts From slate-ot with MIT License | 6 votes |
doc.subscribe((err: any) => {
if (err) {
throw err;
}
e.children = doc.data.children;
console.log(JSON.stringify(e.children));
doc.on('op', (op: Operation | Operation[], options: any) => {
if (options.source === clientId) return;
const ops = Array.isArray(op) ? op : [op];
for (const o of ops) {
console.log(op);
Transforms.transform(e, o);
}
});
e.apply({
type: 'insert_node',
path: [0],
node: { children: [{ text: 'a quick brown fox' }] },
});
});
Example #17
Source File: slate-plugin.ts From slate-vue with MIT License | 6 votes |
elementWatcherPlugin = (vm: any, type: string) => {
const update = vm._watcher.update;
vm._watcher.update = () => {
const op: Operation = vm.$editor._operation;
// some op doesn't change element, so prevent updating
if(op) {
if(op.type === 'remove_text' || op.type === 'insert_text' || op.type === 'set_selection') {
return
}
if(op.type === 'remove_node' && type === 'element') {
return
}
}
update.call(vm._watcher)
}
// gvm.$on('forceUpdate', ()=>{
// update.call(vm._watcher)
// })
}
Example #18
Source File: SlateType.ts From slate-ot with MIT License | 5 votes |
slateType = {
name: 'slate-ot-type',
uri: 'http://sharejs.org/types/slate-ot-type',
create(init: Element): Editor {
const e = createEditor();
e.children = init.children;
return <Editor>init;
},
apply(snapshot: Editor, op: Operation[] | Operation) {
slateType.normalize(op).forEach((o) => {
if (o.type === 'set_node' && o.path.length === 0) {
for (const key in o.newProperties) {
if (key === 'id' || key === 'type' || key === 'children') {
throw new Error(`Cannot set the "${key}" property of nodes!`);
}
const value = o.newProperties[key];
if (value == null) {
delete snapshot[key];
} else {
snapshot[key] = value;
}
}
} else {
Transforms.transform(snapshot, o);
}
});
return snapshot;
},
transform(
leftOps: Operation[],
rightOps: Operation[],
side: 'left' | 'right'
): Operation[] {
let [leftRes] = xTransformMxN(leftOps, rightOps, side);
return leftRes;
},
// serialize(snapshot) {
// return JSON.stringify(snapshot);
// },
// deserialize(data) {
// // return Value.fromJSON(data);
// },
normalize(op: Operation | Operation[]): Operation[] {
return Array.isArray(op) ? op : [op];
},
}
Example #19
Source File: op-generator.ts From slate-ot with MIT License | 5 votes |
generateRandomMergeNodeOp = (snapshot): Operation | null => {
const randomPath = getRandomPathFrom(snapshot);
if (randomPath.length == 0 || randomPath[randomPath.length - 1] == 0) {
return null;
}
const prev = Node.get(snapshot, Path.previous(randomPath));
const node = Node.get(snapshot, randomPath);
const properties = {};
// Object.keys(prev).forEach((key) => {
// if (key !== 'text' && key !== 'children') {
// properties[key] = null;
// }
// });
// Object.keys(node).forEach((key) => {
// if (key !== 'text' && key !== 'children') {
// properties[key] = node[key];
// }
// });
if (Text.isText(prev) && Text.isText(node)) {
return {
type: 'merge_node',
path: randomPath,
position: prev.text.length,
target: null,
properties,
};
}
if (!Text.isText(prev) && !Text.isText(node)) {
return {
type: 'merge_node',
path: randomPath,
position: prev.children.length,
target: null,
properties,
};
}
return null;
}
Example #20
Source File: client.ts From slate-ot with MIT License | 5 votes |
e.onChange = () => {
e.operations.forEach((o: Operation) => {
if (o.type !== 'set_selection') {
doc.submitOp(o, { source: clientId });
}
});
};
Example #21
Source File: transSetNode.ts From slate-ot with MIT License | 5 votes |
transSetNode = (
leftOp: SetNodeOperation,
rightOp: Operation,
side: 'left' | 'right'
): SetNodeOperation[] => {
switch (rightOp.type) {
case 'split_node': {
if (Path.equals(leftOp.path, rightOp.path)) {
return [
leftOp,
{
...leftOp,
path: Path.next(leftOp.path),
},
];
}
return <SetNodeOperation[]>pathTransform(leftOp, rightOp);
}
case 'merge_node': {
if (Path.equals(leftOp.path, rightOp.path)) {
return [];
}
return <SetNodeOperation[]>pathTransform(leftOp, rightOp);
}
case 'set_node': {
if (!Path.equals(leftOp.path, rightOp.path)) {
return [leftOp];
}
return side === 'left'
? [
{
...leftOp,
newProperties: {
...rightOp.newProperties,
...leftOp.newProperties,
},
},
]
: [
{
...leftOp,
newProperties: {
...leftOp.newProperties,
...rightOp.newProperties,
},
},
];
}
// insert_text
// remove_text
// insert_node
// remove_node
// move_node
default:
return <SetNodeOperation[]>pathTransform(leftOp, rightOp);
}
}
Example #22
Source File: runtime-util.ts From slate-vue with MIT License | 5 votes |
transform = function(editor: Editor, op: Operation, Vue?: any) {
switch (op.type) {
case 'insert_node': {
const { path, node } = op
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
getChildren(parent).splice(index, 0, clone(node))
break
}
case 'insert_text': {
const { path, offset, text } = op
const node = Node.leaf(editor, path)
const before = node.text.slice(0, offset)
const after = node.text.slice(offset)
node.text = before + text + after
break
}
case 'merge_node': {
const { path } = op
const node = Node.get(editor, path)
const prevPath = Path.previous(path)
const prev = Node.get(editor, prevPath)
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
if (Text.isText(node) && Text.isText(prev)) {
prev.text += node.text
} else if (!Text.isText(node) && !Text.isText(prev)) {
getChildren(prev).push(...getChildren(node))
} else {
throw new Error(
`Cannot apply a "merge_node" operation at path [${path}] to nodes of different interaces: ${node} ${prev}`
)
}
getChildren(parent).splice(index, 1)
break
}
case 'move_node': {
const { path, newPath } = op
if (Path.isAncestor(path, newPath)) {
throw new Error(
`Cannot move a path [${path}] to new path [${newPath}] because the destination is inside itself.`
)
}
const node = Node.get(editor, path)
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
// This is tricky, but since the `path` and `newPath` both refer to
// the same snapshot in time, there's a mismatch. After either
// removing the original position, the second step's path can be out
// of date. So instead of using the `op.newPath` directly, we
// transform `op.path` to ascertain what the `newPath` would be after
// the operation was applied.
getChildren(parent).splice(index, 1)
const truePath = Path.transform(path, op)!
const newParent = Node.get(editor, Path.parent(truePath))
const newIndex = truePath[truePath.length - 1]
getChildren(newParent).splice(newIndex, 0, node)
break
}
case 'remove_node': {
const { path } = op
NODE_TO_KEY.delete(Node.get(editor, path))
const index = path[path.length - 1]
const parent = Node.parent(editor, path)
getChildren(parent).splice(index, 1)
break
}
case 'remove_text': {
const { path, offset, text } = op
const node = Node.leaf(editor, path)
const before = node.text.slice(0, offset)
const after = node.text.slice(offset + text.length)
node.text = before + after
break
}
case 'set_node': {
const { path, newProperties } = op
if (path.length === 0) {
throw new Error(`Cannot set properties on the root node!`)
}
const node = Node.get(editor, path)
for (const key in newProperties) {
if (key === 'children' || key === 'text') {
throw new Error(`Cannot set the "${key}" property of nodes!`)
}
const value = (newProperties as any)[key]
if(Vue) {
if (value == null) {
Vue.delete(node, key)
} else {
Vue.set(node, key, value)
}
}
}
break
}
case 'set_selection': {
break
}
case 'split_node': {
const { path, position, properties } = op
if (path.length === 0) {
throw new Error(
`Cannot apply a "split_node" operation at path [${path}] because the root node cannot be split.`
)
}
const node = Node.get(editor, path)
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
let newNode: Descendant
if (Text.isText(node)) {
const before = node.text.slice(0, position)
const after = node.text.slice(position)
node.text = before
newNode = {
...node,
...(properties as Partial<Text>),
text: after,
}
} else {
const before = node.children.slice(0, position)
const after = node.children.slice(position)
node.children = before
newNode = {
...node,
...(properties as Partial<Element>),
children: after,
}
}
getChildren(parent).splice(index + 1, 0, newNode)
break
}
}
}
Example #23
Source File: runtime-util.ts From slate-vue with MIT License | 5 votes |
transform = function(editor: Editor, op: Operation) {
switch (op.type) {
case 'insert_node': {
const { path, node } = op
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
getChildren(parent).splice(index, 0, clone(node))
break
}
case 'insert_text': {
const { path, offset, text } = op
const node = Node.leaf(editor, path)
const before = node.text.slice(0, offset)
const after = node.text.slice(offset)
node.text = before + text + after
break
}
case 'merge_node': {
const { path } = op
const node = Node.get(editor, path)
const prevPath = Path.previous(path)
const prev = Node.get(editor, prevPath)
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
if (Text.isText(node) && Text.isText(prev)) {
prev.text += node.text
} else if (!Text.isText(node) && !Text.isText(prev)) {
getChildren(prev).push(...getChildren(node))
} else {
throw new Error(
`Cannot apply a "merge_node" operation at path [${path}] to nodes of different interaces: ${node} ${prev}`
)
}
getChildren(parent).splice(index, 1)
break
}
case 'move_node': {
const { path, newPath } = op
if (Path.isAncestor(path, newPath)) {
throw new Error(
`Cannot move a path [${path}] to new path [${newPath}] because the destination is inside itself.`
)
}
const node = Node.get(editor, path)
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
// This is tricky, but since the `path` and `newPath` both refer to
// the same snapshot in time, there's a mismatch. After either
// removing the original position, the second step's path can be out
// of date. So instead of using the `op.newPath` directly, we
// transform `op.path` to ascertain what the `newPath` would be after
// the operation was applied.
getChildren(parent).splice(index, 1)
const truePath = Path.transform(path, op)!
const newParent = Node.get(editor, Path.parent(truePath))
const newIndex = truePath[truePath.length - 1]
getChildren(newParent).splice(newIndex, 0, node)
break
}
case 'remove_node': {
const { path } = op
NODE_TO_KEY.delete(Node.get(editor, path))
const index = path[path.length - 1]
const parent = Node.parent(editor, path)
getChildren(parent).splice(index, 1)
break
}
case 'remove_text': {
const { path, offset, text } = op
const node = Node.leaf(editor, path)
const before = node.text.slice(0, offset)
const after = node.text.slice(offset + text.length)
node.text = before + after
break
}
case 'set_node': {
const { path, newProperties } = op
if (path.length === 0) {
throw new Error(`Cannot set properties on the root node!`)
}
const node = Node.get(editor, path)
for (const key in newProperties) {
if (key === 'children' || key === 'text') {
throw new Error(`Cannot set the "${key}" property of nodes!`)
}
const value = (newProperties as any)[key]
if (value == null) {
Vue.delete(node, key)
} else {
Vue.set(node, key, value)
}
}
break
}
case 'set_selection': {
break
}
case 'split_node': {
const { path, position, properties } = op
if (path.length === 0) {
throw new Error(
`Cannot apply a "split_node" operation at path [${path}] because the root node cannot be split.`
)
}
const node = Node.get(editor, path)
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
let newNode: Descendant
if (Text.isText(node)) {
const before = node.text.slice(0, position)
const after = node.text.slice(position)
node.text = before
newNode = {
...node,
...(properties as Partial<Text>),
text: after,
}
} else {
const before = node.children.slice(0, position)
const after = node.children.slice(position)
node.children = before
newNode = {
...node,
...(properties as Partial<Element>),
children: after,
}
}
getChildren(parent).splice(index + 1, 0, newNode)
break
}
}
}
Example #24
Source File: with-angular.ts From slate-angular with MIT License | 4 votes |
withAngular = <T extends Editor>(editor: T, clipboardFormatKey = 'x-slate-fragment') => {
const e = editor as T & AngularEditor;
const { apply, onChange, deleteBackward } = e;
e.deleteBackward = unit => {
if (unit !== 'line') {
return deleteBackward(unit);
}
if (editor.selection && Range.isCollapsed(editor.selection)) {
const parentBlockEntry = Editor.above(editor, {
match: n => Editor.isBlock(editor, n),
at: editor.selection,
});
if (parentBlockEntry) {
const [, parentBlockPath] = parentBlockEntry;
const parentElementRange = Editor.range(
editor,
parentBlockPath,
editor.selection.anchor
);
const currentLineRange = findCurrentLineRange(e, parentElementRange);
if (!Range.isCollapsed(currentLineRange)) {
Transforms.delete(editor, { at: currentLineRange });
}
}
}
};
e.apply = (op: Operation) => {
const matches: [Path | PathRef, Key][] = [];
switch (op.type) {
case 'insert_text':
case 'remove_text':
case 'set_node': {
for (const [node, path] of Editor.levels(e, { at: op.path })) {
const key = AngularEditor.findKey(e, node);
matches.push([path, key]);
}
break;
}
case 'insert_node':
case 'remove_node':
case 'merge_node':
case 'split_node': {
for (const [node, path] of Editor.levels(e, {
at: Path.parent(op.path),
})) {
const key = AngularEditor.findKey(e, node);
matches.push([path, key]);
}
break;
}
case 'move_node': {
const commonPath = Path.common(Path.parent(op.path), Path.parent(op.newPath));
for (const [node, path] of Editor.levels(e, { at: Path.parent(op.path) })) {
const key = AngularEditor.findKey(e, node);
matches.push([Editor.pathRef(editor, path), key]);
}
for (const [node, path] of Editor.levels(e, { at: Path.parent(op.newPath) })) {
if(path.length > commonPath.length){
const key = AngularEditor.findKey(e, node);
matches.push([Editor.pathRef(editor, path), key]);
}
}
break;
}
}
apply(op);
for (const [source, key] of matches) {
const [node] = Editor.node(e, Path.isPath(source) ? source: source.current);
NODE_TO_KEY.set(node, key);
}
};
e.onChange = () => {
const onContextChange = EDITOR_TO_ON_CHANGE.get(e);
if (onContextChange) {
onContextChange();
}
onChange();
};
e.setFragmentData = (data: Pick<DataTransfer, 'getData' | 'setData'>) => {
const { selection } = e;
if (!selection) {
return;
}
const [start, end] = Range.edges(selection);
const startVoid = Editor.void(e, { at: start.path });
const endVoid = Editor.void(e, { at: end.path });
if (Range.isCollapsed(selection) && !startVoid) {
return;
}
// Create a fake selection so that we can add a Base64-encoded copy of the
// fragment to the HTML, to decode on future pastes.
const domRange = AngularEditor.toDOMRange(e, selection);
let contents = domRange.cloneContents();
let attach = contents.childNodes[0] as HTMLElement;
// Make sure attach is non-empty, since empty nodes will not get copied.
const contentsArray = Array.from(contents.children);
contentsArray.forEach(node => {
if (node.textContent && node.textContent.trim() !== '') {
attach = node as HTMLElement;
}
});
// COMPAT: If the end node is a void node, we need to move the end of the
// range from the void node's spacer span, to the end of the void node's
// content, since the spacer is before void's content in the DOM.
if (endVoid) {
const [voidNode] = endVoid;
const r = domRange.cloneRange();
const domNode = AngularEditor.toDOMNode(e, voidNode);
r.setEndAfter(domNode);
contents = r.cloneContents();
}
// COMPAT: If the start node is a void node, we need to attach the encoded
// fragment to the void node's content node instead of the spacer, because
// attaching it to empty `<div>/<span>` nodes will end up having it erased by
// most browsers. (2018/04/27)
if (startVoid) {
attach = contents.querySelector('[data-slate-spacer]')! as HTMLElement;
}
// Remove any zero-width space spans from the cloned DOM so that they don't
// show up elsewhere when pasted.
Array.from(contents.querySelectorAll('[data-slate-zero-width]')).forEach(
zw => {
const isNewline = zw.getAttribute('data-slate-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
// node, wrap it in a `<span>` so we have something to set an attribute on.
if (isDOMText(attach)) {
const span = attach.ownerDocument.createElement('span');
// COMPAT: In Chrome and Safari, if we don't add the `white-space` style
// then leading and trailing spaces will be ignored. (2017/09/21)
span.style.whiteSpace = 'pre';
span.appendChild(attach);
contents.appendChild(span);
attach = span;
}
const fragment = e.getFragment();
const stringObj = JSON.stringify(fragment);
const encoded = window.btoa(encodeURIComponent(stringObj));
attach.setAttribute('data-slate-fragment', encoded);
data.setData(`application/${clipboardFormatKey}`, encoded);
// Add the content to a <div> so that we can get its inner HTML.
const div = contents.ownerDocument.createElement('div');
div.appendChild(contents);
div.setAttribute('hidden', 'true');
contents.ownerDocument.body.appendChild(div);
data.setData('text/html', div.innerHTML);
data.setData('text/plain', getPlainText(div));
contents.ownerDocument.body.removeChild(div);
return data;
};
e.deleteCutData = () => {
const { selection } = editor;
if (selection) {
if (Range.isExpanded(selection)) {
Editor.deleteFragment(editor);
} else {
const node = Node.parent(editor, selection.anchor.path);
if (Editor.isVoid(editor, node)) {
Transforms.delete(editor);
}
}
}
};
e.insertData = (data: DataTransfer) => {
if (!e.insertFragmentData(data)) {
e.insertTextData(data);
}
};
e.insertFragmentData = (data: DataTransfer): boolean => {
/**
* Checking copied fragment from application/x-slate-fragment or data-slate-fragment
*/
const fragment =
data.getData(`application/${clipboardFormatKey}`) ||
getSlateFragmentAttribute(data);
if (fragment) {
const decoded = decodeURIComponent(window.atob(fragment));
const parsed = JSON.parse(decoded) as Node[];
e.insertFragment(parsed);
return true;
}
return false;
};
e.insertTextData = (data: DataTransfer): boolean => {
const text = data.getData('text/plain');
if (text) {
const lines = text.split(/\r\n|\r|\n/);
let split = false;
for (const line of lines) {
if (split) {
Transforms.splitNodes(e, { always: true });
}
e.insertText(line);
split = true;
}
return true;
}
return false;
};
e.onKeydown = () => { };
e.onClick = () => { };
e.isBlockCard = (element) => false;
e.onError = (errorData: SlateError) => {
if (errorData.nativeError) {
console.error(errorData.nativeError);
} else {
console.error(errorData);
}
};
return e;
}
Example #25
Source File: transSplitNode.ts From slate-ot with MIT License | 4 votes |
transSplitNode = (
leftOp: SplitNodeOperation,
rightOp: Operation,
_side: 'left' | 'right'
): SplitNodeOperation[] => {
switch (rightOp.type) {
case 'insert_text': {
if (!Path.equals(leftOp.path, rightOp.path)) {
return [leftOp];
}
if (leftOp.position < rightOp.offset) {
return [leftOp];
}
return [
{
...leftOp,
position: leftOp.position + rightOp.text.length,
},
];
}
case 'remove_text': {
if (!Path.equals(leftOp.path, rightOp.path)) {
return [leftOp];
}
if (leftOp.position < rightOp.offset) {
return [leftOp];
}
if (leftOp.position >= rightOp.offset + rightOp.text.length) {
return [
{
...leftOp,
position: leftOp.position - rightOp.text.length,
},
];
}
return [
{
...leftOp,
position: rightOp.offset,
},
];
}
case 'insert_node': {
if (Path.isParent(leftOp.path, rightOp.path)) {
let offset = rightOp.path[rightOp.path.length - 1];
if (leftOp.position <= offset) {
return [leftOp];
}
return [
{
...leftOp,
position: leftOp.position + 1,
},
];
}
return <SplitNodeOperation[]>pathTransform(leftOp, rightOp);
}
case 'remove_node': {
if (Path.isParent(leftOp.path, rightOp.path)) {
let offset = rightOp.path[rightOp.path.length - 1];
if (leftOp.position <= offset) {
return [leftOp];
}
return [
{
...leftOp,
position: leftOp.position - 1,
},
];
}
return <SplitNodeOperation[]>pathTransform(leftOp, rightOp);
}
case 'split_node': {
if (Path.equals(leftOp.path, rightOp.path)) {
if (leftOp.position < rightOp.position) {
return [leftOp];
}
if (leftOp.position === rightOp.position) {
return [];
}
return [
{
...leftOp,
path: Path.next(leftOp.path),
position: leftOp.position - rightOp.position,
},
];
}
if (Path.isParent(leftOp.path, rightOp.path)) {
const offset = rightOp.path[rightOp.path.length - 1];
if (leftOp.position <= offset) {
return [leftOp];
}
return [
{
...leftOp,
position: leftOp.position + 1,
},
];
}
return <SplitNodeOperation[]>pathTransform(leftOp, rightOp);
}
case 'merge_node': {
if (Path.equals(leftOp.path, rightOp.path)) {
return [];
}
if (Path.isParent(leftOp.path, rightOp.path)) {
const offset = rightOp.path[rightOp.path.length - 1];
if (leftOp.position < offset) {
return [leftOp];
}
if (leftOp.position > offset) {
return [
{
...leftOp,
position: leftOp.position - 1,
},
];
}
// conflicting ops, discard split
return [];
}
return <SplitNodeOperation[]>pathTransform(leftOp, rightOp);
}
case 'move_node': {
if (Path.equals(rightOp.path, rightOp.newPath)) {
return [leftOp];
}
let position = leftOp.position;
let offset = rightOp.path[rightOp.path.length - 1];
let newOffset = rightOp.newPath[rightOp.newPath.length - 1];
// only src path is leftOp's child
if (
Path.isParent(leftOp.path, rightOp.path) &&
offset < leftOp.position
) {
position--;
}
// only dst path is leftOp's child
if (
Path.isParent(leftOp.path, rightOp.newPath) &&
newOffset < leftOp.position
) {
position++;
}
return [
{
...leftOp,
path: Path.transform(leftOp.path, rightOp)!,
position,
},
];
}
// set_node
default:
return <SplitNodeOperation[]>pathTransform(leftOp, rightOp);
}
}
Example #26
Source File: transRemoveText.ts From slate-ot with MIT License | 4 votes |
transRemoveText = (
leftOp: RemoveTextOperation,
rightOp: Operation,
_side: 'left' | 'right'
): RemoveTextOperation[] => {
switch (rightOp.type) {
case 'insert_text': {
if (!Path.equals(leftOp.path, rightOp.path)) {
return [leftOp];
}
if (leftOp.offset + leftOp.text.length <= rightOp.offset) {
return [leftOp];
}
if (rightOp.offset <= leftOp.offset) {
return [
{
...leftOp,
offset: leftOp.offset + rightOp.text.length,
},
];
}
const intersectingIndex = rightOp.offset - leftOp.offset;
const leftText = leftOp.text.slice(0, intersectingIndex);
const rightText = leftOp.text.slice(intersectingIndex);
return [
{
...leftOp,
text: leftText + rightOp.text + rightText,
},
];
}
case 'remove_text': {
if (!Path.equals(leftOp.path, rightOp.path)) {
return [leftOp];
}
if (leftOp.offset + leftOp.text.length <= rightOp.offset) {
return [leftOp];
}
if (rightOp.offset + rightOp.text.length <= leftOp.offset) {
return [
{
...leftOp,
offset: leftOp.offset - rightOp.text.length,
},
];
}
// leftText and rightText both come from leftOp
const leftTextEnd = Math.max(rightOp.offset - leftOp.offset, 0);
const leftText = leftOp.text.slice(0, leftTextEnd);
const rightTextStart = Math.min(
leftOp.text.length,
rightOp.offset + rightOp.text.length - leftOp.offset
);
const rightText = leftOp.text.slice(rightTextStart);
return [
{
...leftOp,
offset: Math.min(leftOp.offset, rightOp.offset),
text: leftText + rightText,
},
];
}
case 'split_node': {
if (Path.equals(leftOp.path, rightOp.path)) {
// text to remove all within the former segment
if (leftOp.offset + leftOp.text.length <= rightOp.position) {
return [leftOp];
}
// text to remove all within the latter segment
if (leftOp.offset >= rightOp.position) {
return [
{
...leftOp,
path: Path.next(rightOp.path),
offset: leftOp.offset - rightOp.position,
},
];
}
// text to remove in both segments
return [
{
...leftOp,
text: leftOp.text.slice(0, rightOp.position - leftOp.offset),
},
{
...leftOp,
path: Path.next(rightOp.path),
offset: 0,
text: leftOp.text.slice(rightOp.position - leftOp.offset),
},
];
}
return <RemoveTextOperation[]>pathTransform(leftOp, rightOp);
}
case 'merge_node': {
if (Path.equals(leftOp.path, rightOp.path)) {
return [
{
...leftOp,
path: Path.previous(rightOp.path),
offset: leftOp.offset + rightOp.position,
},
];
}
return <RemoveTextOperation[]>pathTransform(leftOp, rightOp);
}
// insert_node
// remove_node
// move_node
// set_node
default:
return <RemoveTextOperation[]>pathTransform(leftOp, rightOp);
}
}
Example #27
Source File: transRemoveNode.ts From slate-ot with MIT License | 4 votes |
transRemoveNode = (
leftOp: RemoveNodeOperation,
rightOp: Operation,
side: 'left' | 'right'
): (RemoveNodeOperation | SplitNodeOperation)[] => {
switch (rightOp.type) {
case 'split_node': {
if (Path.equals(leftOp.path, rightOp.path)) {
// should remove the target node && the split node
// however, after removing the target node,
// the split node becomes the same path.
// TODO: node within op should be split.
return [leftOp, leftOp];
}
return <RemoveNodeOperation[]>pathTransform(leftOp, rightOp);
}
case 'merge_node': {
// One of the to-merge nodes are removed, we have to discard merging.
// It might be better to keep the unremoved node,
// but it is tricky to keep properties by merge-and-split,
// therefore we choose to remove the entired merged node
if (
Path.equals(leftOp.path, Path.previous(rightOp.path)) ||
Path.equals(leftOp.path, rightOp.path)
) {
return [
{
...leftOp,
path: Path.previous(rightOp.path),
},
];
}
return <RemoveNodeOperation[]>pathTransform(leftOp, rightOp);
}
case 'move_node': {
if (Path.equals(rightOp.path, rightOp.newPath)) {
return [leftOp];
}
let [rr, ri] = decomposeMove(rightOp);
let [l, r] = xTransformMxN([leftOp], [rr, ri], side);
// leftOp removes a node within the moved zone
// ri must have survived and not been changed
// we use ri's path to compute the true path to remove
if (l.length === 0) {
return [
{
...leftOp,
path: ri.path.concat(leftOp.path.slice(rr.path.length)),
},
];
}
// now we have l.length === 1
// in most cases we can return, but to be consist with move-remove
// we need to handle the case that rightOp moved a branch out
if (r.length === 1 && r[0].type === 'insert_node') {
l = [
...l,
{
type: 'remove_node',
path: r[0].path,
node: { text: '' },
},
];
}
return <RemoveNodeOperation[]>l;
}
// insert_text
// remove_text
// insert_node
// remove_node
// set_node
default:
return <RemoveNodeOperation[]>pathTransform(leftOp, rightOp);
}
}
Example #28
Source File: transMoveNode.ts From slate-ot with MIT License | 4 votes |
transMoveNode = (
leftOp: MoveNodeOperation,
rightOp: Operation,
side: 'left' | 'right'
): (MoveNodeOperation | RemoveNodeOperation | SplitNodeOperation)[] => {
if (Path.equals(leftOp.path, leftOp.newPath)) {
return [];
}
let [lr, li] = decomposeMove(leftOp);
switch (rightOp.type) {
case 'insert_node': {
let [l] = xTransformMxN([lr, li], [rightOp], side);
return [
composeMove(<RemoveNodeOperation>l[0], <InsertNodeOperation>l[1]),
];
}
case 'remove_node': {
let [l] = xTransformMxN([lr, li], [rightOp], side);
// normal case
if (l.length === 2) {
return [
{
...leftOp,
...composeMove(
<RemoveNodeOperation>l[0],
<InsertNodeOperation>l[1]
),
},
];
}
// leftOp moves a branch into the removed zone
else if (l.length === 1 && l[0].type === 'remove_node') {
return [l[0]];
}
// leftOp moves a branch out of the removed zone
// we choose NOT to keep it
else if (l.length === 1 && l[0].type === 'insert_node') {
return [];
}
// l.length === 0, move within the removed zone
else {
return [];
}
}
case 'split_node': {
const after: boolean =
Path.isSibling(leftOp.path, leftOp.newPath) &&
Path.endsBefore(leftOp.path, leftOp.newPath);
// the split nodes have to move separately
if (Path.equals(leftOp.path, rightOp.path)) {
const newPath = Path.transform(leftOp.newPath, rightOp)!;
// the split nodes are moved AFTER newPath
if (after) {
return [
{
...leftOp, // move first node
newPath,
},
{
...leftOp, // move second node
newPath,
},
];
}
// the split nodes are moved BEFORE newPath
else {
const firstMove: MoveNodeOperation = {
...leftOp, // move second node
path: Path.next(leftOp.path),
newPath,
};
const secondMove: MoveNodeOperation = {
...leftOp, // move first node
path: Path.transform(leftOp.path, firstMove)!,
newPath: Path.previous(Path.transform(newPath, firstMove)!),
};
return [firstMove, secondMove];
}
}
let newPath = Path.transform(leftOp.newPath, rightOp)!;
// the newPath is between the split nodes
// note that it is impossible for newPath == rightOp.path
if (Path.equals(newPath, Path.next(rightOp.path)) && !after) {
newPath = rightOp.path;
}
// a tricky case:
// when after is true, and the splitOp separated path and newPath
// to no-longer be siblings, the after becomes false
// in this case we should move one step after
else if (after && !Path.isSibling(leftOp.path, newPath)) {
newPath = Path.next(newPath);
}
// finally, the normal case
return [
{
...leftOp,
path: Path.transform(leftOp.path, rightOp)!,
newPath,
},
];
}
case 'merge_node': {
let path = rightOp.path;
let prevPath = Path.previous(path);
path = Path.transform(path, leftOp)!;
prevPath = Path.transform(prevPath, leftOp)!;
// ops conflict with each other, discard merge
// Note that the merge-and-split node cannot keep properties,
// so we have to remove it.
if (!Path.equals(path, Path.next(prevPath))) {
return [
{
...rightOp,
type: 'split_node',
path: Path.previous(rightOp.path),
},
leftOp,
{
type: 'remove_node',
path,
node: { text: '' },
},
];
}
// a tricky case:
// if leftOp.path is a child of rightOp.prevPath,
// and leftOp.newPath is a child of rightOp.path.
// intentionally, leftOp wants to insert BEFORE leftOp.newPath;
// but after merging, leftOp's path and newPath become siblings.
// the move dst turns out to be AFTER the transformed newPath.
// therefore we should move one step ahead
if (
Path.isParent(Path.previous(rightOp.path), leftOp.path) &&
Path.isParent(rightOp.path, leftOp.newPath)
) {
return [
{
...leftOp,
path: Path.transform(leftOp.path, rightOp)!,
newPath: Path.previous(Path.transform(leftOp.newPath, rightOp)!),
},
];
}
return [
{
...leftOp,
path: Path.transform(leftOp.path, rightOp)!,
newPath: Path.transform(leftOp.newPath, rightOp)!,
},
];
}
case 'move_node': {
// the other side didn't do anything
if (Path.equals(rightOp.path, rightOp.newPath)) {
return [leftOp];
}
let [rr, ri] = decomposeMove(rightOp);
let [l, r] = xTransformMxN([lr, li], [rr, ri], side);
// normal case
if (l.length === 2) {
return [
composeMove(<RemoveNodeOperation>l[0], <InsertNodeOperation>l[1]),
];
}
// handling conflict
if (r.length === 1) {
// must have l.length === 1 && l[0].type === r[0].type)
return side === 'left' ? [reverseMove(rr, ri), leftOp] : [];
}
// for the rest we have r.length === 2
if (l.length === 0) {
l[0] = {
type: 'remove_node',
path: ri.path.concat(lr.path.slice(rr.path.length)),
node: { text: '' },
};
l[1] = {
type: 'insert_node',
path: ri.path.concat(li.path.slice(rr.path.length)),
node: { text: '' },
};
}
// for the rest we have l.length === 1
else if (l[0].type === 'remove_node') {
l[1] = {
type: 'insert_node',
path: (<InsertNodeOperation>r[1]).path.concat(
li.path.slice((<RemoveNodeOperation>r[0]).path.length)
),
node: { text: '' },
};
}
// for the rest we have l[0].type === 'insert_node'
else {
l[1] = l[0];
l[0] = {
type: 'remove_node',
path: ri.path.concat(lr.path.slice(rr.path.length)),
node: { text: '' },
};
}
return [
composeMove(<RemoveNodeOperation>l[0], <InsertNodeOperation>l[1]),
];
}
// insert_text
// remove_text
// set_node
default:
return [leftOp];
}
}
Example #29
Source File: transMergeNode.ts From slate-ot with MIT License | 4 votes |
transMergeNode = (
leftOp: MergeNodeOperation,
rightOp: Operation,
_side: 'left' | 'right'
): (MergeNodeOperation | MoveNodeOperation | RemoveNodeOperation)[] => {
switch (rightOp.type) {
case 'insert_text': {
if (Path.equals(leftOp.path, Path.next(rightOp.path))) {
return [
{
...leftOp,
position: leftOp.position + rightOp.text.length,
},
];
}
return [leftOp];
}
case 'remove_text': {
if (Path.equals(leftOp.path, Path.next(rightOp.path))) {
return [
{
...leftOp,
position: leftOp.position - rightOp.text.length,
},
];
}
return [leftOp];
}
case 'insert_node': {
if (Path.equals(leftOp.path, rightOp.path)) {
const offset = Text.isText(rightOp.node)
? rightOp.node.text.length
: rightOp.node.children.length;
return [
leftOp, // merge the inserted node
{
...leftOp, // merge the original node
position: leftOp.position + offset,
},
];
}
if (Path.isParent(Path.previous(leftOp.path), rightOp.path)) {
return [
{
...leftOp,
position: leftOp.position + 1,
},
];
}
return <MergeNodeOperation[]>pathTransform(leftOp, rightOp);
}
case 'remove_node': {
if (Path.isParent(Path.previous(leftOp.path), rightOp.path)) {
return [
{
...leftOp,
position: leftOp.position - 1,
},
];
}
const path = Path.transform(leftOp.path, rightOp);
const prevPath = Path.transform(Path.previous(leftOp.path), rightOp);
if (path && prevPath) {
return [
{
...leftOp,
path,
},
];
}
// conflicting ops, we have to discard merge
// for now we simply remove the merged node
else if (!path && prevPath) {
return [
{
...rightOp,
path: prevPath,
},
];
} else if (path && !prevPath) {
return [
{
...rightOp,
path,
},
];
}
// both to-merge nodes are removed
else {
return [];
}
}
case 'split_node': {
if (Path.equals(leftOp.path, rightOp.path)) {
return [
leftOp,
{
...leftOp,
position: leftOp.position + rightOp.position,
},
];
}
if (Path.equals(Path.previous(leftOp.path), rightOp.path)) {
return [
{
...leftOp,
path: Path.next(leftOp.path),
position: leftOp.position - rightOp.position,
},
];
}
if (Path.isParent(Path.previous(leftOp.path), rightOp.path)) {
return [
{
...leftOp,
position: leftOp.position + 1,
},
];
}
const path = Path.transform(leftOp.path, rightOp)!;
// conflicting ops, we choose to discard split
if (path[path.length - 1] === 0) {
return [
{
...rightOp,
type: 'merge_node',
path: Path.next(rightOp.path),
},
leftOp,
];
}
return <MergeNodeOperation[]>pathTransform(leftOp, rightOp);
}
case 'merge_node': {
if (Path.equals(leftOp.path, rightOp.path)) {
return [];
}
if (Path.equals(Path.previous(leftOp.path), rightOp.path)) {
return [
{
...leftOp,
path: Path.previous(leftOp.path),
position: leftOp.position + rightOp.position,
},
];
}
if (Path.isParent(Path.previous(leftOp.path), rightOp.path)) {
return [
{
...leftOp,
position: leftOp.position - 1,
},
];
}
return <MergeNodeOperation[]>pathTransform(leftOp, rightOp);
}
case 'move_node': {
if (Path.equals(rightOp.path, rightOp.newPath)) {
return [leftOp];
}
const path = leftOp.path;
const prevPath = Path.previous(path);
const newPath = Path.transform(path, rightOp)!;
const newPrevPath = Path.transform(prevPath, rightOp)!;
// Ops conflict with each other, discard merge.
// Note that the merge-and-split node cannot keep properties,
// so we have to remove it.
if (!Path.equals(newPath, Path.next(newPrevPath))) {
return [
{
type: 'remove_node',
path: newPath,
node: { text: '' },
},
];
}
let position = leftOp.position;
// only src path is leftOp's child
if (Path.isParent(prevPath, rightOp.path)) {
position--;
}
// only dst path is leftOp's child
if (Path.isParent(prevPath, rightOp.newPath)) {
position++;
}
return [
{
...leftOp,
path: newPath,
position,
},
];
}
// set_node
default:
return <MergeNodeOperation[]>pathTransform(leftOp, rightOp);
}
}