From fcb9cfd434c8813f78b7e1219b78901e6a847573 Mon Sep 17 00:00:00 2001 From: liujia02 Date: Wed, 3 Feb 2021 16:11:05 +0800 Subject: [PATCH] chore(shared): add vithvue --- packages/slate-vue-shared/plugins/index.ts | 3 +- .../slate-vue-shared/plugins/runtime-util.ts | 357 ++++++++++++++++++ .../slate-vue-shared/plugins/vue-runtime.ts | 39 ++ packages/slate-vue-shared/plugins/with-vue.ts | 191 ++++++++++ packages/slate-vue/components/children.tsx | 6 +- packages/slate-vue/components/editable.tsx | 2 +- packages/slate-vue/components/element.tsx | 6 +- packages/slate-vue/plugins/slate-plugin.ts | 3 +- 8 files changed, 597 insertions(+), 10 deletions(-) create mode 100644 packages/slate-vue-shared/plugins/runtime-util.ts create mode 100644 packages/slate-vue-shared/plugins/vue-runtime.ts create mode 100644 packages/slate-vue-shared/plugins/with-vue.ts diff --git a/packages/slate-vue-shared/plugins/index.ts b/packages/slate-vue-shared/plugins/index.ts index 53bffb7..7e196e9 100644 --- a/packages/slate-vue-shared/plugins/index.ts +++ b/packages/slate-vue-shared/plugins/index.ts @@ -1 +1,2 @@ -export { VueEditor } from './vue-editor' \ No newline at end of file +export { VueEditor } from './vue-editor' +export { withVue } from './with-vue' \ No newline at end of file diff --git a/packages/slate-vue-shared/plugins/runtime-util.ts b/packages/slate-vue-shared/plugins/runtime-util.ts new file mode 100644 index 0000000..a637f63 --- /dev/null +++ b/packages/slate-vue-shared/plugins/runtime-util.ts @@ -0,0 +1,357 @@ +import { Editor, Operation, Node, Path, Text, Descendant, NodeEntry, Transforms as SlateTransforms, Location } from 'slate'; +import { NODE_TO_KEY } from '../utils'; +import Vue from 'vue' + +export const getChildren = (node: Node): any => { + return Editor.isEditor(node) ? node._state: node.children +} + +export const clone = (node: any): any => { + return JSON.parse(JSON.stringify(node)) +} + +// a minimum version of Editor.transform for runtime +export const 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[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: after, + } + } else { + const before = node.children.slice(0, position) + const after = node.children.slice(position) + node.children = before + + newNode = { + ...node, + ...(properties as Partial), + children: after, + } + } + + getChildren(parent).splice(index + 1, 0, newNode) + + break + } + } +} + +// a minimum version of Node for runtime +export const runtimeNode = { + child(root: Node, index: number): Descendant { + if (Text.isText(root)) { + throw new Error( + `Cannot get the child of a text node: ${JSON.stringify(root)}` + ) + } + + const c = getChildren(root)[index] as Descendant + + if (c == null) { + throw new Error( + `Cannot get child at index \`${index}\` in node: ${JSON.stringify( + root + )}` + ) + } + + return c + }, + has(root: Node, path: Path): boolean { + let node = root + + for (let i = 0; i < path.length; i++) { + const p = path[i] + const children = getChildren(node) + if (Text.isText(node) || !children[p]) { + return false + } + + node = children[p] + } + + return true +}, + get(root: Node, path: Path): Node { + let node = root + + for (let i = 0; i < path.length; i++) { + const p = path[i] + const children = getChildren(node) + + if (Text.isText(node) || !children[p]) { + throw new Error( + `Cannot find a descendant at path [${path}] in node: ${JSON.stringify( + root + )}` + ) + } + + node = children[p] + } + + return node + }, + first(root: Node, path: Path): NodeEntry { + const p = path.slice() + let n = Node.get(root, p) + const children = getChildren(n) + + while (n) { + if (Text.isText(n) || children.length === 0) { + break + } else { + n = children[0] + p.push(0) + } + } + + return [n, p] + }, + last(root: Node, path: Path): NodeEntry { + const p = path.slice() + let n = Node.get(root, p) + const children = getChildren(n) + + while (n) { + if (Text.isText(n) || children.length === 0) { + break + } else { + const i = children.length - 1 + n = children[i] + p.push(i) + } + } + + return [n, p] + }, + *nodes( + root: Node, + options: { + from?: Path + to?: Path + reverse?: boolean + pass?: (entry: NodeEntry) => boolean + } = {} + ): Generator { + const { pass, reverse = false } = options + const { from = [], to } = options + const visited = new Set() + let p: Path = [] + let n = root + + while (true) { + if (to && (reverse ? Path.isBefore(p, to) : Path.isAfter(p, to))) { + break + } + + if (!visited.has(n)) { + yield [n, p] + } + + // If we're allowed to go downward and we haven't decsended yet, do. + if ( + !visited.has(n) && + !Text.isText(n) && + getChildren(n).length !== 0 && + (pass == null || pass([n, p]) === false) + ) { + visited.add(n) + let nextIndex = reverse ? getChildren(n).length - 1 : 0 + + if (Path.isAncestor(p, from)) { + nextIndex = from[p.length] + } + + p = p.concat(nextIndex) + n = Node.get(root, p) + continue + } + + // If we're at the root and we can't go down, we're done. + if (p.length === 0) { + break + } + + // If we're going forward... + if (!reverse) { + const newPath = Path.next(p) + + if (Node.has(root, newPath)) { + p = newPath + n = Node.get(root, p) + continue + } + } + + // If we're going backward... + if (reverse && p[p.length - 1] !== 0) { + const newPath = Path.previous(p) + p = newPath + n = Node.get(root, p) + continue + } + + // Otherwise we're going upward... + p = Path.parent(p) + n = Node.get(root, p) + visited.add(n) + } + } +} + +export const isVueObject = (obj: any) => { + return obj.__ob__ +} + +// a Transform version for runtime +export const Transforms = (() => { + const {select} = SlateTransforms + SlateTransforms.select = (editor: Editor, target: Location) => { + if(isVueObject(target)) { + target = clone(target) + } + return select(editor, target) + } + return SlateTransforms +})() diff --git a/packages/slate-vue-shared/plugins/vue-runtime.ts b/packages/slate-vue-shared/plugins/vue-runtime.ts new file mode 100644 index 0000000..e1dd3e3 --- /dev/null +++ b/packages/slate-vue-shared/plugins/vue-runtime.ts @@ -0,0 +1,39 @@ +// in vue runtime, we must change same slate original behavior + +// same +import { Node } from 'slate'; +import {runtimeNode} from './runtime-util'; + +const runtime = () => { + const {get, nodes, has, first, child, last} = Node + Node.child = runtimeNode.child + Node.has = runtimeNode.has + Node.get = runtimeNode.get + Node.first = runtimeNode.first + Node.last = runtimeNode.last + Node.nodes = runtimeNode.nodes + return () => { + Node.get = get + Node.nodes = nodes + Node.has = has + Node.first = first + Node.child = child + Node.last = last + } +} + +export const vueRuntimeFunc = (func: any): any => { + return (...args: any) => { + const restore = runtime() + const result = func(...args) + restore() + return result + } +} + +export const vueRuntime = (func: any, ...args: any): any => { + const restore = runtime() + const result = func(...args) + restore() + return result +} diff --git a/packages/slate-vue-shared/plugins/with-vue.ts b/packages/slate-vue-shared/plugins/with-vue.ts new file mode 100644 index 0000000..e19ebf8 --- /dev/null +++ b/packages/slate-vue-shared/plugins/with-vue.ts @@ -0,0 +1,191 @@ +import { Editor, Node, Path, Operation, Transforms, Range } from 'slate' + +import { VueEditor } from './vue-editor' +import { Key, isDOMText, getPlainText, EDITOR_TO_ON_CHANGE, NODE_TO_KEY } from '../utils' +import {vueRuntime} from './vue-runtime'; +import {transform} from './runtime-util'; + +/** + * `withReact` adds React and DOM specific behaviors to the editor. + */ + +export const withVue = (editor: T) => { + const e = editor as T & VueEditor + const { apply, onChange } = e + + // we must change ob all the time + e.apply = (op: Operation) => { + const matches: [Path, Key][] = [] + // global operation for render + + e._operation = op + + 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 = VueEditor.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 = VueEditor.findKey(e, node) + matches.push([path, key]) + } + + break + } + + case 'move_node': { + // TODO + break + } + } + + apply(op) + + for (const [path, key] of matches) { + const [node] = Editor.node(e, path) + NODE_TO_KEY.set(node, key) + } + + // apply all change to _state + vueRuntime(()=>{ + transform(editor, op) + }) + } + + e.setFragmentData = (data: DataTransfer) => { + 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 = VueEditor.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. + contents.childNodes.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 = VueEditor.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 `
/` 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 `` so we have something to set an attribute on. + if (isDOMText(attach)) { + const span = document.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 string = JSON.stringify(fragment) + const encoded = window.btoa(encodeURIComponent(string)) + attach.setAttribute('data-slate-fragment', encoded) + data.setData('application/x-slate-fragment', encoded) + + // Add the content to a
so that we can get its inner HTML. + const div = document.createElement('div') + div.appendChild(contents) + div.setAttribute('hidden', 'true') + document.body.appendChild(div) + data.setData('text/html', div.innerHTML) + data.setData('text/plain', getPlainText(div)) + document.body.removeChild(div) + } + + e.insertData = (data: DataTransfer) => { + const fragment = data.getData('application/x-slate-fragment') + + if (fragment) { + const decoded = decodeURIComponent(window.atob(fragment)) + const parsed = JSON.parse(decoded) as Node[] + e.insertFragment(parsed) + return + } + + 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 }) + } + + Transforms.insertText(e, line) + split = true + } + } + } + + e.onChange = () => { + const onContextChange = EDITOR_TO_ON_CHANGE.get(e) + + if (onContextChange) { + onContextChange() + } + + onChange() + } + + return e +} diff --git a/packages/slate-vue/components/children.tsx b/packages/slate-vue/components/children.tsx index 61f20ce..419ff33 100644 --- a/packages/slate-vue/components/children.tsx +++ b/packages/slate-vue/components/children.tsx @@ -3,7 +3,7 @@ import * as tsx from "vue-tsx-support"; import { Editor, Range, Element, NodeEntry, Ancestor, Descendant, Operation, Path, Node } from 'slate'; import TextComponent from './text' -import ElementComponent from './element' +import {Element as ElementComponent} from './element' import { VueEditor, elementWatcherPlugin, SlateMixin } from '../plugins'; import { KEY_TO_VNODE, NODE_TO_INDEX, NODE_TO_KEY, NODE_TO_PARENT } from 'slate-vue-shared'; import {fragment} from './fragment'; @@ -13,7 +13,7 @@ import { PropType } from 'vue'; * Children. */ -const Children: any = tsx.component({ +export const Children: any = tsx.component({ props: { // only element or editor node: { @@ -111,4 +111,4 @@ const Children: any = tsx.component({ } }); -export default Children +// export default Children diff --git a/packages/slate-vue/components/editable.tsx b/packages/slate-vue/components/editable.tsx index 1a50f15..096f4de 100644 --- a/packages/slate-vue/components/editable.tsx +++ b/packages/slate-vue/components/editable.tsx @@ -1,4 +1,4 @@ -import Children from './children'; +import {Children} from './children'; import * as tsx from "vue-tsx-support"; import {VueEditor, SlateMixin, useEffect, useRef} from '../plugins'; import { diff --git a/packages/slate-vue/components/element.tsx b/packages/slate-vue/components/element.tsx index 8a409e2..b845f13 100644 --- a/packages/slate-vue/components/element.tsx +++ b/packages/slate-vue/components/element.tsx @@ -9,7 +9,7 @@ import { Editor, Node, Element as SlateElement } from 'slate' import getDirection from 'direction' import Text from './text' -import Children from './children' +import {Children} from './children' import { elementWatcherPlugin, useEffect, useRef, VueEditor } from '../plugins'; import { NODE_TO_PARENT, NODE_TO_INDEX, KEY_TO_ELEMENT, NODE_TO_ELEMENT, ELEMENT_TO_NODE, @@ -22,7 +22,7 @@ import { VNode, PropType } from 'vue'; * Element */ -const Element = tsx.component({ +export const Element = tsx.component({ props: { element: { type: Object as PropType @@ -141,4 +141,4 @@ export const DefaultElement = (props: RenderElementProps) => { }) } -export default Element +// export default Element diff --git a/packages/slate-vue/plugins/slate-plugin.ts b/packages/slate-vue/plugins/slate-plugin.ts index aa443e8..cbc3b27 100644 --- a/packages/slate-vue/plugins/slate-plugin.ts +++ b/packages/slate-vue/plugins/slate-plugin.ts @@ -1,9 +1,8 @@ import * as tsx from 'vue-tsx-support' import { createEditor, Operation, Editor, Range } from 'slate'; import {hooks} from './vue-hooks'; -import {withVue} from './with-vue'; import Vue from 'vue' -import { NODE_TO_KEY, EDITOR_TO_GVM, GVM_TO_EDITOR } from 'slate-vue-shared'; +import { NODE_TO_KEY, EDITOR_TO_GVM, GVM_TO_EDITOR, withVue } from 'slate-vue-shared'; import {VueEditor} from './vue-editor' const createGvm = () => {