@lobehub/editor 4.9.7 → 4.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/{ReactSlashPlugin-BZ0TZ1oy.js → ReactSlashPlugin-DEcBg9nL.js} +44 -44
- package/es/headless.js +737 -37
- package/es/index.js +223 -222
- package/es/react.js +1 -1
- package/package.json +1 -1
package/es/headless.js
CHANGED
|
@@ -6,8 +6,8 @@ import EventEmitter from "eventemitter3";
|
|
|
6
6
|
import { $applyNodeReplacement, $caretFromPoint, $createLineBreakNode, $createNodeSelection, $createParagraphNode, $createRangeSelection, $createTextNode, $getCaretRange, $getCharacterOffsets, $getChildCaret, $getNearestNodeFromDOMNode, $getNodeByKey, $getPreviousSelection, $getRoot, $getSelection, $insertNodes, $isBlockElementNode, $isDecoratorNode, $isElementNode, $isLineBreakNode, $isNodeSelection, $isRangeSelection, $isRootNode, $isRootOrShadowRoot, $isTextNode, $isTextPointCaret, $nodesOfType, $normalizeSelection__EXPERIMENTAL, $parseSerializedNode, $setSelection, COLLABORATION_TAG, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_NORMAL, CONTROLLED_TEXT_INSERTION_COMMAND, COPY_COMMAND, DecoratorNode, ElementNode, FORMAT_TEXT_COMMAND, HISTORIC_TAG, HISTORY_MERGE_TAG, HISTORY_PUSH_TAG, INDENT_CONTENT_COMMAND, INSERT_LINE_BREAK_COMMAND, INSERT_PARAGRAPH_COMMAND, INSERT_TAB_COMMAND, IS_BOLD, IS_CODE, IS_ITALIC, IS_STRIKETHROUGH, IS_SUBSCRIPT, IS_SUPERSCRIPT, IS_UNDERLINE, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND, KEY_ENTER_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, PASTE_COMMAND, ParagraphNode, REDO_COMMAND, SELECTION_CHANGE_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, TEXT_TYPE_TO_FORMAT, TabNode, TextNode, UNDO_COMMAND, createCommand, createEditor, getNearestEditorFromDOMNode, isHTMLElement, isModifierMatch, resetRandomKey } from "lexical";
|
|
7
7
|
import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode, HeadingNode, QuoteNode, registerRichText } from "@lexical/rich-text";
|
|
8
8
|
import createDebug from "debug";
|
|
9
|
-
import { $filter, $findMatchingParent, $getNearestBlockElementAncestorOrThrow, $getNearestNodeOfType, $insertNodeToNearestRoot, CAN_USE_DOM, IS_APPLE as isApple, addClassNamesToElement, calculateZoomLevel, isHTMLElement as isHTMLElement$1, mergeRegister } from "@lexical/utils";
|
|
10
|
-
import { $setBlocksType } from "@lexical/selection";
|
|
9
|
+
import { $filter, $findMatchingParent, $getNearestBlockElementAncestorOrThrow, $getNearestNodeOfType, $insertNodeToNearestRoot, CAN_USE_DOM, IS_APPLE as isApple, addClassNamesToElement, calculateZoomLevel, isHTMLAnchorElement, isHTMLElement as isHTMLElement$1, mergeRegister } from "@lexical/utils";
|
|
10
|
+
import { $isAtNodeEnd, $setBlocksType } from "@lexical/selection";
|
|
11
11
|
import { registerDragonSupport } from "@lexical/dragon";
|
|
12
12
|
import { $createListItemNode, $createListNode, $insertList, $isListItemNode, $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode, registerList, registerListStrictIndentTransform } from "@lexical/list";
|
|
13
13
|
import { DOMParser as DOMParser$1 } from "@xmldom/xmldom";
|
|
@@ -621,7 +621,7 @@ const isHotkeyMatch = (event, hotkey) => {
|
|
|
621
621
|
};
|
|
622
622
|
//#endregion
|
|
623
623
|
//#region src/utils/hotkey/registerHotkey.ts
|
|
624
|
-
const logger$
|
|
624
|
+
const logger$7 = createDebugLogger("hotkey");
|
|
625
625
|
const registerHotkey = (hotkey, callback, options = {}) => {
|
|
626
626
|
return (e) => {
|
|
627
627
|
if (!isHotkeyMatch(e, hotkey.keys)) return false;
|
|
@@ -633,7 +633,7 @@ const registerHotkey = (hotkey, callback, options = {}) => {
|
|
|
633
633
|
...keys,
|
|
634
634
|
scopes: hotkey.scopes
|
|
635
635
|
});
|
|
636
|
-
logger$
|
|
636
|
+
logger$7.debug(`⌨️ Hotkey matched: ${hotkey.id} [${hotkey.keys}]`, hotkey);
|
|
637
637
|
return true;
|
|
638
638
|
};
|
|
639
639
|
};
|
|
@@ -701,6 +701,10 @@ const noop = () => {};
|
|
|
701
701
|
function createEmptyEditorState() {
|
|
702
702
|
return createEditor().getEditorState();
|
|
703
703
|
}
|
|
704
|
+
function assert(cond, message) {
|
|
705
|
+
if (cond) return;
|
|
706
|
+
throw new Error(message);
|
|
707
|
+
}
|
|
704
708
|
function getNodeKeyFromDOMNode(dom, editor) {
|
|
705
709
|
return dom[`__lexicalKey_${editor._key}`];
|
|
706
710
|
}
|
|
@@ -1378,7 +1382,7 @@ function registerCommands(editor) {
|
|
|
1378
1382
|
}
|
|
1379
1383
|
//#endregion
|
|
1380
1384
|
//#region src/plugins/common/node/cursor.ts
|
|
1381
|
-
const logger$
|
|
1385
|
+
const logger$6 = createDebugLogger("common", "cursor");
|
|
1382
1386
|
var CardLikeElementNode = class extends ElementNode {
|
|
1383
1387
|
isCardLike() {
|
|
1384
1388
|
return true;
|
|
@@ -1417,7 +1421,7 @@ function registerCursorNode(editor) {
|
|
|
1417
1421
|
const needAddCursor = [];
|
|
1418
1422
|
for (const [kClass, nodeMaps] of mutatedNodes) if (DecoratorNode.prototype.isPrototypeOf(kClass.prototype)) for (const [key, mutation] of nodeMaps) {
|
|
1419
1423
|
const node = $getNodeByKey(key);
|
|
1420
|
-
logger$
|
|
1424
|
+
logger$6.debug("🎭 DecoratorNode mutated:", node?.getType(), mutation, node);
|
|
1421
1425
|
if (mutation === "created" && node?.isInline() && node.getNextSibling() === null) needAddCursor.push(node);
|
|
1422
1426
|
}
|
|
1423
1427
|
if (needAddCursor.length > 0) editor.update(() => {
|
|
@@ -1519,7 +1523,7 @@ function registerCursorNode(editor) {
|
|
|
1519
1523
|
$setSelection(sel);
|
|
1520
1524
|
return true;
|
|
1521
1525
|
} catch (error) {
|
|
1522
|
-
logger$
|
|
1526
|
+
logger$6.error("❌ Cursor selection error:", error);
|
|
1523
1527
|
}
|
|
1524
1528
|
else if ($isCursorNode(focusNode)) try {
|
|
1525
1529
|
const { key: anchorKey, offset: anchorOffset, type: anchorType } = selection.anchor;
|
|
@@ -1530,7 +1534,7 @@ function registerCursorNode(editor) {
|
|
|
1530
1534
|
$setSelection(sel);
|
|
1531
1535
|
return true;
|
|
1532
1536
|
} catch (error) {
|
|
1533
|
-
logger$
|
|
1537
|
+
logger$6.error("❌ Cursor navigation error:", error);
|
|
1534
1538
|
}
|
|
1535
1539
|
return false;
|
|
1536
1540
|
}, COMMAND_PRIORITY_HIGH), editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, (event) => {
|
|
@@ -1709,7 +1713,7 @@ function charToId(char) {
|
|
|
1709
1713
|
}
|
|
1710
1714
|
//#endregion
|
|
1711
1715
|
//#region src/plugins/litexml/command/index.ts
|
|
1712
|
-
const logger$
|
|
1716
|
+
const logger$5 = createDebugLogger("plugin", "litexml");
|
|
1713
1717
|
function toArrayXml(litexml) {
|
|
1714
1718
|
return Array.isArray(litexml) ? litexml : [litexml];
|
|
1715
1719
|
}
|
|
@@ -1721,7 +1725,7 @@ function tryParseChild(child, editor) {
|
|
|
1721
1725
|
oldNode
|
|
1722
1726
|
};
|
|
1723
1727
|
} catch (error) {
|
|
1724
|
-
logger$
|
|
1728
|
+
logger$5.error("❌ Error parsing child node:", error);
|
|
1725
1729
|
return {
|
|
1726
1730
|
newNode: null,
|
|
1727
1731
|
oldNode: null
|
|
@@ -1834,25 +1838,25 @@ function registerLiteXMLCommand(editor, dataSource) {
|
|
|
1834
1838
|
delay: true
|
|
1835
1839
|
}, dataSource);
|
|
1836
1840
|
break;
|
|
1837
|
-
default: logger$
|
|
1841
|
+
default: logger$5.warn(`⚠️ Unknown action type: ${action}`);
|
|
1838
1842
|
}
|
|
1839
1843
|
});
|
|
1840
|
-
return
|
|
1844
|
+
return false;
|
|
1841
1845
|
} catch (error) {
|
|
1842
|
-
logger$
|
|
1846
|
+
logger$5.error("❌ Error processing LITEXML_MODIFY_COMMAND:", error);
|
|
1843
1847
|
return false;
|
|
1844
1848
|
}
|
|
1845
1849
|
}, COMMAND_PRIORITY_EDITOR), editor.registerCommand(LITEXML_APPLY_COMMAND, (payload) => {
|
|
1846
1850
|
const { litexml, delay } = payload;
|
|
1847
1851
|
handleModify(editor, dataSource, toArrayXml(litexml), delay);
|
|
1848
|
-
return
|
|
1852
|
+
return false;
|
|
1849
1853
|
}, COMMAND_PRIORITY_EDITOR), editor.registerCommand(LITEXML_REMOVE_COMMAND, (payload) => {
|
|
1850
1854
|
const { id, delay } = payload;
|
|
1851
1855
|
handleRemove(editor, charToId(id), delay);
|
|
1852
|
-
return
|
|
1856
|
+
return false;
|
|
1853
1857
|
}, COMMAND_PRIORITY_EDITOR), editor.registerCommand(LITEXML_INSERT_COMMAND, (payload) => {
|
|
1854
1858
|
handleInsert(editor, payload, dataSource);
|
|
1855
|
-
return
|
|
1859
|
+
return false;
|
|
1856
1860
|
}, COMMAND_PRIORITY_EDITOR));
|
|
1857
1861
|
}
|
|
1858
1862
|
function handleModify(editor, dataSource, arrayXml, delay) {
|
|
@@ -1864,9 +1868,9 @@ function handleModify(editor, dataSource, arrayXml, delay) {
|
|
|
1864
1868
|
try {
|
|
1865
1869
|
const { oldNode, newNode } = tryParseChild(child, editor);
|
|
1866
1870
|
if (oldNode && newNode) handleReplaceForApplyDelay(oldNode, newNode, modifyBlockNodes, diffNodeMap, editor);
|
|
1867
|
-
else logger$
|
|
1871
|
+
else logger$5.warn(`⚠️ Node with key ${child.id} not found for diffing.`);
|
|
1868
1872
|
} catch (error) {
|
|
1869
|
-
logger$
|
|
1873
|
+
logger$5.error("❌ Error replacing node:", error);
|
|
1870
1874
|
}
|
|
1871
1875
|
});
|
|
1872
1876
|
});
|
|
@@ -1890,7 +1894,7 @@ function handleModify(editor, dataSource, arrayXml, delay) {
|
|
|
1890
1894
|
} else prevNode = prevNode.insertAfter(newNode);
|
|
1891
1895
|
else $insertNodes([newNode]);
|
|
1892
1896
|
} catch (error) {
|
|
1893
|
-
logger$
|
|
1897
|
+
logger$5.error("❌ Error replacing node:", error);
|
|
1894
1898
|
}
|
|
1895
1899
|
});
|
|
1896
1900
|
});
|
|
@@ -2027,7 +2031,7 @@ function handleInsert(editor, payload, dataSource) {
|
|
|
2027
2031
|
});
|
|
2028
2032
|
}
|
|
2029
2033
|
} catch (error) {
|
|
2030
|
-
logger$
|
|
2034
|
+
logger$5.error("❌ Error inserting node:", error);
|
|
2031
2035
|
}
|
|
2032
2036
|
});
|
|
2033
2037
|
}
|
|
@@ -2167,7 +2171,7 @@ var LitexmlService = class {
|
|
|
2167
2171
|
};
|
|
2168
2172
|
//#endregion
|
|
2169
2173
|
//#region src/plugins/litexml/data-source/litexml-data-source.ts
|
|
2170
|
-
const logger$
|
|
2174
|
+
const logger$4 = createDebugLogger("plugin", "litexml");
|
|
2171
2175
|
var IXmlWriterContext = class {
|
|
2172
2176
|
createXmlNode(tagName, attributes, textContent) {
|
|
2173
2177
|
return {
|
|
@@ -2195,7 +2199,7 @@ var LitexmlDataSource = class extends DataSource {
|
|
|
2195
2199
|
const xml = this.parseXMLString(litexml);
|
|
2196
2200
|
const inode = this.xmlToLexical(xml);
|
|
2197
2201
|
this.getService?.(INodeService)?.processNodeTree(inode);
|
|
2198
|
-
logger$
|
|
2202
|
+
logger$4.debug("Parsed XML to Lexical State:", inode);
|
|
2199
2203
|
return inode;
|
|
2200
2204
|
}
|
|
2201
2205
|
/**
|
|
@@ -2216,7 +2220,7 @@ var LitexmlDataSource = class extends DataSource {
|
|
|
2216
2220
|
});
|
|
2217
2221
|
editor.setEditorState(newState);
|
|
2218
2222
|
} catch (error) {
|
|
2219
|
-
logger$
|
|
2223
|
+
logger$4.error("Failed to parse XML:", error);
|
|
2220
2224
|
throw error;
|
|
2221
2225
|
}
|
|
2222
2226
|
}
|
|
@@ -2251,7 +2255,7 @@ var LitexmlDataSource = class extends DataSource {
|
|
|
2251
2255
|
return this.lexicalToXML(rootNode);
|
|
2252
2256
|
});
|
|
2253
2257
|
} catch (error) {
|
|
2254
|
-
logger$
|
|
2258
|
+
logger$4.error("Failed to export to XML:", error);
|
|
2255
2259
|
throw error;
|
|
2256
2260
|
}
|
|
2257
2261
|
}
|
|
@@ -22546,7 +22550,7 @@ function remarkMath(options) {
|
|
|
22546
22550
|
}
|
|
22547
22551
|
//#endregion
|
|
22548
22552
|
//#region src/plugins/markdown/utils/logger.ts
|
|
22549
|
-
const logger$
|
|
22553
|
+
const logger$3 = createDebugLogger("plugin", "markdown");
|
|
22550
22554
|
//#endregion
|
|
22551
22555
|
//#region src/plugins/markdown/data-source/markdown/parse.ts
|
|
22552
22556
|
const selfClosingHtmlTags = new Set([
|
|
@@ -22617,7 +22621,7 @@ function convertMdastToLexical(node, index, ctx, markdownReaders = {}) {
|
|
|
22617
22621
|
const top = ctx.pop();
|
|
22618
22622
|
htmlStack.pop();
|
|
22619
22623
|
if (top?.tag !== tag) {
|
|
22620
|
-
logger$
|
|
22624
|
+
logger$3.warn("HTML tag mismatch:", tag);
|
|
22621
22625
|
ret.push(...top?.children || []);
|
|
22622
22626
|
return ret;
|
|
22623
22627
|
}
|
|
@@ -22707,7 +22711,7 @@ function registerDefaultReaders(markdownReaders) {
|
|
|
22707
22711
|
}
|
|
22708
22712
|
function parseMarkdownToLexical(markdown, markdownReaders = {}) {
|
|
22709
22713
|
const ast = remark().use(remarkCjkFriendly).use(remarkMath).use([[remarkGfm, { singleTilde: false }]]).parse(markdown);
|
|
22710
|
-
logger$
|
|
22714
|
+
logger$3.debug("Parsed MDAST:", ast);
|
|
22711
22715
|
const ctx = new MarkdownContext(ast);
|
|
22712
22716
|
registerDefaultReaders(markdownReaders);
|
|
22713
22717
|
return convertMdastToLexical(ast, 0, ctx, markdownReaders);
|
|
@@ -22855,7 +22859,7 @@ function insertIRootNode(editor, root, selection) {
|
|
|
22855
22859
|
}
|
|
22856
22860
|
//#endregion
|
|
22857
22861
|
//#region src/plugins/markdown/command/index.ts
|
|
22858
|
-
const logger$
|
|
22862
|
+
const logger$2 = createDebugLogger("plugin", "markdown");
|
|
22859
22863
|
const INSERT_MARKDOWN_COMMAND = createCommand("INSERT_MARKDOWN_COMMAND");
|
|
22860
22864
|
const GET_MARKDOWN_SELECTION_COMMAND = createCommand("GET_MARKDOWN_SELECTION_COMMAND");
|
|
22861
22865
|
function restoreToEntry(editor, entry) {
|
|
@@ -22869,7 +22873,7 @@ const getLineNumber = (content, charIndex) => {
|
|
|
22869
22873
|
function registerMarkdownCommand(editor, kernel, service) {
|
|
22870
22874
|
return mergeRegister(editor.registerCommand(INSERT_MARKDOWN_COMMAND, (payload) => {
|
|
22871
22875
|
const { markdown } = payload;
|
|
22872
|
-
logger$
|
|
22876
|
+
logger$2.debug("INSERT_MARKDOWN_COMMAND payload:", payload);
|
|
22873
22877
|
restoreToEntry(editor, payload.historyState);
|
|
22874
22878
|
setTimeout(() => {
|
|
22875
22879
|
editor.update(() => {
|
|
@@ -22877,11 +22881,11 @@ function registerMarkdownCommand(editor, kernel, service) {
|
|
|
22877
22881
|
const root = parseMarkdownToLexical(markdown, service.markdownReaders);
|
|
22878
22882
|
const selection = $getSelection();
|
|
22879
22883
|
const nodes = $generateNodesFromSerializedNodes(root.children);
|
|
22880
|
-
logger$
|
|
22884
|
+
logger$2.debug("INSERT_MARKDOWN_COMMAND nodes:", nodes);
|
|
22881
22885
|
$insertGeneratedNodes(editor, nodes, selection);
|
|
22882
22886
|
return true;
|
|
22883
22887
|
} catch (error) {
|
|
22884
|
-
logger$
|
|
22888
|
+
logger$2.error("Failed to handle markdown paste:", error);
|
|
22885
22889
|
}
|
|
22886
22890
|
}, { tag: HISTORY_PUSH_TAG });
|
|
22887
22891
|
}, 0);
|
|
@@ -22912,8 +22916,8 @@ function registerMarkdownCommand(editor, kernel, service) {
|
|
|
22912
22916
|
const startLine = getLineNumber(markdownContent, startIndex);
|
|
22913
22917
|
const endLine = getLineNumber(markdownContent, endIndex);
|
|
22914
22918
|
payload.onResult(startLine, endLine);
|
|
22915
|
-
logger$
|
|
22916
|
-
logger$
|
|
22919
|
+
logger$2.debug("GET_MARKDOWN_SELECTION_COMMAND markdownContent:", markdownContent);
|
|
22920
|
+
logger$2.debug("GET_MARKDOWN_SELECTION_COMMAND startLine:", startLine, "endLine:", endLine);
|
|
22917
22921
|
return markdownContent;
|
|
22918
22922
|
} });
|
|
22919
22923
|
}
|
|
@@ -22973,7 +22977,7 @@ var MarkdownDataSource = class extends DataSource {
|
|
|
22973
22977
|
}).processSync(markdown);
|
|
22974
22978
|
return String(result);
|
|
22975
22979
|
} catch (error) {
|
|
22976
|
-
logger$
|
|
22980
|
+
logger$3.error("Failed to format markdown:", error);
|
|
22977
22981
|
return markdown;
|
|
22978
22982
|
}
|
|
22979
22983
|
}
|
|
@@ -22986,7 +22990,7 @@ var MarkdownDataSource = class extends DataSource {
|
|
|
22986
22990
|
read(editor, data) {
|
|
22987
22991
|
const inode = { root: parseMarkdownToLexical(data, this.markdownService.markdownReaders) };
|
|
22988
22992
|
this.getService?.(INodeService)?.processNodeTree(inode);
|
|
22989
|
-
logger$
|
|
22993
|
+
logger$3.debug("Parsed Lexical State:", inode);
|
|
22990
22994
|
editor.setEditorState(editor.parseEditorState(inode));
|
|
22991
22995
|
}
|
|
22992
22996
|
write(editor, options) {
|
|
@@ -24401,7 +24405,7 @@ function runPasteHandlers(ctx, handlers) {
|
|
|
24401
24405
|
}
|
|
24402
24406
|
//#endregion
|
|
24403
24407
|
//#region src/plugins/common/plugin/register.ts
|
|
24404
|
-
const logger = createDebugLogger("plugin", "common");
|
|
24408
|
+
const logger$1 = createDebugLogger("plugin", "common");
|
|
24405
24409
|
function resolveElement(element, isBackward, focusOffset) {
|
|
24406
24410
|
const parent = element.getParent();
|
|
24407
24411
|
let offset = focusOffset;
|
|
@@ -24502,7 +24506,7 @@ function registerRichKeydown(editor, kernel, options) {
|
|
|
24502
24506
|
selection.insertText(text);
|
|
24503
24507
|
});
|
|
24504
24508
|
} catch (error) {
|
|
24505
|
-
logger.error("❌ Failed to paste as plain text:", error);
|
|
24509
|
+
logger$1.error("❌ Failed to paste as plain text:", error);
|
|
24506
24510
|
}
|
|
24507
24511
|
}, {
|
|
24508
24512
|
enabled: enableHotkey,
|
|
@@ -25307,6 +25311,701 @@ const HRPlugin = class extends KernelPlugin {
|
|
|
25307
25311
|
}
|
|
25308
25312
|
};
|
|
25309
25313
|
//#endregion
|
|
25314
|
+
//#region src/plugins/link/node/LinkNode.ts
|
|
25315
|
+
const logger = createDebugLogger("plugin", "link");
|
|
25316
|
+
const SUPPORTED_URL_PROTOCOLS$1 = new Set([
|
|
25317
|
+
"http:",
|
|
25318
|
+
"https:",
|
|
25319
|
+
"mailto:",
|
|
25320
|
+
"sms:",
|
|
25321
|
+
"tel:"
|
|
25322
|
+
]);
|
|
25323
|
+
const HOVER_LINK_COMMAND = createCommand("HOVER_LINK_COMMAND");
|
|
25324
|
+
const HOVER_OUT_LINK_COMMAND = createCommand("HOVER_OUT_LINK_COMMAND");
|
|
25325
|
+
/** @noInheritDoc */
|
|
25326
|
+
var LinkNode = class LinkNode extends ElementNode {
|
|
25327
|
+
static getType() {
|
|
25328
|
+
return "link";
|
|
25329
|
+
}
|
|
25330
|
+
static clone(node) {
|
|
25331
|
+
return new LinkNode(node.__url, {
|
|
25332
|
+
rel: node.__rel,
|
|
25333
|
+
target: node.__target,
|
|
25334
|
+
title: node.__title
|
|
25335
|
+
}, node.__key);
|
|
25336
|
+
}
|
|
25337
|
+
constructor(url = "", attributes = {}, key) {
|
|
25338
|
+
super(key);
|
|
25339
|
+
const { target = null, rel = null, title = null } = attributes;
|
|
25340
|
+
this.__url = url;
|
|
25341
|
+
this.__target = target;
|
|
25342
|
+
this.__rel = rel;
|
|
25343
|
+
this.__title = title;
|
|
25344
|
+
}
|
|
25345
|
+
createDOM(config, editor) {
|
|
25346
|
+
logger.debug("🔍 config", config);
|
|
25347
|
+
const element = document.createElement("a");
|
|
25348
|
+
this.updateLinkDOM(null, element, config);
|
|
25349
|
+
addClassNamesToElement(element, config.theme.link);
|
|
25350
|
+
element.addEventListener("mouseenter", (event) => {
|
|
25351
|
+
if (event.target instanceof HTMLElement) {
|
|
25352
|
+
event.target.classList.add("hover");
|
|
25353
|
+
editor.dispatchCommand(HOVER_LINK_COMMAND, {
|
|
25354
|
+
event,
|
|
25355
|
+
linkNode: this
|
|
25356
|
+
});
|
|
25357
|
+
}
|
|
25358
|
+
});
|
|
25359
|
+
element.addEventListener("mouseleave", (event) => {
|
|
25360
|
+
if (event.target instanceof HTMLElement) {
|
|
25361
|
+
event.target.classList.remove("hover");
|
|
25362
|
+
editor.dispatchCommand(HOVER_OUT_LINK_COMMAND, { event });
|
|
25363
|
+
}
|
|
25364
|
+
});
|
|
25365
|
+
return element;
|
|
25366
|
+
}
|
|
25367
|
+
updateLinkDOM(prevNode, anchor, _config) {
|
|
25368
|
+
if (isHTMLAnchorElement(anchor)) {
|
|
25369
|
+
if (!prevNode || prevNode.__url !== this.__url) anchor.href = this.sanitizeUrl(this.__url);
|
|
25370
|
+
for (const attr of [
|
|
25371
|
+
"target",
|
|
25372
|
+
"rel",
|
|
25373
|
+
"title"
|
|
25374
|
+
]) {
|
|
25375
|
+
const key = `__${attr}`;
|
|
25376
|
+
const value = this[key];
|
|
25377
|
+
if (!prevNode || prevNode[key] !== value) if (value) anchor[attr] = value;
|
|
25378
|
+
else anchor.removeAttribute(attr);
|
|
25379
|
+
}
|
|
25380
|
+
}
|
|
25381
|
+
}
|
|
25382
|
+
updateDOM(prevNode, anchor, config) {
|
|
25383
|
+
this.updateLinkDOM(prevNode, anchor, config);
|
|
25384
|
+
return false;
|
|
25385
|
+
}
|
|
25386
|
+
static importDOM() {
|
|
25387
|
+
return { a: () => ({
|
|
25388
|
+
conversion: $convertAnchorElement,
|
|
25389
|
+
priority: 1
|
|
25390
|
+
}) };
|
|
25391
|
+
}
|
|
25392
|
+
static importJSON(serializedNode) {
|
|
25393
|
+
return $createLinkNode().updateFromJSON(serializedNode);
|
|
25394
|
+
}
|
|
25395
|
+
updateFromJSON(serializedNode) {
|
|
25396
|
+
return super.updateFromJSON(serializedNode).setURL(serializedNode.url).setRel(serializedNode.rel || null).setTarget(serializedNode.target || null).setTitle(serializedNode.title || null);
|
|
25397
|
+
}
|
|
25398
|
+
sanitizeUrl(url) {
|
|
25399
|
+
url = formatUrl(url);
|
|
25400
|
+
try {
|
|
25401
|
+
const parsedUrl = new URL(formatUrl(url));
|
|
25402
|
+
if (!SUPPORTED_URL_PROTOCOLS$1.has(parsedUrl.protocol)) return "about:blank";
|
|
25403
|
+
} catch {
|
|
25404
|
+
return url;
|
|
25405
|
+
}
|
|
25406
|
+
return url;
|
|
25407
|
+
}
|
|
25408
|
+
exportJSON() {
|
|
25409
|
+
return {
|
|
25410
|
+
...super.exportJSON(),
|
|
25411
|
+
rel: this.getRel(),
|
|
25412
|
+
target: this.getTarget(),
|
|
25413
|
+
title: this.getTitle(),
|
|
25414
|
+
url: this.getURL()
|
|
25415
|
+
};
|
|
25416
|
+
}
|
|
25417
|
+
getURL() {
|
|
25418
|
+
return this.getLatest().__url;
|
|
25419
|
+
}
|
|
25420
|
+
setURL(url) {
|
|
25421
|
+
const writable = this.getWritable();
|
|
25422
|
+
writable.__url = url;
|
|
25423
|
+
return writable;
|
|
25424
|
+
}
|
|
25425
|
+
getTarget() {
|
|
25426
|
+
return this.getLatest().__target;
|
|
25427
|
+
}
|
|
25428
|
+
setTarget(target) {
|
|
25429
|
+
const writable = this.getWritable();
|
|
25430
|
+
writable.__target = target;
|
|
25431
|
+
return writable;
|
|
25432
|
+
}
|
|
25433
|
+
getRel() {
|
|
25434
|
+
return this.getLatest().__rel;
|
|
25435
|
+
}
|
|
25436
|
+
setRel(rel) {
|
|
25437
|
+
const writable = this.getWritable();
|
|
25438
|
+
writable.__rel = rel;
|
|
25439
|
+
return writable;
|
|
25440
|
+
}
|
|
25441
|
+
getTitle() {
|
|
25442
|
+
return this.getLatest().__title;
|
|
25443
|
+
}
|
|
25444
|
+
setTitle(title) {
|
|
25445
|
+
const writable = this.getWritable();
|
|
25446
|
+
writable.__title = title;
|
|
25447
|
+
return writable;
|
|
25448
|
+
}
|
|
25449
|
+
insertNewAfter(_, restoreSelection = true) {
|
|
25450
|
+
const linkNode = $createLinkNode(this.__url, {
|
|
25451
|
+
rel: this.__rel,
|
|
25452
|
+
target: this.__target,
|
|
25453
|
+
title: this.__title
|
|
25454
|
+
});
|
|
25455
|
+
this.insertAfter(linkNode, restoreSelection);
|
|
25456
|
+
return linkNode;
|
|
25457
|
+
}
|
|
25458
|
+
canInsertTextBefore() {
|
|
25459
|
+
return false;
|
|
25460
|
+
}
|
|
25461
|
+
canInsertTextAfter() {
|
|
25462
|
+
return false;
|
|
25463
|
+
}
|
|
25464
|
+
canBeEmpty() {
|
|
25465
|
+
return false;
|
|
25466
|
+
}
|
|
25467
|
+
isInline() {
|
|
25468
|
+
return true;
|
|
25469
|
+
}
|
|
25470
|
+
extractWithChild(child, selection) {
|
|
25471
|
+
if (!$isRangeSelection(selection)) return false;
|
|
25472
|
+
const anchorNode = selection.anchor.getNode();
|
|
25473
|
+
const focusNode = selection.focus.getNode();
|
|
25474
|
+
return this.isParentOf(anchorNode) && this.isParentOf(focusNode) && selection.getTextContent().length > 0;
|
|
25475
|
+
}
|
|
25476
|
+
isEmailURI() {
|
|
25477
|
+
return this.__url.startsWith("mailto:");
|
|
25478
|
+
}
|
|
25479
|
+
isWebSiteURI() {
|
|
25480
|
+
return this.__url.startsWith("https://") || this.__url.startsWith("http://");
|
|
25481
|
+
}
|
|
25482
|
+
};
|
|
25483
|
+
function $convertAnchorElement(domNode) {
|
|
25484
|
+
let node = null;
|
|
25485
|
+
if (isHTMLAnchorElement(domNode)) {
|
|
25486
|
+
const content = domNode.textContent;
|
|
25487
|
+
if (content !== null && content !== "" || domNode.children.length > 0) node = $createLinkNode(domNode.getAttribute("href") || "", {
|
|
25488
|
+
rel: domNode.getAttribute("rel"),
|
|
25489
|
+
target: domNode.getAttribute("target"),
|
|
25490
|
+
title: domNode.getAttribute("title")
|
|
25491
|
+
});
|
|
25492
|
+
}
|
|
25493
|
+
return { node };
|
|
25494
|
+
}
|
|
25495
|
+
/**
|
|
25496
|
+
* Takes a URL and creates a LinkNode.
|
|
25497
|
+
* @param url - The URL the LinkNode should direct to.
|
|
25498
|
+
* @param attributes - Optional HTML a tag attributes \\{ target, rel, title \\}
|
|
25499
|
+
* @returns The LinkNode.
|
|
25500
|
+
*/
|
|
25501
|
+
function $createLinkNode(url = "", attributes) {
|
|
25502
|
+
return $applyNodeReplacement(new LinkNode(url, attributes));
|
|
25503
|
+
}
|
|
25504
|
+
/**
|
|
25505
|
+
* Determines if node is a LinkNode.
|
|
25506
|
+
* @param node - The node to be checked.
|
|
25507
|
+
* @returns true if node is a LinkNode, false otherwise.
|
|
25508
|
+
*/
|
|
25509
|
+
function $isLinkNode(node) {
|
|
25510
|
+
return node instanceof LinkNode;
|
|
25511
|
+
}
|
|
25512
|
+
var AutoLinkNode = class AutoLinkNode extends LinkNode {
|
|
25513
|
+
constructor(url = "", attributes = {}, key) {
|
|
25514
|
+
super(url, attributes, key);
|
|
25515
|
+
this.__isUnlinked = attributes.isUnlinked !== void 0 && attributes.isUnlinked !== null ? attributes.isUnlinked : false;
|
|
25516
|
+
}
|
|
25517
|
+
static getType() {
|
|
25518
|
+
return "autolink";
|
|
25519
|
+
}
|
|
25520
|
+
static clone(node) {
|
|
25521
|
+
return new AutoLinkNode(node.__url, {
|
|
25522
|
+
isUnlinked: node.__isUnlinked,
|
|
25523
|
+
rel: node.__rel,
|
|
25524
|
+
target: node.__target,
|
|
25525
|
+
title: node.__title
|
|
25526
|
+
}, node.__key);
|
|
25527
|
+
}
|
|
25528
|
+
getIsUnlinked() {
|
|
25529
|
+
return this.__isUnlinked;
|
|
25530
|
+
}
|
|
25531
|
+
setIsUnlinked(value) {
|
|
25532
|
+
const self = this.getWritable();
|
|
25533
|
+
self.__isUnlinked = value;
|
|
25534
|
+
return self;
|
|
25535
|
+
}
|
|
25536
|
+
createDOM(config, editor) {
|
|
25537
|
+
logger.debug("🔍 config", config);
|
|
25538
|
+
if (this.__isUnlinked) return document.createElement("span");
|
|
25539
|
+
else return super.createDOM(config, editor);
|
|
25540
|
+
}
|
|
25541
|
+
updateDOM(prevNode, anchor, config) {
|
|
25542
|
+
return super.updateDOM(prevNode, anchor, config) || prevNode.__isUnlinked !== this.__isUnlinked;
|
|
25543
|
+
}
|
|
25544
|
+
static importJSON(serializedNode) {
|
|
25545
|
+
return $createAutoLinkNode().updateFromJSON(serializedNode);
|
|
25546
|
+
}
|
|
25547
|
+
updateFromJSON(serializedNode) {
|
|
25548
|
+
return super.updateFromJSON(serializedNode).setIsUnlinked(serializedNode.isUnlinked || false);
|
|
25549
|
+
}
|
|
25550
|
+
static importDOM() {
|
|
25551
|
+
return null;
|
|
25552
|
+
}
|
|
25553
|
+
exportJSON() {
|
|
25554
|
+
return {
|
|
25555
|
+
...super.exportJSON(),
|
|
25556
|
+
isUnlinked: this.__isUnlinked
|
|
25557
|
+
};
|
|
25558
|
+
}
|
|
25559
|
+
insertNewAfter(selection, restoreSelection = true) {
|
|
25560
|
+
const element = this.getParentOrThrow().insertNewAfter(selection, restoreSelection);
|
|
25561
|
+
if ($isElementNode(element)) {
|
|
25562
|
+
const linkNode = $createAutoLinkNode(this.__url, {
|
|
25563
|
+
isUnlinked: this.__isUnlinked,
|
|
25564
|
+
rel: this.__rel,
|
|
25565
|
+
target: this.__target,
|
|
25566
|
+
title: this.__title
|
|
25567
|
+
});
|
|
25568
|
+
element.append(linkNode);
|
|
25569
|
+
return linkNode;
|
|
25570
|
+
}
|
|
25571
|
+
return null;
|
|
25572
|
+
}
|
|
25573
|
+
};
|
|
25574
|
+
/**
|
|
25575
|
+
* Takes a URL and creates an AutoLinkNode. AutoLinkNodes are generally automatically generated
|
|
25576
|
+
* during typing, which is especially useful when a button to generate a LinkNode is not practical.
|
|
25577
|
+
* @param url - The URL the LinkNode should direct to.
|
|
25578
|
+
* @param attributes - Optional HTML a tag attributes. \\{ target, rel, title \\}
|
|
25579
|
+
* @returns The LinkNode.
|
|
25580
|
+
*/
|
|
25581
|
+
function $createAutoLinkNode(url = "", attributes) {
|
|
25582
|
+
return $applyNodeReplacement(new AutoLinkNode(url, attributes));
|
|
25583
|
+
}
|
|
25584
|
+
/**
|
|
25585
|
+
* Determines if node is an AutoLinkNode.
|
|
25586
|
+
* @param node - The node to be checked.
|
|
25587
|
+
* @returns true if node is an AutoLinkNode, false otherwise.
|
|
25588
|
+
*/
|
|
25589
|
+
function $isAutoLinkNode(node) {
|
|
25590
|
+
return node instanceof AutoLinkNode;
|
|
25591
|
+
}
|
|
25592
|
+
const TOGGLE_LINK_COMMAND = createCommand("TOGGLE_LINK_COMMAND");
|
|
25593
|
+
function $getPointNode(point, offset) {
|
|
25594
|
+
if (point.type === "element") {
|
|
25595
|
+
const node = point.getNode();
|
|
25596
|
+
assert($isElementNode(node), "$getPointNode: element point is not an ElementNode");
|
|
25597
|
+
return node.getChildren()[point.offset + offset] || null;
|
|
25598
|
+
}
|
|
25599
|
+
return null;
|
|
25600
|
+
}
|
|
25601
|
+
/**
|
|
25602
|
+
* Preserve the logical start/end of a RangeSelection in situations where
|
|
25603
|
+
* the point is an element that may be reparented in the callback.
|
|
25604
|
+
*
|
|
25605
|
+
* @param $fn The function to run
|
|
25606
|
+
* @returns The result of the callback
|
|
25607
|
+
*/
|
|
25608
|
+
function $withSelectedNodes($fn) {
|
|
25609
|
+
const initialSelection = $getSelection();
|
|
25610
|
+
if (!$isRangeSelection(initialSelection)) return $fn();
|
|
25611
|
+
const normalized = $normalizeSelection__EXPERIMENTAL(initialSelection);
|
|
25612
|
+
const isBackwards = normalized.isBackward();
|
|
25613
|
+
const anchorNode = $getPointNode(normalized.anchor, isBackwards ? -1 : 0);
|
|
25614
|
+
const focusNode = $getPointNode(normalized.focus, isBackwards ? 0 : -1);
|
|
25615
|
+
const rval = $fn();
|
|
25616
|
+
if (anchorNode || focusNode) {
|
|
25617
|
+
const updatedSelection = $getSelection();
|
|
25618
|
+
if ($isRangeSelection(updatedSelection)) {
|
|
25619
|
+
const finalSelection = updatedSelection.clone();
|
|
25620
|
+
if (anchorNode) {
|
|
25621
|
+
const anchorParent = anchorNode.getParent();
|
|
25622
|
+
if (anchorParent) finalSelection.anchor.set(anchorParent.getKey(), anchorNode.getIndexWithinParent() + (isBackwards ? 1 : 0), "element");
|
|
25623
|
+
}
|
|
25624
|
+
if (focusNode) {
|
|
25625
|
+
const focusParent = focusNode.getParent();
|
|
25626
|
+
if (focusParent) finalSelection.focus.set(focusParent.getKey(), focusNode.getIndexWithinParent() + (isBackwards ? 0 : 1), "element");
|
|
25627
|
+
}
|
|
25628
|
+
$setSelection($normalizeSelection__EXPERIMENTAL(finalSelection));
|
|
25629
|
+
}
|
|
25630
|
+
}
|
|
25631
|
+
return rval;
|
|
25632
|
+
}
|
|
25633
|
+
/**
|
|
25634
|
+
* Generates or updates a LinkNode. It can also delete a LinkNode if the URL is null,
|
|
25635
|
+
* but saves any children and brings them up to the parent node.
|
|
25636
|
+
* @param url - The URL the link directs to.
|
|
25637
|
+
* @param attributes - Optional HTML a tag attributes. \\{ target, rel, title \\}
|
|
25638
|
+
*/
|
|
25639
|
+
function $toggleLink(url, attributes = {}) {
|
|
25640
|
+
const { target, title } = attributes;
|
|
25641
|
+
const rel = attributes.rel === void 0 ? "noreferrer" : attributes.rel;
|
|
25642
|
+
const selection = $getSelection();
|
|
25643
|
+
if (selection === null || !$isRangeSelection(selection) && !$isNodeSelection(selection)) return;
|
|
25644
|
+
if ($isNodeSelection(selection)) {
|
|
25645
|
+
const nodes = selection.getNodes();
|
|
25646
|
+
if (nodes.length === 0) return;
|
|
25647
|
+
nodes.forEach((node) => {
|
|
25648
|
+
if (url === null) {
|
|
25649
|
+
const linkParent = $findMatchingParent(node, (parent) => !$isAutoLinkNode(parent) && $isLinkNode(parent));
|
|
25650
|
+
if (linkParent) {
|
|
25651
|
+
linkParent.insertBefore(node);
|
|
25652
|
+
if (linkParent.getChildren().length === 0) linkParent.remove();
|
|
25653
|
+
}
|
|
25654
|
+
} else {
|
|
25655
|
+
const existingLink = $findMatchingParent(node, (parent) => !$isAutoLinkNode(parent) && $isLinkNode(parent));
|
|
25656
|
+
if (existingLink) {
|
|
25657
|
+
existingLink.setURL(url);
|
|
25658
|
+
if (target !== void 0) existingLink.setTarget(target);
|
|
25659
|
+
if (rel !== void 0) existingLink.setRel(rel);
|
|
25660
|
+
} else {
|
|
25661
|
+
const linkNode = $createLinkNode(url, {
|
|
25662
|
+
rel,
|
|
25663
|
+
target
|
|
25664
|
+
});
|
|
25665
|
+
node.insertBefore(linkNode);
|
|
25666
|
+
linkNode.append(node);
|
|
25667
|
+
}
|
|
25668
|
+
}
|
|
25669
|
+
});
|
|
25670
|
+
return;
|
|
25671
|
+
}
|
|
25672
|
+
const nodes = selection.extract();
|
|
25673
|
+
if (url === null) {
|
|
25674
|
+
nodes.forEach((node) => {
|
|
25675
|
+
const parentLink = $findMatchingParent(node, (parent) => !$isAutoLinkNode(parent) && $isLinkNode(parent));
|
|
25676
|
+
if (parentLink) {
|
|
25677
|
+
const children = parentLink.getChildren();
|
|
25678
|
+
for (const child of children) parentLink.insertBefore(child);
|
|
25679
|
+
parentLink.remove();
|
|
25680
|
+
}
|
|
25681
|
+
});
|
|
25682
|
+
return;
|
|
25683
|
+
}
|
|
25684
|
+
const updatedNodes = /* @__PURE__ */ new Set();
|
|
25685
|
+
const updateLinkNode = (linkNode) => {
|
|
25686
|
+
if (updatedNodes.has(linkNode.getKey())) return;
|
|
25687
|
+
updatedNodes.add(linkNode.getKey());
|
|
25688
|
+
linkNode.setURL(url);
|
|
25689
|
+
if (target !== void 0) linkNode.setTarget(target);
|
|
25690
|
+
if (rel !== void 0) linkNode.setRel(rel);
|
|
25691
|
+
if (title !== void 0) linkNode.setTitle(title);
|
|
25692
|
+
};
|
|
25693
|
+
if (nodes.length === 1) {
|
|
25694
|
+
const firstNode = nodes[0];
|
|
25695
|
+
const linkNode = $getAncestor(firstNode, $isLinkNode);
|
|
25696
|
+
if (linkNode !== null) return updateLinkNode(linkNode);
|
|
25697
|
+
}
|
|
25698
|
+
$withSelectedNodes(() => {
|
|
25699
|
+
let linkNode = null;
|
|
25700
|
+
for (const node of nodes) {
|
|
25701
|
+
if (!node.isAttached()) continue;
|
|
25702
|
+
const parentLinkNode = $getAncestor(node, $isLinkNode);
|
|
25703
|
+
if (parentLinkNode) {
|
|
25704
|
+
updateLinkNode(parentLinkNode);
|
|
25705
|
+
continue;
|
|
25706
|
+
}
|
|
25707
|
+
if ($isElementNode(node)) {
|
|
25708
|
+
if (!node.isInline()) continue;
|
|
25709
|
+
if ($isLinkNode(node)) {
|
|
25710
|
+
if (!$isAutoLinkNode(node) && (linkNode === null || !linkNode.getParentOrThrow().isParentOf(node))) {
|
|
25711
|
+
updateLinkNode(node);
|
|
25712
|
+
linkNode = node;
|
|
25713
|
+
continue;
|
|
25714
|
+
}
|
|
25715
|
+
for (const child of node.getChildren()) node.insertBefore(child);
|
|
25716
|
+
node.remove();
|
|
25717
|
+
continue;
|
|
25718
|
+
}
|
|
25719
|
+
}
|
|
25720
|
+
const prevLinkNode = node.getPreviousSibling();
|
|
25721
|
+
if ($isLinkNode(prevLinkNode) && prevLinkNode.is(linkNode)) {
|
|
25722
|
+
prevLinkNode.append(node);
|
|
25723
|
+
continue;
|
|
25724
|
+
}
|
|
25725
|
+
linkNode = $createLinkNode(url, {
|
|
25726
|
+
rel,
|
|
25727
|
+
target,
|
|
25728
|
+
title
|
|
25729
|
+
});
|
|
25730
|
+
node.insertAfter(linkNode);
|
|
25731
|
+
linkNode.append(node);
|
|
25732
|
+
}
|
|
25733
|
+
});
|
|
25734
|
+
}
|
|
25735
|
+
function $getAncestor(node, predicate) {
|
|
25736
|
+
let parent = node;
|
|
25737
|
+
while (parent !== null && parent.getParent() !== null && !predicate(parent)) parent = parent.getParentOrThrow();
|
|
25738
|
+
return predicate(parent) ? parent : null;
|
|
25739
|
+
}
|
|
25740
|
+
const PHONE_NUMBER_REGEX = /^\+?[\d\s()-]{5,}$/;
|
|
25741
|
+
/**
|
|
25742
|
+
* Formats a URL string by adding appropriate protocol if missing
|
|
25743
|
+
*
|
|
25744
|
+
* @param url - URL to format
|
|
25745
|
+
* @returns Formatted URL with appropriate protocol
|
|
25746
|
+
*/
|
|
25747
|
+
function formatUrl(url) {
|
|
25748
|
+
if (/^[a-z][\d+.a-z-]*:/i.test(url)) return url;
|
|
25749
|
+
else if (/^[#./]/.test(url)) return url;
|
|
25750
|
+
else if (url.includes("@")) return `mailto:${url}`;
|
|
25751
|
+
else if (PHONE_NUMBER_REGEX.test(url)) return `tel:${url}`;
|
|
25752
|
+
return url;
|
|
25753
|
+
}
|
|
25754
|
+
//#endregion
|
|
25755
|
+
//#region src/plugins/link/command/index.ts
|
|
25756
|
+
const INSERT_LINK_COMMAND = createCommand("INSERT_LINK_COMMAND");
|
|
25757
|
+
const UPDATE_LINK_TEXT_COMMAND = createCommand("UPDATE_LINK_TEXT_COMMAND");
|
|
25758
|
+
function registerLinkCommand(editor) {
|
|
25759
|
+
return mergeRegister(editor.registerCommand(INSERT_LINK_COMMAND, (payload) => {
|
|
25760
|
+
const { url, title = url } = payload;
|
|
25761
|
+
editor.update(() => {
|
|
25762
|
+
const linkNode = $createLinkNode(url, { title });
|
|
25763
|
+
const textNode = $createTextNode(title);
|
|
25764
|
+
linkNode.append(textNode);
|
|
25765
|
+
$insertNodes([linkNode]);
|
|
25766
|
+
});
|
|
25767
|
+
return false;
|
|
25768
|
+
}, COMMAND_PRIORITY_EDITOR), editor.registerCommand(UPDATE_LINK_TEXT_COMMAND, (payload) => {
|
|
25769
|
+
const { key, text } = payload;
|
|
25770
|
+
editor.update(() => {
|
|
25771
|
+
const linkNode = $getNodeByKey(key);
|
|
25772
|
+
if (linkNode) {
|
|
25773
|
+
const newLinkNode = $createLinkNode(linkNode.getURL(), { title: text });
|
|
25774
|
+
const textNode = $createTextNode(text);
|
|
25775
|
+
newLinkNode.append(textNode);
|
|
25776
|
+
linkNode?.replace(newLinkNode);
|
|
25777
|
+
newLinkNode.select(1);
|
|
25778
|
+
}
|
|
25779
|
+
});
|
|
25780
|
+
return false;
|
|
25781
|
+
}, COMMAND_PRIORITY_EDITOR));
|
|
25782
|
+
}
|
|
25783
|
+
//#endregion
|
|
25784
|
+
//#region src/plugins/link/service/i-link-service.ts
|
|
25785
|
+
const ILinkService = genServiceId("LinkService");
|
|
25786
|
+
var LinkService = class extends EventEmitter {
|
|
25787
|
+
constructor(..._args) {
|
|
25788
|
+
super(..._args);
|
|
25789
|
+
this._enableLinkToolbar = true;
|
|
25790
|
+
}
|
|
25791
|
+
get enableLinkToolbar() {
|
|
25792
|
+
return this._enableLinkToolbar;
|
|
25793
|
+
}
|
|
25794
|
+
setLinkToolbar(enable) {
|
|
25795
|
+
this._enableLinkToolbar = enable;
|
|
25796
|
+
this.emit("linkToolbarChange", enable);
|
|
25797
|
+
}
|
|
25798
|
+
};
|
|
25799
|
+
//#endregion
|
|
25800
|
+
//#region src/plugins/link/utils/index.ts
|
|
25801
|
+
const SUPPORTED_URL_PROTOCOLS = new Set([
|
|
25802
|
+
"http:",
|
|
25803
|
+
"https:",
|
|
25804
|
+
"mailto:",
|
|
25805
|
+
"sms:",
|
|
25806
|
+
"tel:"
|
|
25807
|
+
]);
|
|
25808
|
+
function sanitizeUrl(url) {
|
|
25809
|
+
try {
|
|
25810
|
+
const parsedUrl = new URL(url);
|
|
25811
|
+
if (!SUPPORTED_URL_PROTOCOLS.has(parsedUrl.protocol)) return "about:blank";
|
|
25812
|
+
} catch {
|
|
25813
|
+
return url;
|
|
25814
|
+
}
|
|
25815
|
+
return url;
|
|
25816
|
+
}
|
|
25817
|
+
const urlRegExp = /* @__PURE__ */ new RegExp(/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\w$&+,:;=-]+@)?[\d.A-Za-z-]+|(?:www.|[\w$&+,:;=-]+@)[\d.A-Za-z-]+)((?:\/[%+./~\w-_]*)?\??[\w%&+.;=@-]*#?\w*)?)/);
|
|
25818
|
+
function extractUrlFromText(text) {
|
|
25819
|
+
const match = urlRegExp.exec(text);
|
|
25820
|
+
if (!match) return null;
|
|
25821
|
+
const raw = match[0];
|
|
25822
|
+
const start = match.index ?? text.indexOf(raw);
|
|
25823
|
+
const trimmed = raw.replace(/[)\],.;:]+$/u, "");
|
|
25824
|
+
return {
|
|
25825
|
+
index: start,
|
|
25826
|
+
length: trimmed.length,
|
|
25827
|
+
url: trimmed
|
|
25828
|
+
};
|
|
25829
|
+
}
|
|
25830
|
+
function getSelectedNode(selection) {
|
|
25831
|
+
const anchor = selection.anchor;
|
|
25832
|
+
const focus = selection.focus;
|
|
25833
|
+
const anchorNode = selection.anchor.getNode();
|
|
25834
|
+
const focusNode = selection.focus.getNode();
|
|
25835
|
+
if (anchorNode === focusNode) return anchorNode;
|
|
25836
|
+
if (selection.isBackward()) return $isAtNodeEnd(focus) ? anchorNode : focusNode;
|
|
25837
|
+
else return $isAtNodeEnd(anchor) ? anchorNode : focusNode;
|
|
25838
|
+
}
|
|
25839
|
+
//#endregion
|
|
25840
|
+
//#region src/plugins/link/plugin/registry.ts
|
|
25841
|
+
function registerLinkCommands(editor, kernel, options) {
|
|
25842
|
+
const { validateUrl, attributes, enableHotkey = true } = options || {};
|
|
25843
|
+
const state = { isLink: false };
|
|
25844
|
+
const registrations = [editor.registerUpdateListener(() => {
|
|
25845
|
+
const selection = editor.getEditorState().read(() => $getSelection());
|
|
25846
|
+
if (!selection) return;
|
|
25847
|
+
if ($isRangeSelection(selection)) editor.getEditorState().read(() => {
|
|
25848
|
+
const node = getSelectedNode(selection);
|
|
25849
|
+
state.isLink = $isLinkNode(node.getParent()) || $isLinkNode(node);
|
|
25850
|
+
});
|
|
25851
|
+
else state.isLink = false;
|
|
25852
|
+
}), editor.registerCommand(TOGGLE_LINK_COMMAND, (payload) => {
|
|
25853
|
+
if (payload === null) {
|
|
25854
|
+
$toggleLink(payload);
|
|
25855
|
+
return true;
|
|
25856
|
+
} else if (typeof payload === "string") {
|
|
25857
|
+
if (validateUrl === void 0 || validateUrl(payload)) {
|
|
25858
|
+
$toggleLink(payload, attributes);
|
|
25859
|
+
return true;
|
|
25860
|
+
}
|
|
25861
|
+
return false;
|
|
25862
|
+
} else {
|
|
25863
|
+
const { url, target, rel, title } = payload;
|
|
25864
|
+
$toggleLink(url, {
|
|
25865
|
+
...attributes,
|
|
25866
|
+
rel,
|
|
25867
|
+
target,
|
|
25868
|
+
title
|
|
25869
|
+
});
|
|
25870
|
+
return true;
|
|
25871
|
+
}
|
|
25872
|
+
}, COMMAND_PRIORITY_LOW)];
|
|
25873
|
+
registrations.push(kernel.registerHotkey(HotkeyEnum.Link, () => {
|
|
25874
|
+
if (state.isLink) {
|
|
25875
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
25876
|
+
return;
|
|
25877
|
+
}
|
|
25878
|
+
let nextUrl = sanitizeUrl("https://");
|
|
25879
|
+
let expandTo = null;
|
|
25880
|
+
editor.getEditorState().read(() => {
|
|
25881
|
+
const selection = $getSelection();
|
|
25882
|
+
if ($isRangeSelection(selection)) if (!selection.isCollapsed()) {
|
|
25883
|
+
const maybeUrl = formatUrl(selection.getTextContent().trim());
|
|
25884
|
+
if (validateUrl?.(maybeUrl)) nextUrl = maybeUrl;
|
|
25885
|
+
} else {
|
|
25886
|
+
const found = extractUrlFromText(selection.anchor.getNode().getTextContent());
|
|
25887
|
+
if (found && validateUrl?.(formatUrl(found.url))) {
|
|
25888
|
+
nextUrl = formatUrl(found.url);
|
|
25889
|
+
expandTo = {
|
|
25890
|
+
index: found.index,
|
|
25891
|
+
length: found.length
|
|
25892
|
+
};
|
|
25893
|
+
}
|
|
25894
|
+
}
|
|
25895
|
+
});
|
|
25896
|
+
editor.update(() => {
|
|
25897
|
+
if (expandTo) {
|
|
25898
|
+
const selection = $getSelection();
|
|
25899
|
+
if ($isRangeSelection(selection)) {
|
|
25900
|
+
const anchorNode = selection.anchor.getNode();
|
|
25901
|
+
selection.anchor.set(anchorNode.getKey(), expandTo.index, "text");
|
|
25902
|
+
selection.focus.set(anchorNode.getKey(), expandTo.index + expandTo.length, "text");
|
|
25903
|
+
}
|
|
25904
|
+
}
|
|
25905
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, nextUrl);
|
|
25906
|
+
});
|
|
25907
|
+
}, {
|
|
25908
|
+
enabled: enableHotkey,
|
|
25909
|
+
preventDefault: true,
|
|
25910
|
+
stopPropagation: true
|
|
25911
|
+
}));
|
|
25912
|
+
return mergeRegister(...registrations);
|
|
25913
|
+
}
|
|
25914
|
+
//#endregion
|
|
25915
|
+
//#region src/plugins/link/plugin/index.ts
|
|
25916
|
+
const LinkPlugin = class extends KernelPlugin {
|
|
25917
|
+
static {
|
|
25918
|
+
this.pluginName = "LinkPlugin";
|
|
25919
|
+
}
|
|
25920
|
+
constructor(kernel, config) {
|
|
25921
|
+
super();
|
|
25922
|
+
this.kernel = kernel;
|
|
25923
|
+
this.config = config;
|
|
25924
|
+
this.linkRegex = /^https?:\/\/\S+$/;
|
|
25925
|
+
this.service = new LinkService();
|
|
25926
|
+
kernel.registerNodes([LinkNode, AutoLinkNode]);
|
|
25927
|
+
kernel.registerService(ILinkService, this.service);
|
|
25928
|
+
if (config?.theme) kernel.registerThemes(config.theme);
|
|
25929
|
+
if (config?.linkRegex) this.linkRegex = config.linkRegex;
|
|
25930
|
+
}
|
|
25931
|
+
onInit(editor) {
|
|
25932
|
+
this.register(registerLinkCommand(editor));
|
|
25933
|
+
this.register(registerLinkCommands(editor, this.kernel, {
|
|
25934
|
+
attributes: this.config?.attributes,
|
|
25935
|
+
enableHotkey: this.config?.enableHotkey,
|
|
25936
|
+
validateUrl: this.config?.validateUrl
|
|
25937
|
+
}));
|
|
25938
|
+
this.register(editor.registerCommand(PASTE_COMMAND, (payload) => {
|
|
25939
|
+
const { clipboardData } = payload;
|
|
25940
|
+
if (clipboardData && clipboardData.types && clipboardData.types.length === 1 && clipboardData.types[0] === "text/plain") {
|
|
25941
|
+
const data = clipboardData.getData("text/plain").trim();
|
|
25942
|
+
if (this.linkRegex.test(data)) {
|
|
25943
|
+
payload.stopImmediatePropagation();
|
|
25944
|
+
payload.preventDefault();
|
|
25945
|
+
editor.dispatchCommand(INSERT_LINK_COMMAND, { url: data });
|
|
25946
|
+
return true;
|
|
25947
|
+
}
|
|
25948
|
+
}
|
|
25949
|
+
return false;
|
|
25950
|
+
}, COMMAND_PRIORITY_NORMAL));
|
|
25951
|
+
this.registerMarkdown();
|
|
25952
|
+
this.registerLiteXml();
|
|
25953
|
+
}
|
|
25954
|
+
registerLiteXml() {
|
|
25955
|
+
const litexmlService = this.kernel.requireService(ILitexmlService);
|
|
25956
|
+
if (!litexmlService) return;
|
|
25957
|
+
litexmlService.registerXMLWriter(LinkNode.getType(), (node, ctx) => {
|
|
25958
|
+
if ($isLinkNode(node)) {
|
|
25959
|
+
const attributes = { href: node.getURL() };
|
|
25960
|
+
return ctx.createXmlNode("a", attributes);
|
|
25961
|
+
}
|
|
25962
|
+
return false;
|
|
25963
|
+
});
|
|
25964
|
+
litexmlService.registerXMLReader("a", (xmlNode, children) => {
|
|
25965
|
+
return [INodeHelper.createElementNode("link", {
|
|
25966
|
+
children,
|
|
25967
|
+
direction: "ltr",
|
|
25968
|
+
format: "",
|
|
25969
|
+
indent: 0,
|
|
25970
|
+
type: "link",
|
|
25971
|
+
url: xmlNode.getAttribute("href") || "",
|
|
25972
|
+
version: 1
|
|
25973
|
+
})];
|
|
25974
|
+
});
|
|
25975
|
+
}
|
|
25976
|
+
registerMarkdown() {
|
|
25977
|
+
this.kernel.requireService(IMarkdownShortCutService)?.registerMarkdownShortCut({
|
|
25978
|
+
regExp: /\[([^[]+)]\(([^\s()]+)(?:\s"((?:[^"]*\\")*[^"]*)"\s*)?\)\s?$/,
|
|
25979
|
+
replace: (textNode, match) => {
|
|
25980
|
+
const [, linkText, linkUrl, linkTitle] = match;
|
|
25981
|
+
const linkNode = $createLinkNode(linkUrl, { title: linkTitle });
|
|
25982
|
+
const linkTextNode = $createTextNode(linkText);
|
|
25983
|
+
linkTextNode.setFormat(textNode.getFormat());
|
|
25984
|
+
linkNode.append(linkTextNode);
|
|
25985
|
+
textNode.replace(linkNode);
|
|
25986
|
+
return linkTextNode;
|
|
25987
|
+
},
|
|
25988
|
+
trigger: ")",
|
|
25989
|
+
type: "text-match"
|
|
25990
|
+
});
|
|
25991
|
+
this.kernel.requireService(IMarkdownShortCutService)?.registerMarkdownWriter(LinkNode.getType(), (ctx, node) => {
|
|
25992
|
+
if ($isLinkNode(node)) ctx.wrap("[", `](${node.getURL()})`);
|
|
25993
|
+
});
|
|
25994
|
+
this.kernel.requireService(IMarkdownShortCutService)?.registerMarkdownReader("link", (node, children) => {
|
|
25995
|
+
return [INodeHelper.createElementNode("link", {
|
|
25996
|
+
children,
|
|
25997
|
+
direction: "ltr",
|
|
25998
|
+
format: "",
|
|
25999
|
+
indent: 0,
|
|
26000
|
+
title: node.title || void 0,
|
|
26001
|
+
type: "link",
|
|
26002
|
+
url: node.url || "",
|
|
26003
|
+
version: 1
|
|
26004
|
+
})];
|
|
26005
|
+
});
|
|
26006
|
+
}
|
|
26007
|
+
};
|
|
26008
|
+
//#endregion
|
|
25310
26009
|
//#region src/utils/cx.ts
|
|
25311
26010
|
const cx = (...classNames) => classNames.filter(Boolean).join(" ");
|
|
25312
26011
|
//#endregion
|
|
@@ -26113,6 +26812,7 @@ const normalizeLegacyEditorData = (editorData) => {
|
|
|
26113
26812
|
const DEFAULT_HEADLESS_EDITOR_PLUGINS = [
|
|
26114
26813
|
[CommonPlugin, { enableHotkey: false }],
|
|
26115
26814
|
MarkdownPlugin,
|
|
26815
|
+
[LinkPlugin, { enableHotkey: false }],
|
|
26116
26816
|
CodePlugin,
|
|
26117
26817
|
HeadlessCodeblockPlugin,
|
|
26118
26818
|
HRPlugin,
|