so that we can get its inner HTML.
144 | const div = document.createElement('div')
145 | div.appendChild(contents)
146 | div.setAttribute('hidden', 'true')
147 | document.body.appendChild(div)
148 | data.setData('text/html', div.innerHTML)
149 | data.setData('text/plain', getPlainText(div))
150 | document.body.removeChild(div)
151 | }
152 |
153 | e.insertData = (data: DataTransfer) => {
154 | const fragment = data.getData('application/x-slate-fragment')
155 |
156 | if (fragment) {
157 | const decoded = decodeURIComponent(window.atob(fragment))
158 | const parsed = JSON.parse(decoded) as Node[]
159 | e.insertFragment(parsed)
160 | return
161 | }
162 |
163 | const text = data.getData('text/plain')
164 |
165 | if (text) {
166 | const lines = text.split(/\r\n|\r|\n/)
167 | let split = false
168 |
169 | for (const line of lines) {
170 | if (split) {
171 | Transforms.splitNodes(e, { always: true })
172 | }
173 |
174 | Transforms.insertText(e, line)
175 | split = true
176 | }
177 | }
178 | }
179 |
180 | e.onChange = () => {
181 | const onContextChange = EDITOR_TO_ON_CHANGE.get(e)
182 |
183 | if (onContextChange) {
184 | onContextChange()
185 | }
186 |
187 | onChange()
188 | }
189 |
190 | return e
191 | }
192 |
--------------------------------------------------------------------------------
/packages/slate-vue-shared/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/slate-vue-shared/types/index.ts:
--------------------------------------------------------------------------------
1 | import { Element, Text } from 'slate';
2 |
3 | interface o {
4 | [key: string]: any
5 | }
6 |
7 | type Observable
= T extends o ? (T & PickObservableObject): T;
8 |
9 | interface PickObservableObject {
10 | __ob__: any
11 | }
12 |
13 | export type FlatObservableArray = T extends (infer U)[] ? Observable : T;
14 |
15 | export interface RenderLeafProps {
16 | children: any
17 | leaf: Text
18 | text: Text | undefined
19 | attributes: {
20 | 'data-slate-leaf': true
21 | }
22 | }
23 |
24 | export interface RenderElementAttributes {
25 | 'data-slate-node': 'element'
26 | 'data-slate-void'?: true
27 | 'data-slate-inline'?: true
28 | contentEditable?: false
29 | dir?: 'rtl'
30 | }
31 |
32 | export interface RenderElementProps {
33 | children: any
34 | element: Element
35 | attributes: RenderElementAttributes
36 | }
--------------------------------------------------------------------------------
/packages/slate-vue-shared/utils/dom.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Types.
3 | */
4 |
5 | // COMPAT: This is required to prevent TypeScript aliases from doing some very
6 | // weird things for Slate's types with the same name as globals. (2019/11/27)
7 | // https://github.com/microsoft/TypeScript/issues/35002
8 | import DOMNode = globalThis.Node
9 | import DOMComment = globalThis.Comment
10 | import DOMElement = globalThis.Element
11 | import DOMText = globalThis.Text
12 | import DOMRange = globalThis.Range
13 | import DOMSelection = globalThis.Selection
14 | import DOMStaticRange = globalThis.StaticRange
15 | export {
16 | DOMNode,
17 | DOMComment,
18 | DOMElement,
19 | DOMText,
20 | DOMRange,
21 | DOMSelection,
22 | DOMStaticRange,
23 | }
24 |
25 | export type DOMPoint = [Node, number]
26 |
27 | /**
28 | * Check if a DOM node is a comment node.
29 | */
30 |
31 | export const isDOMComment = (value: any): value is DOMComment => {
32 | return isDOMNode(value) && value.nodeType === 8
33 | }
34 |
35 | /**
36 | * Check if a DOM node is an element node.
37 | */
38 |
39 | export const isDOMElement = (value: any): value is DOMElement => {
40 | return isDOMNode(value) && value.nodeType === 1
41 | }
42 |
43 | /**
44 | * Check if a value is a DOM node.
45 | */
46 |
47 | export const isDOMNode = (value: any): value is DOMNode => {
48 | return value instanceof Node
49 | }
50 |
51 | /**
52 | * Check if a DOM node is an element node.
53 | */
54 |
55 | export const isDOMText = (value: any): value is DOMText => {
56 | return isDOMNode(value) && value.nodeType === 3
57 | }
58 |
59 | /**
60 | * Checks whether a paste event is a plaintext-only event.
61 | */
62 |
63 | export const isPlainTextOnlyPaste = (event: ClipboardEvent) => {
64 | return (
65 | event.clipboardData &&
66 | event.clipboardData.getData('text/plain') !== '' &&
67 | event.clipboardData.types.length === 1
68 | )
69 | }
70 |
71 | /**
72 | * Normalize a DOM point so that it always refers to a text node.
73 | */
74 |
75 | export const normalizeDOMPoint = (domPoint: DOMPoint): DOMPoint => {
76 | let [node, offset] = domPoint
77 |
78 | // If it's an element node, its offset refers to the index of its children
79 | // including comment nodes, so try to find the right text child node.
80 | if (isDOMElement(node) && node.childNodes.length) {
81 | const isLast = offset === node.childNodes.length
82 | const direction = isLast ? 'backward' : 'forward'
83 | const index = isLast ? offset - 1 : offset
84 | node = getEditableChild(node, index, direction)
85 |
86 | // If the node has children, traverse until we have a leaf node. Leaf nodes
87 | // can be either text nodes, or other void DOM nodes.
88 | while (isDOMElement(node) && node.childNodes.length) {
89 | const i = isLast ? node.childNodes.length - 1 : 0
90 | node = getEditableChild(node, i, direction)
91 | }
92 |
93 | // Determine the new offset inside the text node.
94 | offset = isLast && node.textContent != null ? node.textContent.length : 0
95 | }
96 |
97 | // Return the node and offset.
98 | return [node, offset]
99 | }
100 |
101 | /**
102 | * Get the nearest editable child at `index` in a `parent`, preferring
103 | * `direction`.
104 | */
105 |
106 | export const getEditableChild = (
107 | parent: DOMElement,
108 | index: number,
109 | direction: 'forward' | 'backward'
110 | ): DOMNode => {
111 | const { childNodes } = parent
112 | let child = childNodes[index]
113 | let i = index
114 | let triedForward = false
115 | let triedBackward = false
116 |
117 | // While the child is a comment node, or an element node with no children,
118 | // keep iterating to find a sibling non-void, non-comment node.
119 | while (
120 | isDOMComment(child) ||
121 | (isDOMElement(child) && child.childNodes.length === 0) ||
122 | (isDOMElement(child) && child.getAttribute('contenteditable') === 'false')
123 | ) {
124 | if (triedForward && triedBackward) {
125 | break
126 | }
127 |
128 | if (i >= childNodes.length) {
129 | triedForward = true
130 | i = index - 1
131 | direction = 'backward'
132 | continue
133 | }
134 |
135 | if (i < 0) {
136 | triedBackward = true
137 | i = index + 1
138 | direction = 'forward'
139 | continue
140 | }
141 |
142 | child = childNodes[i]
143 | i += direction === 'forward' ? 1 : -1
144 | }
145 |
146 | return child
147 | }
148 |
149 | /**
150 | * Get a plaintext representation of the content of a node, accounting for block
151 | * elements which get a newline appended.
152 | *
153 | * The domNode must be attached to the DOM.
154 | */
155 |
156 | export const getPlainText = (domNode: DOMNode) => {
157 | let text = ''
158 |
159 | if (isDOMText(domNode) && domNode.nodeValue) {
160 | return domNode.nodeValue
161 | }
162 |
163 | if (isDOMElement(domNode)) {
164 | for (const childNode of Array.from(domNode.childNodes)) {
165 | text += getPlainText(childNode)
166 | }
167 |
168 | const display = getComputedStyle(domNode).getPropertyValue('display')
169 |
170 | if (display === 'block' || display === 'list' || domNode.tagName === 'BR') {
171 | text += '\n'
172 | }
173 | }
174 |
175 | return text
176 | }
177 |
--------------------------------------------------------------------------------
/packages/slate-vue-shared/utils/environment.ts:
--------------------------------------------------------------------------------
1 | export const IS_IOS =
2 | typeof navigator !== 'undefined' &&
3 | typeof window !== 'undefined' &&
4 | /iPad|iPhone|iPod/.test(navigator.userAgent) &&
5 | !window.MSStream
6 |
7 | export const IS_APPLE =
8 | typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent)
9 |
10 | export const IS_FIREFOX =
11 | typeof navigator !== 'undefined' &&
12 | /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent)
13 |
14 | export const IS_SAFARI =
15 | typeof navigator !== 'undefined' &&
16 | /Version\/[\d\.]+.*Safari/.test(navigator.userAgent)
17 |
18 | // "modern" Edge was released at 79.x
19 | export const IS_EDGE_LEGACY =
20 | typeof navigator !== 'undefined' &&
21 | /Edge?\/(?:[0-6][0-9]|[0-7][0-8])/i.test(navigator.userAgent)
22 |
--------------------------------------------------------------------------------
/packages/slate-vue-shared/utils/hotkeys.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import { isKeyHotkey } from 'is-hotkey'
3 | import { IS_APPLE } from './environment'
4 |
5 | /**
6 | * Hotkey mappings for each platform.
7 | */
8 |
9 | const HOTKEYS = {
10 | bold: 'mod+b',
11 | compose: ['down', 'left', 'right', 'up', 'backspace', 'enter'],
12 | moveBackward: 'left',
13 | moveForward: 'right',
14 | moveWordBackward: 'ctrl+left',
15 | moveWordForward: 'ctrl+right',
16 | deleteBackward: 'shift?+backspace',
17 | deleteForward: 'shift?+delete',
18 | extendBackward: 'shift+left',
19 | extendForward: 'shift+right',
20 | italic: 'mod+i',
21 | splitBlock: 'shift?+enter',
22 | undo: 'mod+z',
23 | }
24 |
25 | const APPLE_HOTKEYS = {
26 | moveLineBackward: 'opt+up',
27 | moveLineForward: 'opt+down',
28 | moveWordBackward: 'opt+left',
29 | moveWordForward: 'opt+right',
30 | deleteBackward: ['ctrl+backspace', 'ctrl+h'],
31 | deleteForward: ['ctrl+delete', 'ctrl+d'],
32 | deleteLineBackward: 'cmd+shift?+backspace',
33 | deleteLineForward: ['cmd+shift?+delete', 'ctrl+k'],
34 | deleteWordBackward: 'opt+shift?+backspace',
35 | deleteWordForward: 'opt+shift?+delete',
36 | extendLineBackward: 'opt+shift+up',
37 | extendLineForward: 'opt+shift+down',
38 | redo: 'cmd+shift+z',
39 | transposeCharacter: 'ctrl+t',
40 | }
41 |
42 | const WINDOWS_HOTKEYS = {
43 | deleteWordBackward: 'ctrl+shift?+backspace',
44 | deleteWordForward: 'ctrl+shift?+delete',
45 | redo: ['ctrl+y', 'ctrl+shift+z'],
46 | }
47 |
48 | /**
49 | * Create a platform-aware hotkey checker.
50 | */
51 |
52 | const create = (key: string) => {
53 | const generic = (HOTKEYS as any)[key]
54 | const apple = (APPLE_HOTKEYS as any)[key]
55 | const windows = (WINDOWS_HOTKEYS as any)[key]
56 | const isGeneric = generic && isKeyHotkey(generic)
57 | const isApple = apple && isKeyHotkey(apple)
58 | const isWindows = windows && isKeyHotkey(windows)
59 |
60 | return (event: KeyboardEvent) => {
61 | if (isGeneric && isGeneric(event)) return true
62 | if (IS_APPLE && isApple && isApple(event)) return true
63 | if (!IS_APPLE && isWindows && isWindows(event)) return true
64 | return false
65 | }
66 | }
67 |
68 | /**
69 | * Hotkeys.
70 | */
71 |
72 | export default {
73 | isBold: create('bold'),
74 | isCompose: create('compose'),
75 | isMoveBackward: create('moveBackward'),
76 | isMoveForward: create('moveForward'),
77 | isDeleteBackward: create('deleteBackward'),
78 | isDeleteForward: create('deleteForward'),
79 | isDeleteLineBackward: create('deleteLineBackward'),
80 | isDeleteLineForward: create('deleteLineForward'),
81 | isDeleteWordBackward: create('deleteWordBackward'),
82 | isDeleteWordForward: create('deleteWordForward'),
83 | isExtendBackward: create('extendBackward'),
84 | isExtendForward: create('extendForward'),
85 | isExtendLineBackward: create('extendLineBackward'),
86 | isExtendLineForward: create('extendLineForward'),
87 | isItalic: create('italic'),
88 | isMoveLineBackward: create('moveLineBackward'),
89 | isMoveLineForward: create('moveLineForward'),
90 | isMoveWordBackward: create('moveWordBackward'),
91 | isMoveWordForward: create('moveWordForward'),
92 | isRedo: create('redo'),
93 | isSplitBlock: create('splitBlock'),
94 | isTransposeCharacter: create('transposeCharacter'),
95 | isUndo: create('undo'),
96 | }
97 |
--------------------------------------------------------------------------------
/packages/slate-vue-shared/utils/index.ts:
--------------------------------------------------------------------------------
1 | import Hotkeys from './hotkeys'
2 |
3 | export * from './beforeInput'
4 | export * from './dom'
5 | export * from './environment'
6 | export * from './key'
7 | export * from './weak-maps'
8 | export {
9 | Hotkeys
10 | }
--------------------------------------------------------------------------------
/packages/slate-vue-shared/utils/key.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * An auto-incrementing identifier for keys.
3 | */
4 |
5 | let n = 0
6 |
7 | /**
8 | * A class that keeps track of a key string. We use a full class here because we
9 | * want to be able to use them as keys in `WeakMap` objects.
10 | */
11 |
12 | export class Key {
13 | id: string
14 |
15 | constructor() {
16 | this.id = `${n++}`
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/slate-vue-shared/utils/weak-maps.ts:
--------------------------------------------------------------------------------
1 | import { Node, Ancestor, Editor } from 'slate'
2 | import { Key } from './key'
3 | import { VueEditor } from '../plugins';
4 | import { VNode } from 'vue';
5 |
6 | /**
7 | * Two weak maps that allow us rebuild a path given a node. They are populated
8 | * at render time such that after a render occurs we can always backtrack.
9 | */
10 |
11 | export const NODE_TO_INDEX: WeakMap = new WeakMap()
12 | export const NODE_TO_PARENT: WeakMap = new WeakMap()
13 |
14 | /**
15 | * Weak maps that allow us to go between Slate nodes and DOM nodes. These
16 | * are used to resolve DOM event-related logic into Slate actions.
17 | */
18 |
19 | export const EDITOR_TO_ELEMENT: WeakMap = new WeakMap()
20 | export const EDITOR_TO_PLACEHOLDER: WeakMap = new WeakMap()
21 | export const ELEMENT_TO_NODE: WeakMap = new WeakMap()
22 | export const KEY_TO_ELEMENT: WeakMap = new WeakMap()
23 | export const NODE_TO_ELEMENT: WeakMap = new WeakMap()
24 | export const NODE_TO_KEY: WeakMap = new WeakMap()
25 |
26 | /**
27 | * Weak maps for storing editor-related state.
28 | */
29 |
30 | export const IS_READ_ONLY: WeakMap = new WeakMap()
31 | export const IS_FOCUSED: WeakMap = new WeakMap()
32 |
33 | /**
34 | * Weak map for associating the context `onChange` context with the plugin.
35 | */
36 |
37 | export const EDITOR_TO_ON_CHANGE = new WeakMap void>()
38 |
39 | /**
40 | * Symbols.
41 | */
42 |
43 | export const PLACEHOLDER_SYMBOL = (Symbol('placeholder') as unknown) as string
44 |
45 | /**
46 | * vue component
47 | */
48 | export const KEY_TO_VNODE = new Map