UNPKG

edix

Version:

An experimental, framework agnostic, small (~3kB) contenteditable state manager.

1 lines 68.6 kB
{"version":3,"file":"index.mjs","sources":["../src/src/core/types.ts","../src/src/core/position.ts","../src/src/core/commands/edit.ts","../src/src/core/commands/index.ts","../src/src/core/dom/parser.ts","../src/src/core/utils.ts","../src/src/core/dom/index.ts","../src/src/core/editable.ts","../src/src/core/history.ts","../src/src/core/mutation.ts","../src/src/core/schema/plain.ts","../src/src/core/schema/structured.ts"],"sourcesContent":["export const NODE_TEXT = 1;\nexport const NODE_VOID = 2;\nexport type TextNode = Readonly<{\n type: typeof NODE_TEXT;\n text: string;\n}>;\nexport type VoidNode = Readonly<{\n type: typeof NODE_VOID;\n data: Record<string, unknown>;\n}>;\nexport type NodeData = TextNode | VoidNode;\n\nexport type DocFragment = readonly (readonly NodeData[])[];\n\nexport type Position = readonly [line: number, offset: number];\n\nexport type SelectionSnapshot = readonly [anchor: Position, focus: Position];\n\nexport type Writeable<T> = T extends\n | Record<string, unknown>\n | readonly unknown[]\n ? {\n -readonly [key in keyof T]: T[key];\n }\n : T;\n","import type { Position } from \"./types\";\n\n/**\n * @internal\n * 0 : same\n * 1 : A is before B (forward)\n * -1: A is after B (backward)\n */\nexport const compareLine = (\n [lineA]: Position,\n [lineB]: Position\n): 0 | 1 | -1 => {\n if (lineA === lineB) {\n return 0;\n } else {\n return lineA < lineB ? 1 : -1;\n }\n};\n\n/**\n * @internal\n * 0 : same\n * 1 : A is before B (forward)\n * -1: A is after B (backward)\n */\nexport const comparePosition = (posA: Position, posB: Position): 0 | 1 | -1 => {\n const line = compareLine(posA, posB);\n if (line === 0) {\n return posA[1] === posB[1] ? 0 : posA[1] < posB[1] ? 1 : -1;\n } else {\n return line;\n }\n};\n","import { compareLine, comparePosition } from \"../position\";\nimport {\n DocFragment,\n NODE_TEXT,\n NodeData,\n Position,\n SelectionSnapshot,\n Writeable,\n} from \"../types\";\n\nconst isTextNode = (node: NodeData) => node.type === NODE_TEXT;\nconst getNodeSize = (node: NodeData): number =>\n isTextNode(node) ? node.text.length : 1;\n\nconst insertNodeAfter = (line: NodeData[], index: number, node: NodeData) => {\n const target = line[index]!;\n if (isTextNode(node) && isTextNode(target)) {\n line[index] = { type: NODE_TEXT, text: target.text + node.text };\n } else {\n line.splice(index + 1, 0, node);\n }\n};\n\nconst join = (...lines: (readonly NodeData[])[]): readonly NodeData[] => {\n const line: NodeData[] = [];\n for (let i = 0; i < lines.length; i++) {\n const current = lines[i]!;\n if (!line.length) {\n line.push(...current);\n } else {\n for (const node of current) {\n insertNodeAfter(line, line.length - 1, node);\n }\n }\n }\n return line;\n};\n\nconst split = (\n line: readonly NodeData[],\n offset: number\n): [readonly NodeData[], readonly NodeData[]] => {\n for (let i = 0; i < line.length; i++) {\n const node = line[i]!;\n const length = getNodeSize(node);\n if (length > offset) {\n const before = line.slice(0, i);\n const after = line.slice(i + 1);\n if (isTextNode(node)) {\n before.push({ type: NODE_TEXT, text: node.text.slice(0, offset) });\n after.unshift({ type: NODE_TEXT, text: node.text.slice(offset) });\n } else {\n // TODO improve\n if (offset === 0) {\n after.unshift(node);\n } else {\n before.push(node);\n }\n }\n return [before, after];\n }\n offset -= length;\n }\n return [line, []];\n};\n\nconst fixPositionAfterInsert = (\n selectionPos: Position,\n pos: Position,\n lineDiff: number,\n lastRowLength: number\n): Position => {\n return [\n selectionPos[0] + lineDiff,\n selectionPos[1] +\n (compareLine(selectionPos, pos) === 0\n ? lastRowLength - (lineDiff === 0 ? 0 : pos[1])\n : 0),\n ];\n};\n\nconst fixPositionAfterDelete = (\n selectionPos: Position,\n start: Position,\n end: Position\n): Position => {\n return comparePosition(end, selectionPos) === 1\n ? [\n selectionPos[0] - end[0] - start[0],\n selectionPos[1] +\n (compareLine(end, selectionPos) === 0 ? start[1] - end[1] : 0),\n ]\n : start;\n};\n\nconst replaceRange = (\n doc: Writeable<DocFragment>,\n fragment: DocFragment,\n start: Position,\n end?: Position\n) => {\n const [startLine] = start;\n const [endLine] = end || start;\n\n const splitByStart = split(doc[start[0]]!, start[1]);\n const before = splitByStart[0];\n const after = end ? split(doc[end[0]]!, end[1])[1] : splitByStart[1];\n\n const lines: (readonly NodeData[])[] = [...fragment];\n if (lines.length) {\n lines[0] = join(before, lines[0]!);\n lines[lines.length - 1] = join(lines[lines.length - 1]!, after);\n } else {\n lines.push(join(before, after));\n }\n\n doc.splice(startLine, endLine - startLine + 1, ...lines);\n};\n\n/**\n * @internal\n */\nexport const insertEdit = (\n doc: Writeable<DocFragment>,\n selection: Writeable<SelectionSnapshot>,\n lines: DocFragment,\n pos: Position\n) => {\n const [anchor, focus] = selection;\n\n const lineLength = lines.length;\n const lineDiff = lineLength - 1;\n const lastRowLength = lines[lineLength - 1]!.reduce(\n (acc, n) => acc + getNodeSize(n),\n 0\n );\n\n replaceRange(doc, lines, pos);\n\n if (comparePosition(anchor, pos) !== 1) {\n selection[0] = fixPositionAfterInsert(anchor, pos, lineDiff, lastRowLength);\n }\n if (comparePosition(focus, pos) !== 1) {\n selection[1] = fixPositionAfterInsert(focus, pos, lineDiff, lastRowLength);\n }\n};\n\n/**\n * @internal\n */\nexport const deleteEdit = (\n doc: Writeable<DocFragment>,\n selection: Writeable<SelectionSnapshot>,\n start: Position,\n end: Position\n) => {\n const [anchor, focus] = selection;\n\n replaceRange(doc, [], start, end);\n\n if (comparePosition(anchor, start) !== 1) {\n selection[0] = fixPositionAfterDelete(anchor, start, end);\n }\n if (comparePosition(focus, start) !== 1) {\n selection[1] = fixPositionAfterDelete(focus, start, end);\n }\n};\n\n/**\n * @internal\n */\nexport const flatten = (\n doc: DocFragment,\n [[anchorLine, anchorOffset], [focusLine, focusOffset]]: SelectionSnapshot\n): [DocFragment, SelectionSnapshot] => {\n let offsetBeforeAnchor = 0;\n let offsetBeforeFocus = 0;\n\n for (let i = 0; i < doc.length; i++) {\n for (const node of doc[i]!) {\n const length = getNodeSize(node);\n if (i < anchorLine) {\n offsetBeforeAnchor += length;\n }\n if (i < focusLine) {\n offsetBeforeFocus += length;\n }\n }\n }\n\n return [\n [join(...doc)],\n [\n [0, offsetBeforeAnchor + anchorOffset],\n [0, offsetBeforeFocus + focusOffset],\n ],\n ];\n};\n","import {\n NODE_TEXT,\n type DocFragment,\n type Position,\n type SelectionSnapshot,\n type Writeable,\n} from \"../types\";\nimport { comparePosition } from \"../position\";\nimport { deleteEdit, insertEdit } from \"./edit\";\n\nexport type EditableCommand<T extends unknown[]> = (\n doc: Writeable<DocFragment>,\n selection: Writeable<SelectionSnapshot>,\n ...args: T\n) => void;\n\nexport const Delete: EditableCommand<[range?: SelectionSnapshot]> = (\n doc,\n selection,\n [anchor, focus] = selection\n) => {\n const posDiff = comparePosition(anchor, focus);\n if (posDiff !== 0) {\n const backward = posDiff === -1;\n\n deleteEdit(\n doc,\n selection,\n backward ? focus : anchor,\n backward ? anchor : focus\n );\n }\n};\n\n/**\n * @internal\n */\nexport const InsertFragment: EditableCommand<[lines: DocFragment]> = (\n doc,\n selection,\n lines\n) => {\n Delete(doc, selection);\n\n insertEdit(\n doc,\n selection,\n lines,\n // selection was collapsed with delete command\n selection[0]\n );\n};\n\nexport const InsertText: EditableCommand<[text: string]> = (\n doc,\n selection,\n text\n) => {\n InsertFragment(\n doc,\n selection,\n text.split(\"\\n\").map((l) => [{ type: NODE_TEXT, text: l }])\n );\n};\n\n/**\n * @internal\n */\nexport const MoveToPosition: EditableCommand<[position: Position]> = (\n _doc,\n selection,\n position\n) => {\n selection[0] = selection[1] = position;\n};\n","let walker: TreeWalker | null;\nlet node: Node | null;\nlet nodeType: NodeType | null;\nlet isBrDetected = false;\nlet isBlockNode: (node: Element) => boolean;\n\n/**\n * @internal\n */\nexport interface ParserConfig {\n _isBlock?: (node: Element) => boolean;\n}\n\nconst SHOW_ELEMENT = 0x1;\nconst SHOW_TEXT = 0x4;\n\n/** @internal */\nexport const TYPE_TEXT = 1;\n/** @internal */\nexport const TYPE_VOID = 2;\n/** @internal */\nexport const TYPE_SOFT_BREAK = 3;\n/** @internal */\nexport const TYPE_HARD_BREAK = 4;\n/** @internal */\nexport const TYPE_EMPTY_BLOCK_ANCHOR = 5;\n\n/**\n * @internal\n */\nexport type NodeType =\n | typeof TYPE_TEXT\n | typeof TYPE_VOID\n | typeof TYPE_SOFT_BREAK\n | typeof TYPE_HARD_BREAK\n | typeof TYPE_EMPTY_BLOCK_ANCHOR;\n\nconst ELEMENT_NODE = 1;\nconst TEXT_NODE = 3;\nconst COMMENT_NODE = 8;\n\n/**\n * @internal\n */\nexport const isTextNode = (node: Node): node is Text => {\n return node.nodeType === TEXT_NODE;\n};\n\n/**\n * @internal\n */\nexport const isElementNode = (node: Node): node is Element => {\n return node.nodeType === ELEMENT_NODE;\n};\n\n/**\n * @internal\n */\nexport const isCommentNode = (node: Node): node is Comment => {\n return node.nodeType === COMMENT_NODE;\n};\n\nconst SINGLE_LINE_CONTAINER_NAMES = new Set([\n // https://w3c.github.io/editing/docs/execCommand/#single-line-container\n // non-list single-line container\n \"DIV\",\n \"H1\",\n \"H2\",\n \"H3\",\n \"H4\",\n \"H5\",\n \"H6\",\n \"P\",\n \"PRE\",\n // list single-line container\n \"LI\",\n \"DT\",\n \"DD\",\n\n // other elements for HTML paste\n \"TR\",\n]);\n\n// https://developer.mozilla.org/en-US/docs/Web/HTML/Content_categories\n// https://html.spec.whatwg.org/multipage/dom.html#embedded-content-category\nconst EMBEDDED_CONTENT_TAG_NAMES = new Set([\n \"EMBED\",\n \"IMG\",\n \"PICTURE\",\n \"AUDIO\",\n \"VIDEO\",\n \"SVG\",\n \"CANVAS\",\n \"MATH\",\n \"IFRAME\",\n \"OBJECT\",\n]);\n\nconst defaultIsBlockNode = (node: Element): boolean => {\n return SINGLE_LINE_CONTAINER_NAMES.has(node.tagName);\n};\n\n/**\n * @internal\n */\nexport const getDomNode = <\n T extends NodeType | void\n>(): T extends typeof TYPE_TEXT\n ? Text\n : T extends NodeType\n ? Element\n : Text | Element => {\n return node as any;\n};\n\n/**\n * @internal\n */\nexport const getNodeSize = (): number => {\n return nodeType === TYPE_TEXT\n ? (node as Text).data.length\n : nodeType === TYPE_VOID\n ? 1\n : 0;\n};\n\nconst isValidSoftBreak = (node: Node): boolean => {\n const next = node.nextSibling;\n\n // This function will return false if there are no nodes after soft break.\n //\n // In contenteditable, Shift+Enter will insert soft break. \\n in Chrome, <br/> in Firefox. Safari doesn't insert soft break.\n // And \\n or <br/> has a special role that represents empty block in contenteditable.\n // We have to distinguish real soft breaks from empty blocks.\n //\n // There are many possible markups for soft break ([] means text node):\n // <div>[\\n][abc]</div> Shift+Enter at start of line in Chrome\n // <div><br/>[abc]</div> Shift+Enter at start of line in Firefox\n // <div>[ab][\\n][c]</div> Shift+Enter at mid of line in Chrome\n // <div>[ab]<br/>[c]</div> Shift+Enter at mid of line in Firefox\n // <div>[abc][\\n][\\n]</div> Shift+Enter at end of line in Chrome\n // <div>[abc]<br/><br/></div> Shift+Enter at end of line in Firefox\n // <div>[\\n]<br/></div> Shift+Enter at empty line in Chrome\n // <div><br/><br/></div> Shift+Enter at empty line in Firefox\n //\n // And these do not include soft breaks:\n // <div><br/></div> empty line\n // <div>[a]<br/></div> type on empty line in Firefox\n return (\n !!next &&\n // svelte/angular may have comment node\n !isCommentNode(next)\n );\n};\n\nconst readNext = (endNode?: Node): NodeType | void => {\n while (true) {\n if (nodeType === TYPE_VOID) {\n const current = node!;\n // don't use TreeWalker.nextSibling() to support case like <body><p><a><img /></a></p><p>hello</p></body>\n while ((node = walker!.nextNode())) {\n if (!current.contains(node)) {\n break;\n }\n }\n } else {\n node = walker!.nextNode();\n }\n\n nodeType = null;\n\n if (!node || (endNode && node === endNode)) {\n break;\n }\n\n if (isTextNode(node)) {\n // Especially Shift+Enter in Chrome\n if (node.data === \"\\n\") {\n if (isValidSoftBreak(node)) {\n return (nodeType = TYPE_SOFT_BREAK);\n }\n } else {\n return (nodeType = TYPE_TEXT);\n }\n } else if (isElementNode(node)) {\n const tagName = node.tagName;\n if (tagName === \"BR\") {\n const isBr = isBrDetected;\n isBrDetected = true;\n // Especially Shift+Enter in Firefox\n if (isValidSoftBreak(node)) {\n return (nodeType = TYPE_SOFT_BREAK);\n } else {\n if (!isBr) {\n // Returning <div><br/></div> is necessary to anchor selection\n return (nodeType = TYPE_EMPTY_BLOCK_ANCHOR);\n }\n }\n } else if (\n (node as HTMLElement).contentEditable === \"false\" ||\n EMBEDDED_CONTENT_TAG_NAMES.has(tagName)\n ) {\n return (nodeType = TYPE_VOID);\n } else if (isBlockNode(node)) {\n const prev = node.previousElementSibling;\n if (prev && isBlockNode(prev)) {\n return (nodeType = TYPE_HARD_BREAK);\n }\n }\n }\n }\n};\n\n/**\n * @internal\n */\nexport const parse = <T>(\n scopeFn: (read: typeof readNext) => T,\n document: Document,\n root: Node,\n config: ParserConfig\n): T => {\n try {\n isBlockNode = config._isBlock || defaultIsBlockNode;\n\n walker = document.createTreeWalker(root, SHOW_TEXT | SHOW_ELEMENT);\n\n return scopeFn(readNext);\n } finally {\n walker = node = nodeType = null;\n isBrDetected = false;\n }\n};\n","/**\n * @internal\n */\nexport const { min } = Math;\n\n/**\n * @internal\n */\nexport const microtask: (fn: () => void) => void =\n typeof queueMicrotask === \"function\"\n ? queueMicrotask\n : (fn) => {\n Promise.resolve().then(fn);\n };\n","import {\n NodeType,\n parse,\n getNodeSize,\n getDomNode,\n isElementNode,\n TYPE_TEXT,\n TYPE_VOID,\n TYPE_SOFT_BREAK,\n TYPE_HARD_BREAK,\n ParserConfig,\n} from \"./parser\";\nimport { comparePosition } from \"../position\";\nimport {\n DocFragment,\n Position,\n NodeData,\n SelectionSnapshot,\n NODE_TEXT,\n NODE_VOID,\n} from \"../types\";\nimport { min } from \"../utils\";\n\n// const DOCUMENT_POSITION_DISCONNECTED = 0x01;\nconst DOCUMENT_POSITION_PRECEDING = 0x02;\n// const DOCUMENT_POSITION_FOLLOWING = 0x04;\n// const DOCUMENT_POSITION_CONTAINS = 0x08;\n// const DOCUMENT_POSITION_CONTAINED_BY = 0x10;\n// const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20;\n\n/**\n * @internal\n */\nexport const getCurrentDocument = (node: Element): Document =>\n node.ownerDocument;\n\nconst getDOMSelection = (element: Element): Selection => {\n // TODO support ShadowRoot\n return getCurrentDocument(element).getSelection()!;\n};\n\nconst getSelectionRangeInEditor = (\n selection: Selection,\n root: Element\n): Range | void => {\n if (selection.rangeCount) {\n const range = selection.getRangeAt(0);\n if (root.contains(range.commonAncestorContainer)) {\n return range;\n }\n }\n};\n\nconst setRangeToSelection = (\n root: Element,\n range: Range,\n backward?: boolean\n) => {\n const selection = getDOMSelection(root);\n selection.removeAllRanges();\n selection.addRange(range);\n if (backward) {\n selection.collapseToEnd();\n selection.extend(range.startContainer, range.startOffset);\n }\n};\n\n/**\n * @internal\n */\nexport const setSelectionToDOM = (\n document: Document,\n root: Element,\n [anchor, focus]: SelectionSnapshot,\n isSingleline: boolean,\n config: ParserConfig\n): boolean => {\n const posDiff = comparePosition(anchor, focus);\n const isCollapsed = posDiff === 0;\n const backward = posDiff === -1;\n const start = backward ? focus : anchor;\n const end = backward ? anchor : focus;\n // special path for empty content with empty selection, necessary for placeholder\n if (\n start[0] === 0 &&\n start[1] === 0 &&\n isCollapsed &&\n !root.hasChildNodes()\n ) {\n const range = document.createRange();\n range.setStart(root, 0);\n range.setEnd(root, 0);\n\n setRangeToSelection(root, range);\n return true;\n }\n\n const domStart = findPosition(document, root, start, isSingleline, config);\n if (!domStart) {\n return false;\n }\n\n const domEnd = isCollapsed\n ? domStart\n : findPosition(document, root, end, isSingleline, config);\n if (!domEnd) {\n return false;\n }\n\n // https://w3c.github.io/contentEditable/#dfn-legal-caret-positions\n const range = document.createRange();\n {\n const [node, offset] = domStart;\n // embed or br\n if (isElementNode(node)) {\n if (offset < 1) {\n range.setStartBefore(node);\n } else {\n range.setStartAfter(node);\n }\n } else {\n range.setStart(node, min(offset, node.data.length));\n }\n }\n {\n const [node, offset] = domEnd;\n // embed or br\n if (isElementNode(node)) {\n if (offset < 1) {\n range.setEndBefore(node);\n } else {\n range.setEndAfter(node);\n }\n } else {\n range.setEnd(node, min(offset, node.data.length));\n }\n }\n\n setRangeToSelection(root, range, backward);\n return true;\n};\n\ntype DOMPosition = [node: Text | Element, offsetAtNode: number];\n\nconst findPosition = (\n document: Document,\n root: Element,\n [line, offset]: Position,\n isSingleline: boolean,\n config: ParserConfig\n): DOMPosition | void => {\n return parse(\n (readNext): DOMPosition | void => {\n while (readNext()) {\n const length = getNodeSize();\n if (offset <= length) {\n return [getDomNode(), offset];\n }\n offset -= length;\n }\n },\n document,\n isSingleline || root.childElementCount === 0 ? root : root.children[line]!,\n config\n );\n};\n\nconst serializePosition = (\n document: Document,\n root: Element,\n targetNode: Node,\n offsetAtNode: number,\n isSingleline: boolean,\n config: ParserConfig\n): Position => {\n let row: Node = targetNode;\n let lineIndex: number;\n if (isSingleline || root.childElementCount === 0) {\n row = root;\n lineIndex = 0;\n } else {\n while (true) {\n const parent = row.parentElement!;\n if (parent === root) {\n break;\n }\n row = parent;\n }\n lineIndex = Array.prototype.indexOf.call(root.children, row);\n }\n\n if (isElementNode(targetNode)) {\n // If anchor/focus of selection is not selectable node, it will have offset relative to its parent\n // 0 1 2 3\n // <div>aaaa<img /><span>bbbb</span></div>\n targetNode = targetNode.childNodes[offsetAtNode]!;\n offsetAtNode = 0;\n }\n\n return parse(\n (readNext) => {\n let type: NodeType | void;\n let offset = 0;\n\n while ((type = readNext(targetNode))) {\n if (type === TYPE_SOFT_BREAK) {\n lineIndex++;\n offset = 0;\n } else {\n offset += getNodeSize();\n }\n }\n return [lineIndex, offset + offsetAtNode];\n },\n document,\n row,\n config\n );\n};\n\n/**\n * @internal\n */\nexport const getEmptySelectionSnapshot = (): SelectionSnapshot => {\n return [\n [0, 0],\n [0, 0],\n ];\n};\n\n/**\n * @internal\n */\nexport const takeSelectionSnapshot = (\n document: Document,\n root: Element,\n isSingleline: boolean,\n config: ParserConfig\n): SelectionSnapshot => {\n const selection = getDOMSelection(root);\n const range = getSelectionRangeInEditor(selection, root);\n if (!range) {\n return getEmptySelectionSnapshot();\n }\n\n const { startOffset, startContainer, endOffset, endContainer } = range;\n\n // https://stackoverflow.com/questions/9180405/detect-direction-of-user-selection-with-javascript\n const backward =\n startContainer === endContainer\n ? selection.anchorOffset > selection.focusOffset\n : (selection.anchorNode!.compareDocumentPosition(selection.focusNode!) &\n DOCUMENT_POSITION_PRECEDING) !==\n 0;\n\n let start: Position;\n let end: Position;\n if (root === startContainer && !isSingleline) {\n if (\n startOffset === 0 &&\n endOffset !== 0 &&\n root.children.length <= endOffset\n ) {\n // special case for Ctrl+A in firefox\n start = [0, 0];\n end = serializePosition(\n document,\n root,\n root.lastElementChild!,\n root.lastElementChild!.textContent!.length,\n isSingleline,\n config\n );\n } else {\n return getEmptySelectionSnapshot();\n }\n } else {\n start = serializePosition(\n document,\n root,\n startContainer,\n startOffset,\n isSingleline,\n config\n );\n end = selection.isCollapsed\n ? start\n : serializePosition(\n document,\n root,\n endContainer,\n endOffset,\n isSingleline,\n config\n );\n }\n\n return [backward ? end : start, backward ? start : end];\n};\n\n/**\n * @internal\n */\nexport const takeDomSnapshot = (\n document: Document,\n root: Node,\n config: ParserConfig,\n serializeVoid: (node: Element) => Record<string, unknown> | void\n): DocFragment => {\n return parse(\n (readNext) => {\n let type: NodeType | void;\n let row: NodeData[] | null = null;\n let text = \"\";\n\n const rows: NodeData[][] = [];\n\n const completeNode = (element?: Element) => {\n if (!row) {\n row = [];\n }\n if (text) {\n row.push({ type: NODE_TEXT, text });\n text = \"\";\n }\n if (element) {\n const data = serializeVoid(element);\n if (data) {\n row.push({ type: NODE_VOID, data });\n }\n }\n };\n const completeRow = () => {\n completeNode();\n if (row) {\n rows.push(row);\n }\n row = null;\n };\n\n while ((type = readNext())) {\n if (type === TYPE_TEXT) {\n text += getDomNode<typeof type>().data;\n } else if (type === TYPE_VOID) {\n completeNode(getDomNode<typeof type>());\n } else if (type === TYPE_SOFT_BREAK || type === TYPE_HARD_BREAK) {\n completeRow();\n }\n }\n completeRow();\n\n return rows;\n },\n document,\n root,\n config\n );\n};\n\n/**\n * @internal\n */\nexport const getSelectedElements = (root: Element): Node | undefined => {\n const range = getSelectionRangeInEditor(getDOMSelection(root), root);\n if (!range) return;\n return range.cloneContents();\n};\n\n/**\n * @internal\n */\nexport const getPointedCaretPosition = (\n document: Document,\n root: Element,\n { clientX, clientY }: MouseEvent,\n isSingleline: boolean,\n config: ParserConfig\n): Position | void => {\n // https://developer.mozilla.org/en-US/docs/Web/API/Document/caretPositionFromPoint\n // https://developer.mozilla.org/en-US/docs/Web/API/Document/caretRangeFromPoint\n // caretPositionFromPoint caretRangeFromPoint\n // Chrome: 128 4\n // Firefox: 20 -\n // Safari: - 5\n if (document.caretPositionFromPoint) {\n const position = document.caretPositionFromPoint(clientX, clientY);\n if (position) {\n return serializePosition(\n document,\n root,\n position.offsetNode,\n position.offset,\n isSingleline,\n config\n );\n }\n } else if (document.caretRangeFromPoint) {\n const range = document.caretRangeFromPoint(clientX, clientY);\n if (range) {\n return serializePosition(\n document,\n root,\n range.startContainer,\n range.startOffset,\n isSingleline,\n config\n );\n }\n }\n};\n","import { createHistory } from \"./history\";\nimport {\n getCurrentDocument,\n takeSelectionSnapshot,\n setSelectionToDOM,\n takeDomSnapshot,\n getEmptySelectionSnapshot,\n getSelectedElements,\n getPointedCaretPosition,\n} from \"./dom\";\nimport { createMutationObserver } from \"./mutation\";\nimport { DocFragment, SelectionSnapshot, Writeable } from \"./types\";\nimport { microtask } from \"./utils\";\nimport {\n Delete,\n EditableCommand,\n InsertText,\n InsertFragment,\n MoveToPosition,\n} from \"./commands\";\nimport { flatten } from \"./commands/edit\";\nimport { EditableSchema } from \"./schema\";\nimport { ParserConfig } from \"./dom/parser\";\n\n/**\n * https://www.w3.org/TR/input-events-1/#interface-InputEvent-Attributes\n */\ntype InputType =\n | \"insertText\" // insert typed plain text\n | \"insertReplacementText\" // replace existing text by means of a spell checker, auto-correct or similar\n | \"insertLineBreak\" // insert a line break\n | \"insertParagraph\" // insert a paragraph break\n | \"insertOrderedList\" // insert a numbered list\n | \"insertUnorderedList\" // insert a bulleted list\n | \"insertHorizontalRule\" // insert a horizontal rule\n | \"insertFromYank\" // replace the current selection with content stored in a kill buffer\n | \"insertFromDrop\" // insert content into the DOM by means of drop\n | \"insertFromPaste\" // paste\n | \"insertFromPasteAsQuotation\" // paste content as a quotation\n | \"insertTranspose\" // transpose the last two characters that were entered\n | \"insertCompositionText\" // replace the current composition string\n | \"insertLink\" // insert a link\n | \"deleteWordBackward\" // delete a word directly before the caret position\n | \"deleteWordForward\" // delete a word directly after the caret position\n | \"deleteSoftLineBackward\" // delete from the caret to the nearest visual line break before the caret position\n | \"deleteSoftLineForward\" // delete from the caret to the nearest visual line break after the caret position\n | \"deleteEntireSoftLine\" // delete from to the nearest visual line break before the caret position to the nearest visual line break after the caret position\n | \"deleteHardLineBackward\" // delete from the caret to the nearest beginning of a block element or br element before the caret position\n | \"deleteHardLineForward\" // delete from the caret to the nearest end of a block element or br element after the caret position\n | \"deleteByDrag\" // remove content from the DOM by means of drag\n | \"deleteByCut\" // remove the current selection as part of a cut\n | \"deleteContent\" // delete the selection without specifying the direction of the deletion and this intention is not covered by another inputType\n | \"deleteContentBackward\" // delete the content directly before the caret position and this intention is not covered by another inputType or delete the selection with the selection collapsing to its start after the deletion\n | \"deleteContentForward\" // delete the content directly after the caret position and this intention is not covered by another inputType or delete the selection with the selection collapsing to its end after the deletion\n | \"historyUndo\" // undo the last editing action\n | \"historyRedo\" // to redo the last undone editing action\n | \"formatBold\" // initiate bold text\n | \"formatItalic\" // initiate italic text\n | \"formatUnderline\" // initiate underline text\n | \"formatStrikeThrough\" // initiate stricken through text\n | \"formatSuperscript\" // initiate superscript text\n | \"formatSubscript\" // initiate subscript text\n | \"formatJustifyFull\" // make the current selection fully justified\n | \"formatJustifyCenter\" // center align the current selection\n | \"formatJustifyRight\" // right align the current selection\n | \"formatJustifyLeft\" // left align the current selection\n | \"formatIndent\" // indent the current selection\n | \"formatOutdent\" // outdent the current selection\n | \"formatRemove\" // remove all formatting from the current selection\n | \"formatSetBlockTextDirection\" // set the text block direction\n | \"formatSetInlineTextDirection\" // set the text inline direction\n | \"formatBackColor\" // change the background color\n | \"formatFontColor\" // change the font color\n | \"formatFontName\"; // change the font-family\n\n/**\n * Options of {@link editable}.\n */\nexport interface EditableOptions<T> {\n /**\n * TODO\n */\n schema: EditableSchema<T>;\n /**\n * TODO\n */\n isBlock?: (node: HTMLElement) => boolean;\n /**\n * TODO\n */\n onChange: (value: T) => void;\n}\n\n/**\n * Methods of editor instance.\n */\nexport interface EditableHandle {\n /**\n * Disposes editor and restores previous DOM state.\n */\n dispose: () => void;\n /**\n * Dispatches editing command.\n * @param fn command function\n * @param args arguments of command\n */\n command: <A extends unknown[]>(fn: EditableCommand<A>, ...args: A) => void;\n /**\n * Changes editor's read-only state.\n * @param value `true` to read-only. `false` to editable.\n */\n readonly: (value: boolean) => void;\n}\n\n/**\n * A function to make DOM editable.\n */\nexport const editable = <T>(\n element: HTMLElement,\n {\n schema: {\n single: isSingleline,\n js: docToJS,\n void: serializeVoid,\n copy,\n paste: getPastableData,\n },\n isBlock,\n onChange,\n }: EditableOptions<T>\n): EditableHandle => {\n // https://w3c.github.io/contentEditable/\n // https://w3c.github.io/editing/docs/execCommand/\n // https://w3c.github.io/selection-api/\n const {\n contentEditable: prevContentEditable,\n role: prevRole,\n ariaMultiLine: prevAriaMultiLine,\n ariaReadOnly: prevAriaReadOnly,\n } = element;\n const prevWhiteSpace = element.style.whiteSpace;\n\n element.role = \"textbox\";\n // https://html.spec.whatwg.org/multipage/interaction.html#best-practices-for-in-page-editors\n element.style.whiteSpace = \"pre-wrap\";\n if (!isSingleline) {\n element.ariaMultiLine = \"true\";\n }\n\n let readonly = false;\n let disposed = false;\n let selectionReverted = false;\n let currentSelection: SelectionSnapshot = getEmptySelectionSnapshot();\n let restoreSelectionQueue: ReturnType<typeof setTimeout> | null = null;\n let isComposing = false;\n let hasFocus = false;\n let isDragging = false;\n\n const parserConfig: ParserConfig = {\n _isBlock: isBlock as ParserConfig[\"_isBlock\"],\n };\n\n const setContentEditable = () => {\n element.contentEditable = readonly ? \"false\" : \"true\";\n element.ariaReadOnly = readonly ? \"true\" : null;\n };\n\n setContentEditable();\n\n const commands: [EditableCommand<any[]>, args: unknown[]][] = [];\n\n const document = getCurrentDocument(element);\n\n const history = createHistory<\n readonly [doc: DocFragment, selection: SelectionSnapshot]\n >([\n takeDomSnapshot(document, element, parserConfig, serializeVoid),\n currentSelection,\n ]);\n\n const observer = createMutationObserver(element, () => {\n if (hasFocus) {\n // Mutation to selected DOM may change selection, so restore it.\n setSelectionToDOM(\n document,\n element,\n currentSelection,\n isSingleline,\n parserConfig\n );\n if (restoreSelectionQueue != null) {\n clearTimeout(restoreSelectionQueue);\n restoreSelectionQueue = null;\n }\n }\n });\n\n const tasks = new Set<() => void>();\n\n const queueTask = (fn: () => void) => {\n if (!tasks.has(fn)) {\n tasks.add(fn);\n\n microtask(() => {\n tasks.delete(fn);\n fn();\n });\n }\n };\n\n const restoreSelectionOnTimeout = () => {\n // We set updated selection after the next rerender, because it will modify DOM and selection again.\n // However frameworks may not rerender for optimization in some case, for example if selection is updated but document is the same.\n // So we also schedule restoring on timeout for safe.\n const nextSelection = currentSelection;\n restoreSelectionQueue = setTimeout(() => {\n setSelectionToDOM(\n document,\n element,\n nextSelection,\n isSingleline,\n parserConfig\n );\n });\n };\n\n const updateState = (\n doc: DocFragment,\n selection: SelectionSnapshot,\n prevSelection: SelectionSnapshot\n ) => {\n if (!readonly) {\n if (isSingleline) {\n [doc, selection] = flatten(doc, selection);\n }\n\n currentSelection = selection;\n\n // TODO improve\n const prevDoc = history.get()[0];\n if (\n doc.length !== prevDoc.length ||\n doc.some((l, i) => l !== prevDoc[i])\n ) {\n history.set([prevDoc, prevSelection]);\n history.push([doc, selection]);\n onChange(docToJS(doc));\n }\n }\n\n restoreSelectionOnTimeout();\n };\n\n const syncSelection = () => {\n currentSelection = takeSelectionSnapshot(\n document,\n element,\n isSingleline,\n parserConfig\n );\n };\n\n const flushInput = () => {\n const queue = observer._flush();\n\n observer._accept(false);\n if (queue.length) {\n // Get current document and selection from DOM\n const selection = takeSelectionSnapshot(\n document,\n element,\n isSingleline,\n parserConfig\n );\n const doc = takeDomSnapshot(\n document,\n element,\n parserConfig,\n serializeVoid\n );\n\n // Revert DOM\n let m: MutationRecord | undefined;\n while ((m = queue.pop())) {\n if (m.type === \"childList\") {\n const { target, removedNodes, addedNodes, nextSibling } = m;\n for (let i = removedNodes.length - 1; i >= 0; i--) {\n target.insertBefore(removedNodes[i]!, nextSibling);\n }\n for (let i = addedNodes.length - 1; i >= 0; i--) {\n target.removeChild(addedNodes[i]!);\n }\n } else {\n (m.target as CharacterData).nodeValue = m.oldValue!;\n }\n }\n observer._flush();\n\n // Restore previous selection\n // Updating selection may schedule the next selectionchange event\n // It should be ignored especially in firefox not to confuse editor state\n selectionReverted = setSelectionToDOM(\n document,\n element,\n currentSelection,\n isSingleline,\n parserConfig\n );\n\n updateState(doc, selection, currentSelection);\n }\n };\n\n const flushCommand = () => {\n if (commands.length) {\n const selection: Writeable<SelectionSnapshot> = [...currentSelection];\n const doc: Writeable<DocFragment> = [...history.get()[0]];\n\n let command: (typeof commands)[number] | undefined;\n while ((command = commands.pop())) {\n command[0](doc, selection, ...command[1]);\n }\n updateState(doc, selection, currentSelection);\n }\n };\n\n const execCommand: EditableHandle[\"command\"] = (fn, ...args) => {\n commands.unshift([fn, args]);\n\n queueTask(flushCommand);\n };\n\n const onKeyDown = (e: KeyboardEvent) => {\n if (isComposing) return;\n\n if ((e.metaKey || e.ctrlKey) && !e.altKey && e.code === \"KeyZ\") {\n e.preventDefault();\n\n observer._accept(false);\n if (!readonly) {\n const nextHistory = e.shiftKey ? history.redo() : history.undo();\n\n if (nextHistory) {\n currentSelection = nextHistory[1];\n onChange(docToJS(nextHistory[0]));\n\n restoreSelectionOnTimeout();\n }\n }\n }\n };\n\n const onInput = (() => {\n if (isComposing) return;\n queueTask(flushInput);\n }) as (e: Event) => void;\n const onBeforeInput = (e: InputEvent) => {\n switch (e.inputType as InputType) {\n case \"historyUndo\": {\n e.preventDefault();\n return;\n }\n case \"historyRedo\": {\n e.preventDefault();\n return;\n }\n case \"insertLineBreak\":\n case \"insertParagraph\": {\n if (isSingleline) {\n e.preventDefault();\n return;\n }\n }\n }\n\n observer._accept(true);\n };\n const onCompositionStart = () => {\n isComposing = true;\n };\n const onCompositionEnd = () => {\n isComposing = false;\n queueTask(flushInput);\n };\n\n const onFocus = () => {\n hasFocus = true;\n syncSelection();\n };\n const onBlur = () => {\n hasFocus = false;\n };\n\n const onSelectionChange = () => {\n if (selectionReverted) {\n selectionReverted = false;\n return;\n }\n // Safari may dispatch selectionchange event after dragstart\n if (hasFocus && !isComposing && !isDragging) {\n syncSelection();\n }\n };\n\n const copySelectedDOM = (dataTransfer: DataTransfer) => {\n const selected = getSelectedElements(element);\n if (!selected) return;\n\n copy(\n dataTransfer,\n takeDomSnapshot(document, selected, parserConfig, serializeVoid),\n selected\n );\n };\n\n const insertData = (dataTransfer: DataTransfer) => {\n const data = getPastableData(dataTransfer);\n if (typeof data === \"string\") {\n execCommand(InsertText, data);\n } else {\n execCommand(\n InsertFragment,\n takeDomSnapshot(document, data, parserConfig, serializeVoid)\n );\n }\n };\n\n const onCopy = (e: ClipboardEvent) => {\n e.preventDefault();\n copySelectedDOM(e.clipboardData!);\n };\n const onCut = (e: ClipboardEvent) => {\n e.preventDefault();\n if (!readonly) {\n copySelectedDOM(e.clipboardData!);\n execCommand(Delete);\n }\n };\n const onPaste = (e: ClipboardEvent) => {\n e.preventDefault();\n insertData(e.clipboardData!);\n };\n\n const onDrop = (e: DragEvent) => {\n e.preventDefault();\n\n const dataTransfer = e.dataTransfer;\n const droppedPosition = getPointedCaretPosition(\n document,\n element,\n e,\n isSingleline,\n parserConfig\n );\n if (dataTransfer && droppedPosition) {\n // move selection first to keep selection after modifications\n execCommand(MoveToPosition, droppedPosition);\n if (isDragging) {\n execCommand(Delete, currentSelection);\n } else {\n element.focus();\n }\n insertData(dataTransfer);\n }\n };\n const onDragStart = (e: DragEvent) => {\n isDragging = true;\n copySelectedDOM(e.dataTransfer!);\n };\n const onDragEnd = () => {\n isDragging = false;\n };\n\n document.addEventListener(\"selectionchange\", onSelectionChange);\n element.addEventListener(\"keydown\", onKeyDown);\n element.addEventListener(\"input\", onInput);\n element.addEventListener(\"beforeinput\", onBeforeInput);\n element.addEventListener(\"compositionstart\", onCompositionStart);\n element.addEventListener(\"compositionend\", onCompositionEnd);\n element.addEventListener(\"focus\", onFocus);\n element.addEventListener(\"blur\", onBlur);\n element.addEventListener(\"copy\", onCopy);\n element.addEventListener(\"cut\", onCut);\n element.addEventListener(\"paste\", onPaste);\n element.addEventListener(\"drop\", onDrop);\n element.addEventListener(\"dragstart\", onDragStart);\n element.addEventListener(\"dragend\", onDragEnd);\n\n return {\n dispose: () => {\n if (disposed) return;\n disposed = true;\n\n element.contentEditable = prevContentEditable;\n element.role = prevRole;\n element.ariaMultiLine = prevAriaMultiLine;\n element.ariaReadOnly = prevAriaReadOnly;\n element.style.whiteSpace = prevWhiteSpace;\n\n observer._dispose();\n\n document.removeEventListener(\"selectionchange\", onSelectionChange);\n element.removeEventListener(\"keydown\", onKeyDown);\n element.removeEventListener(\"input\", onInput);\n element.removeEventListener(\"beforeinput\", onBeforeInput);\n element.removeEventListener(\"compositionstart\", onCompositionStart);\n element.removeEventListener(\"compositionend\", onCompositionEnd);\n element.removeEventListener(\"focus\", onFocus);\n element.removeEventListener(\"blur\", onBlur);\n element.removeEventListener(\"copy\", onCopy);\n element.removeEventListener(\"cut\", onCut);\n element.removeEventListener(\"paste\", onPaste);\n element.removeEventListener(\"drop\", onDrop);\n element.removeEventListener(\"dragstart\", onDragStart);\n element.removeEventListener(\"dragend\", onDragEnd);\n },\n command: execCommand,\n readonly: (value) => {\n readonly = value;\n setContentEditable();\n },\n };\n};\n","const MAX_HISTORY_LENGTH = 500;\nconst BATCH_HISTORY_TIME = 500;\n\n/**\n * @internal\n */\nexport const createHistory = <T>(initialValue: T) => {\n let index = 0;\n let prevTime = 0;\n const now = Date.now;\n const histories: T[] = [initialValue];\n\n const get = () => histories[index]!;\n\n const set = (history: T) => {\n histories[index] = history;\n };\n\n const push = (history: T) => {\n const time = now();\n if (index !== 0 && time - prevTime < BATCH_HISTORY_TIME) {\n index--;\n }\n prevTime = time;\n\n histories[++index] = history;\n histories.splice(index + 1);\n if (index > MAX_HISTORY_LENGTH) {\n index--;\n histories.shift();\n }\n };\n\n const isUndoable = (): boolean => {\n return index > 0;\n };\n\n const isRedoable = (): boolean => {\n return index < histories.length - 1;\n };\n\n const undo = (): T | undefined => {\n if (isUndoable()) {\n index--;\n return get();\n } else {\n return;\n }\n };\n\n const redo = (): T | undefined => {\n if (isRedoable()) {\n index++;\n return get();\n } else {\n return;\n }\n };\n\n return {\n get,\n set,\n undo,\n redo,\n push,\n };\n};\n","/**\n * @internal\n */\nexport const createMutationObserver = (\n element: Element,\n onMutationIgnored: () => void\n) => {\n let isInputing = false;\n\n const queue: MutationRecord[] = [];\n const process = (records: MutationRecord[]) => {\n if (isInputing) {\n queue.push(...records);\n }\n };\n // https://dom.spec.whatwg.org/#interface-mutationobserver\n const mo = new MutationObserver((records) => {\n process(records);\n if (!isInputing) {\n onMutationIgnored();\n }\n });\n\n const sync = () => {\n process(mo.takeRecords());\n };\n\n mo.observe(element, {\n characterData: true,\n characterDataOldValue: true,\n childList: true,\n subtree: true,\n });\n\n return {\n _accept(enable: boolean) {\n if (!isInputing && enable) {\n sync();\n }\n isInputing = enable;\n },\n _flush: (): MutationRecord[] => {\n sync();\n return queue.splice(0);\n },\n _dispose() {\n queue.splice(0);\n mo.disconnect();\n },\n };\n};\n","import { NODE_TEXT, type DocFragment } from \"../types\";\nimport type { EditableSchema } from \"./types\";\n\nconst toString = (doc: DocFragment): string => {\n return doc.reduce((acc, r, i) => {\n if (i !== 0) {\n acc += \"\\n\";\n }\n return (\n acc + r.reduce((acc, n) => acc + (n.type === NODE_TEXT ? n.text : \"\"), \"\")\n );\n }, \"\");\n};\n\n/**\n * Defines plain text schema.\n */\nexport const plainSchema = ({\n multiline,\n}: {\n multiline?: boolean;\n} = {}): EditableSchema<string> => {\n return {\n single: !multiline,\n js: toString,\n void: () => {}, // not supported\n copy: (dataTransfer, data) => {\n dataTransfer.setData(\"text/plain\", toString(data));\n },\n paste: (dataTransfer) => {\n return dataTransfer.getData(\"text/plain\");\n },\n };\n};\n","import { isCommentNode } from \"../dom/parser\";\nimport { NODE_TEXT, TextNode, type NodeData } from \"../types\";\nimport type { EditableSchema } from \"./types\";\n\nexport interface EditableVoidSerializer<T> {\n is: (node: HTMLElement) => boolean;\n data: (node: HTMLElement) => T;\n plain: (data: T) => string;\n}\n\nconst emptyString = (): string => \"\";\n\nexport const voidNode = <const D>({\n is,\n data,\n plain = emptyString,\n}: {\n is: (node: HTMLElement) => boolean;\n data: (node: HTMLElement) => D;\n plain?: (data: D) => string;\n}): EditableVoidSerializer<D> => {\n return {\n is,\n data,\n plain,\n };\n};\n\ntype Prettify<T> = {\n [K in keyof T]: T[K];\n} & {};\n\ntype ExtractVoidData<T> = T extends EditableVoidSerializer<infer D> ? D : never;\ntype ExtractVoidNode<T> = Prettify<\n {\n [K in keyof T]: {\n type: K;\n data: ExtractVoidData<T[K]>;\n };\n }[keyof T]\n>;\n\n/**\n * Defines structured text schema.\n */\nexport const schema = <\n V extends Record<string, EditableVoidSerializer<any>>,\n M extends boolean = false\n>({\n multiline,\n void: voids,\n}: {\n multiline?: M;\n void: V;\n}): EditableSchema<\n M extends true\n ? (ExtractVoidNode<V> | { type: \"text\"; text: string })[][]\n : (ExtractVoidNode<V> | { type: \"text\"; text: string })[]\n> => {\n type VoidNodeData = ExtractVoidData<V[keyof V]>;\n type TextNodeType = { type: \"text\"; text: string };\n type VoidNodeType = ExtractVoidNode<V>;\n type RowType = (TextNodeType | VoidNodeType)[];\n\n const voidSerializers = Object.entries(voids);\n\n const textCache = new WeakMap<TextNode, TextNodeType>();\n // TODO replace VoidNodeData with VoidNode\n const voidCache = new WeakMap<VoidNodeData, VoidNodeType>();\n\n const serializeRow = (r: readonly NodeData[]): RowType => {\n return r.reduce((acc, t) => {\n if (t.type === NODE_TEXT) {\n let text = textCache.get(t);\n if (!text) {\n textCache.set(t, (text = { type: \"text\", text: t.text }));\n }\n acc.push(text);\n } else {\n acc.push(voidCache.get(t.data as VoidNodeData)!);\n }\n return acc;\n }, [] as RowType);\n };\n\n return {\n single: !multiline,\n js: multiline\n ? (doc) => {\n return doc.map(serializeRow);\n }\n : (doc) => {\n return serializeRow(doc[0]!) satisfies RowType as any; // TODO improve type\n },\n void: (element) => {\n for (const [type, s] of voidSerializers) {\n if (s.is(element as HTMLElement)) {\n const data = s.data(element as HTMLElement) as VoidNodeData;\n // TODO improve\n voidCache.set(data, {\n type,\n data: { ...data },\n } as VoidNodeType);\n return data;\n }\n }\n return;\n },\n copy: (dataTransfer, doc, dom) => {\n const str = doc.reduce((acc, r, i) => {\n if (i !== 0) {\n acc += \"\\n\";\n }\n return (\n acc +\n r.reduce((acc, t) => {\n if (t.type === NODE_TEXT) {\n return acc + t.text;\n }\n const voidNode = voidCache.get(t.data as VoidNodeData)!;\n return acc + voids[voidNode.type]!.plain(t.data);\n }, \"\")\n );\n }, \"\");\n dataTransfer.setData(\"text/plain\", str);\n\n const wrapper = document.createElement(\"div\");\n wrapper.appendChild(dom);\n dataTransfer.setData(\"text/html\", wrapper.innerHTML);\n },\n paste: (dataTransfer) => {\n const html = dataTransfer.getData(\"text/html\");\n if (html) {\n let dom: Node = new DOMParser().parseFromString(html, \"text/html\").body;\n let isWindowsCopy = false;\n // https://github.com/w3c/clipboard-apis/issues/193\n for (const n of [...dom.childNodes]) {\n if (isCommentNode(n)) {\n if (n.data === \"StartFragment\") {\n isWindowsCopy = true;\n dom = new DocumentFragment();\n } else if (n.data === \"EndFragment\") {\n isWindowsCopy = false;\n }\n } else if (isWindowsCopy) {\n dom.appendChild(n);\n }\n }\n return dom;\n }\n return dataTransfer.getData(\"text/plain\");\n },\n };\n};\n"],"names":["compareLine","lineA","lineB","comparePosition","posA","posB","line","isTextNode","node","type","getNodeSize","text","length","insertNodeAfter","index","target","splice","join","lines","i","current","push","split","offset","before","slice","after","unshift","fixPositionAfterInsert","selectionPos","pos","lineDiff","lastRowLength","fixPositionAfterDelete","start","end","replaceRange","doc","fragment","startLine","endLine","splitByStart","Delete","selection","anchor","focus","posDiff","backward","deleteEdit","InsertFragment","lineLength","reduce","acc","n","insertEdit","InsertText","map","l","MoveToPosition","_doc","position","walker","nodeType","isBlockNode","isBrDetected","isElementNode","isCommentNode","SINGLE_LINE_CONTAINER_NAMES","Set","EMBEDDED_CONTENT_TAG_NAMES","defaultIsBlockNode","has","tagName","getDomNode","data","isValidSoftBreak","next","nextSibling","readNext","endNode","nextNode","contains","isBr","contentEditable","prev","previousElementSibling","parse","scopeFn","document","root","config"