@37signals/lexxy 0.8.2-beta → 0.8.6-beta
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/dist/lexxy.esm.js +1640 -1188
- package/dist/lexxy_helpers.esm.js +116 -1
- package/dist/stylesheets/lexxy-content.css +10 -10
- package/dist/stylesheets/lexxy-editor.css +169 -11
- package/package.json +1 -1
package/dist/lexxy.esm.js
CHANGED
|
@@ -9,21 +9,21 @@ import 'prismjs/components/prism-bash';
|
|
|
9
9
|
import 'prismjs/components/prism-json';
|
|
10
10
|
import 'prismjs/components/prism-diff';
|
|
11
11
|
import DOMPurify from 'dompurify';
|
|
12
|
-
import { getStyleObjectFromCSS, getCSSFromStyleObject, $isAtNodeEnd, $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/selection';
|
|
13
|
-
import { SKIP_DOM_SELECTION_TAG, $getSelection, $isRangeSelection, $getNodeByKey, $isTextNode, $createRangeSelection, $setSelection, DecoratorNode, $createNodeSelection, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $createParagraphNode, TextNode, createCommand, createState, defineExtension, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND,
|
|
12
|
+
import { getStyleObjectFromCSS, getCSSFromStyleObject, $isAtNodeEnd, $getSelectionStyleValueForProperty, $patchStyleText, $setBlocksType } from '@lexical/selection';
|
|
13
|
+
import { SKIP_DOM_SELECTION_TAG, $getSelection, $isRangeSelection, $getNodeByKey, $isTextNode, $createRangeSelection, $setSelection, DecoratorNode, $createTextNode, $createNodeSelection, $isDecoratorNode, $isLineBreakNode, $isElementNode, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $createParagraphNode, TextNode, createCommand, createState, defineExtension, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $getEditor, $getNearestRootOrShadowRoot, $isNodeSelection, $getRoot, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $isRootOrShadowRoot, ElementNode, $splitNode, $isParagraphNode, $createLineBreakNode, $isRootNode, ParagraphNode, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, CLEAR_HISTORY_COMMAND, $addUpdateTag, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
|
|
14
14
|
import { buildEditorFromExtensions } from '@lexical/extension';
|
|
15
|
-
import { ListNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, ListItemNode, $getListDepth, $isListItemNode, $isListNode,
|
|
15
|
+
import { ListNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, ListItemNode, $getListDepth, $isListItemNode, $isListNode, registerList } from '@lexical/list';
|
|
16
16
|
import { $createAutoLinkNode, $toggleLink, LinkNode, $createLinkNode, AutoLinkNode, $isLinkNode } from '@lexical/link';
|
|
17
17
|
import { registerPlainText } from '@lexical/plain-text';
|
|
18
|
-
import { RichTextExtension, $
|
|
18
|
+
import { RichTextExtension, $isQuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
19
19
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
20
|
-
import { $isCodeNode, CodeHighlightNode, $isCodeHighlightNode,
|
|
20
|
+
import { $isCodeNode, CodeHighlightNode, CodeNode, $createCodeNode, $isCodeHighlightNode, $createCodeHighlightNode, normalizeCodeLang, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
|
|
21
21
|
import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown';
|
|
22
22
|
import { createEmptyHistoryState, registerHistory } from '@lexical/history';
|
|
23
|
-
import { createElement, createAttachmentFigure, isPreviewableImage, dispatch, parseHtml, addBlockSpacing, generateDomId } from './lexxy_helpers.esm.js';
|
|
23
|
+
import { createElement, extractPlainTextFromHtml, createAttachmentFigure, isPreviewableImage, dispatch, parseHtml, addBlockSpacing, generateDomId } from './lexxy_helpers.esm.js';
|
|
24
24
|
export { highlightCode as highlightAll, highlightCode } from './lexxy_helpers.esm.js';
|
|
25
25
|
import { INSERT_TABLE_COMMAND, $getTableCellNodeFromLexicalNode, TableCellNode, TableNode, TableRowNode, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive, TableCellHeaderStates, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $findCellNode, $getElementForTableNode } from '@lexical/table';
|
|
26
|
-
import { $getNearestNodeOfType, $wrapNodeInElement, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $descendantsMatching } from '@lexical/utils';
|
|
26
|
+
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $descendantsMatching } from '@lexical/utils';
|
|
27
27
|
import { marked } from 'marked';
|
|
28
28
|
import { $insertDataTransferForRichText } from '@lexical/clipboard';
|
|
29
29
|
|
|
@@ -81,7 +81,9 @@ const presets = new Configuration({
|
|
|
81
81
|
markdown: true,
|
|
82
82
|
multiLine: true,
|
|
83
83
|
richText: true,
|
|
84
|
-
toolbar:
|
|
84
|
+
toolbar: {
|
|
85
|
+
upload: "both"
|
|
86
|
+
},
|
|
85
87
|
highlight: {
|
|
86
88
|
buttons: {
|
|
87
89
|
color: range(1, 9).map(n => `var(--highlight-${n})`),
|
|
@@ -260,11 +262,37 @@ var ToolbarIcons = {
|
|
|
260
262
|
<path d="M9.2981 1.91602C10.111 1.91604 10.9109 2.02122 11.6975 2.23096C12.4855 2.44111 13.1683 2.74431 13.7417 3.14429L13.8655 3.23071L13.8083 3.36987L13.1726 4.91235L13.0869 5.1189L12.8987 4.99878C12.3487 4.64881 11.761 4.38633 11.1365 4.21143L11.1328 4.20996C10.585 4.04564 10.0484 3.95419 9.52295 3.93384L9.2981 3.92944C8.22329 3.92944 7.44693 4.12611 6.94043 4.49121C6.44619 4.85665 6.20874 5.31616 6.20874 5.88135L6.21533 6.03296C6.24495 6.37662 6.37751 6.65526 6.61011 6.87964L6.72144 6.97632C6.98746 7.19529 7.30625 7.37584 7.68018 7.51538L8.05151 7.63184C8.45325 7.75061 8.94669 7.87679 9.53247 8.01123L9.53467 8.01196C10.1213 8.15305 10.6426 8.29569 11.0991 8.4375H15C15.5178 8.4375 15.9375 8.85723 15.9375 9.375C15.9375 9.89277 15.5178 10.3125 15 10.3125H3C2.48223 10.3125 2.0625 9.89277 2.0625 9.375C2.0625 8.85723 2.48223 8.4375 3 8.4375H4.93726C4.83783 8.34526 4.74036 8.24896 4.64795 8.146L4.64502 8.14233C4.1721 7.58596 3.94482 6.85113 3.94482 5.95825C3.94483 5.20441 4.14059 4.51965 4.53369 3.90967L4.53516 3.90747C4.94397 3.29427 5.55262 2.81114 6.34863 2.45288C7.15081 2.0919 8.13683 1.91602 9.2981 1.91602Z"/>
|
|
261
263
|
</svg>`,
|
|
262
264
|
|
|
265
|
+
"underline":
|
|
266
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
267
|
+
<path d="M14 14C14.5523 14 15 14.4477 15 15C15 15.5523 14.5523 16 14 16H4C3.44772 16 3 15.5523 3 15C3 14.4477 3.44772 14 4 14H14Z"/>
|
|
268
|
+
<path d="M12.625 1.59375C13.25 1.59375 13.625 1.97656 13.625 2.64062V9.02344C13.625 11.4844 11.8516 13.1875 8.99219 13.1875C6.14062 13.1875 4.35938 11.4844 4.35938 9.02344V2.64062C4.35938 1.97656 4.74219 1.59375 5.36719 1.59375C6 1.59375 6.375 1.97656 6.375 2.64062V8.84375C6.375 10.3828 7.32031 11.4297 8.99219 11.4297C10.6641 11.4297 11.6172 10.3828 11.6172 8.84375V2.64062C11.6172 1.97656 11.9922 1.59375 12.625 1.59375Z"/>
|
|
269
|
+
</svg>`,
|
|
270
|
+
|
|
263
271
|
"heading":
|
|
264
272
|
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
265
273
|
<path d="M11.5 2C12.0523 2 12.5 2.44772 12.5 3V3.5C12.5 4.05228 12.0523 4.5 11.5 4.5H8V15C8 15.5523 7.55228 16 7 16H6.5C5.94772 16 5.5 15.5523 5.5 15V4.5H2C1.44772 4.5 1 4.05228 1 3.5V3C1 2.44772 1.44772 2 2 2H11.5ZM16 7C16.5523 7 17 7.44772 17 8V8.5C17 9.05228 16.5523 9.5 16 9.5H15V15C15 15.5523 14.5523 16 14 16H13.5C12.9477 16 12.5 15.5523 12.5 15V9.5H11.5C10.9477 9.5 10.5 9.05228 10.5 8.5V8C10.5 7.44772 10.9477 7 11.5 7H16Z"/>
|
|
266
274
|
</svg>`,
|
|
267
275
|
|
|
276
|
+
"h2":
|
|
277
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
278
|
+
<path d="M8.12207 4.30078C8.84668 4.30078 9.25684 4.74512 9.25684 5.51758V12.5518C9.25677 13.3241 8.84662 13.7686 8.12207 13.7686C7.39752 13.7686 6.9942 13.3309 6.99414 12.5518V9.87207H3.56934V12.5518C3.56927 13.3241 3.15912 13.7686 2.43457 13.7686C1.71002 13.7686 1.3067 13.3309 1.30664 12.5518V5.51758C1.30664 4.73828 1.70996 4.30078 2.43457 4.30078C3.15918 4.30078 3.56934 4.74512 3.56934 5.51758V8.07422H6.99414V5.51758C6.99414 4.73828 7.39746 4.30078 8.12207 4.30078ZM13.6445 4.19824C15.5244 4.19824 16.8984 5.34668 16.8984 6.91211C16.8984 7.8759 16.4335 8.7237 15.292 9.84473L13.3438 11.8135V11.9092H16.1875C16.8232 11.9092 17.2197 12.251 17.2197 12.8115C17.2196 13.3651 16.83 13.7002 16.1875 13.7002H11.5117C10.8487 13.7002 10.4112 13.3241 10.4111 12.75C10.4111 12.3399 10.6368 11.9843 11.3203 11.3145L13.6855 8.88086C14.4169 8.13583 14.7245 7.64349 14.7246 7.12402C14.7246 6.4541 14.2393 6.00293 13.5215 6.00293C12.9404 6.00293 12.5166 6.29688 12.2158 6.90527C11.9151 7.37002 11.6552 7.54785 11.2588 7.54785C10.7188 7.54785 10.3429 7.17861 10.3428 6.65918C10.3428 5.3877 11.7783 4.19824 13.6445 4.19824Z"/>
|
|
279
|
+
</svg>`,
|
|
280
|
+
|
|
281
|
+
"h3":
|
|
282
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
283
|
+
<path d="M13.5967 4.19824C15.5928 4.19824 16.9873 5.23047 16.9873 6.7002C16.9872 7.705 16.2421 8.60059 15.2988 8.7168V8.86719C16.4199 8.94923 17.2607 9.89942 17.2607 11.0889C17.2606 12.7362 15.7362 13.9053 13.583 13.9053C11.6827 13.9053 10.1925 12.873 10.1924 11.7041C10.1924 11.1846 10.5547 10.8154 11.0537 10.8154C11.3818 10.8154 11.6553 10.9727 11.9629 11.3555C12.3799 11.9159 12.92 12.2031 13.583 12.2031C14.4853 12.2031 15.0731 11.7313 15.0732 11C15.0732 10.2754 14.4785 9.7832 13.5898 9.7832H13.0361C12.5645 9.7832 12.2159 9.4208 12.2158 8.92188C12.2158 8.44336 12.5576 8.07422 13.0361 8.07422H13.5693C14.3075 8.07422 14.8544 7.60928 14.8545 6.97363C14.8545 6.33789 14.3213 5.90039 13.5557 5.90039C12.9678 5.90039 12.5029 6.16016 12.0859 6.71387C11.8399 7.03508 11.5527 7.17871 11.1973 7.17871C10.671 7.17871 10.295 6.82314 10.2949 6.31738C10.2949 5.18945 11.751 4.19824 13.5967 4.19824ZM8.0332 4.30078C8.75781 4.30078 9.16797 4.74512 9.16797 5.51758V12.5518C9.1679 13.3241 8.75776 13.7686 8.0332 13.7686C7.30865 13.7686 6.90534 13.3309 6.90527 12.5518V9.87207H3.48047V12.5518C3.4804 13.3241 3.07026 13.7686 2.3457 13.7686C1.62115 13.7686 1.21784 13.3309 1.21777 12.5518V5.51758C1.21777 4.73828 1.62109 4.30078 2.3457 4.30078C3.07031 4.30078 3.48047 4.74512 3.48047 5.51758V8.07422H6.90527V5.51758C6.90527 4.73828 7.30859 4.30078 8.0332 4.30078Z"/>
|
|
284
|
+
</svg>`,
|
|
285
|
+
|
|
286
|
+
"h4":
|
|
287
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
288
|
+
<path d="M14.6357 4.22559C15.7432 4.22559 16.4336 4.80664 16.4336 5.73633V10.3164H16.7275C17.2881 10.3164 17.6436 10.6787 17.6436 11.2256C17.6435 11.7655 17.3017 12.1006 16.7275 12.1006H16.4336V12.6611C16.4335 13.3515 16.0234 13.7891 15.374 13.7891C14.7247 13.7891 14.3282 13.3583 14.3281 12.6611V12.1006H11.04C10.2335 12.1006 9.76863 11.6766 9.76855 10.918C9.76855 10.5762 9.85064 10.3026 10.1104 9.74219C10.7666 8.42289 11.5733 7.0146 12.5713 5.54492C13.2549 4.56738 13.7812 4.22559 14.6357 4.22559ZM7.88965 4.30078C8.61426 4.30078 9.02441 4.74512 9.02441 5.51758V12.5518C9.02435 13.3241 8.6142 13.7686 7.88965 13.7686C7.1651 13.7686 6.76178 13.3309 6.76172 12.5518V9.87207H3.33691V12.5518C3.33685 13.3241 2.9267 13.7686 2.20215 13.7686C1.4776 13.7686 1.07428 13.3309 1.07422 12.5518V5.51758C1.07422 4.73828 1.47754 4.30078 2.20215 4.30078C2.92676 4.30078 3.33691 4.74512 3.33691 5.51758V8.07422H6.76172V5.51758C6.76172 4.73828 7.16504 4.30078 7.88965 4.30078ZM14.2188 6.07812C13.6035 7.02841 12.2158 9.48929 11.7988 10.2686V10.3164H14.3281V6.07812H14.2188Z"/>
|
|
289
|
+
</svg>`,
|
|
290
|
+
|
|
291
|
+
"paragraph":
|
|
292
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
293
|
+
<path d="M9 12C9.55228 12 10 12.4477 10 13C10 13.5523 9.55228 14 9 14H3C2.44772 14 2 13.5523 2 13C2 12.4477 2.44772 12 3 12H9ZM15 8C15.5523 8 16 8.44772 16 9C16 9.55228 15.5523 10 15 10H3C2.44772 10 2 9.55228 2 9C2 8.44772 2.44772 8 3 8H15ZM15 4C15.5523 4 16 4.44772 16 5C16 5.55228 15.5523 6 15 6H3C2.44772 6 2 5.55228 2 5C2 4.44772 2.44772 4 3 4H15Z"/>
|
|
294
|
+
</svg>`,
|
|
295
|
+
|
|
268
296
|
"highlight":
|
|
269
297
|
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
270
298
|
<path d="M16.4564 14.4272C17.1356 15.5592 16.3204 17.0002 15.0003 17.0004C13.68 17.0004 12.864 15.5593 13.5433 14.4272L15.0003 12.0004L16.4564 14.4272ZM5.1214 1.70746C5.51192 1.31693 6.14494 1.31693 6.53546 1.70746L9.7171 4.8891L13.2532 8.42426C14.2295 9.40056 14.2295 10.9841 13.2532 11.9604L9.7171 15.4955C8.74078 16.4718 7.15822 16.4718 6.18195 15.4955L2.64679 11.9604C1.67048 10.9841 1.67048 9.40057 2.64679 8.42426L6.18195 4.8891C6.30299 4.76805 6.43323 4.66177 6.57062 4.57074L5.1214 3.12152C4.73091 2.73104 4.73099 2.09799 5.1214 1.70746ZM8.30304 6.30316C8.10776 6.10815 7.79119 6.10799 7.59601 6.30316L4.06085 9.83929L3.9964 9.91742C3.88661 10.0838 3.88645 10.3019 3.9964 10.4682L4.02277 10.5004H11.8763C12.0312 10.3043 12.02 10.0205 11.8392 9.83929L8.30304 6.30316Z"/>
|
|
@@ -300,6 +328,11 @@ var ToolbarIcons = {
|
|
|
300
328
|
<path d="M2.84155 6V3.01367H2.79053L1.85596 3.64478V2.79614L2.84155 2.12476H3.82715V6H2.84155Z"/>
|
|
301
329
|
</svg>`,
|
|
302
330
|
|
|
331
|
+
"image":
|
|
332
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
333
|
+
<path d="M14 2C15.6569 2 17 3.34315 17 5V13C17 14.6569 15.6569 16 14 16H4C2.34315 16 1 14.6569 1 13V5C1 3.34315 2.34315 2 4 2H14ZM3.06348 13.3496C3.2053 13.7294 3.57078 14 4 14H13.5859L11 11.4141L9.70703 12.707C9.31651 13.0976 8.68349 13.0976 8.29297 12.707C7.90244 12.3165 7.90244 11.6835 8.29297 11.293L8.58594 11L7 9.41406L3.06348 13.3496ZM4 4C3.44772 4 3 4.44772 3 5V10.5859L6.29297 7.29297L6.36914 7.22461C6.76191 6.90427 7.34092 6.92686 7.70703 7.29297L10 9.58594L10.293 9.29297L10.3691 9.22461C10.7619 8.90427 11.3409 8.92686 11.707 9.29297L15 12.5859V5C15 4.44772 14.5523 4 14 4H4ZM12.5 5C13.3284 5 14 5.67157 14 6.5C14 7.32843 13.3284 8 12.5 8C11.6716 8 11 7.32843 11 6.5C11 5.67157 11.6716 5 12.5 5Z"/>
|
|
334
|
+
</svg>`,
|
|
335
|
+
|
|
303
336
|
"attachment":
|
|
304
337
|
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
305
338
|
<path d="M13 13.5V6C13 4.067 11.433 2.5 9.5 2.5C7.567 2.5 6 4.067 6 6V13.5C6 14.6046 6.89543 15.5 8 15.5H8.23047C9.20759 15.5 10 14.7076 10 13.7305V7C10 6.72386 9.77614 6.5 9.5 6.5C9.22386 6.5 9 6.72386 9 7V12.5C9 13.0523 8.55228 13.5 8 13.5C7.44772 13.5 7 13.0523 7 12.5V7C7 5.61929 8.11929 4.5 9.5 4.5C10.8807 4.5 12 5.61929 12 7V13.7305C12 15.8122 10.3122 17.5 8.23047 17.5H8C5.79086 17.5 4 15.7091 4 13.5V6C4 2.96243 6.46243 0.5 9.5 0.5C12.5376 0.5 15 2.96243 15 6V13.5C15 14.0523 14.5523 14.5 14 14.5C13.4477 14.5 13 14.0523 13 13.5Z"/>
|
|
@@ -360,6 +393,14 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
360
393
|
}
|
|
361
394
|
}
|
|
362
395
|
|
|
396
|
+
configure(config) {
|
|
397
|
+
if (typeof config === "object" && config !== null) {
|
|
398
|
+
for (const [ button, value ] of Object.entries(config)) {
|
|
399
|
+
this.setAttribute(`data-${button}`, value);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
363
404
|
setEditor(editorElement) {
|
|
364
405
|
this.editorElement = editorElement;
|
|
365
406
|
this.editor = editorElement.editor;
|
|
@@ -525,19 +566,29 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
525
566
|
const anchorNode = selection.anchor.getNode();
|
|
526
567
|
if (!anchorNode.getParent()) { return }
|
|
527
568
|
|
|
528
|
-
const { isBold, isItalic, isStrikethrough, isHighlight, isInLink, isInQuote, isInHeading,
|
|
529
|
-
isInCode, isInList, listType, isInTable } = this.selection.getFormat();
|
|
569
|
+
const { isBold, isItalic, isStrikethrough, isUnderline, isHighlight, isInLink, isInQuote, isInHeading,
|
|
570
|
+
headingTag, isInCode, isInList, listType, isInTable } = this.selection.getFormat();
|
|
530
571
|
|
|
531
572
|
this.#setButtonPressed("bold", isBold);
|
|
532
573
|
this.#setButtonPressed("italic", isItalic);
|
|
574
|
+
|
|
575
|
+
this.#setButtonPressed("format", isInHeading || isStrikethrough || isUnderline);
|
|
576
|
+
this.#setButtonPressed("paragraph", !isInHeading);
|
|
577
|
+
this.#setButtonPressed("heading-large", headingTag === "h2");
|
|
578
|
+
this.#setButtonPressed("heading-medium", headingTag === "h3");
|
|
579
|
+
this.#setButtonPressed("heading-small", headingTag === "h4");
|
|
533
580
|
this.#setButtonPressed("strikethrough", isStrikethrough);
|
|
581
|
+
this.#setButtonPressed("underline", isUnderline);
|
|
582
|
+
|
|
583
|
+
this.#setButtonPressed("lists", isInList);
|
|
584
|
+
this.#setButtonPressed("unordered-list", isInList && listType === "bullet");
|
|
585
|
+
this.#setButtonPressed("ordered-list", isInList && listType === "number");
|
|
586
|
+
|
|
534
587
|
this.#setButtonPressed("highlight", isHighlight);
|
|
535
588
|
this.#setButtonPressed("link", isInLink);
|
|
536
589
|
this.#setButtonPressed("quote", isInQuote);
|
|
537
|
-
this.#setButtonPressed("heading", isInHeading);
|
|
538
590
|
this.#setButtonPressed("code", isInCode);
|
|
539
|
-
|
|
540
|
-
this.#setButtonPressed("ordered-list", isInList && listType === "number");
|
|
591
|
+
|
|
541
592
|
this.#setButtonPressed("table", isInTable);
|
|
542
593
|
|
|
543
594
|
this.#updateUndoRedoButtonStates();
|
|
@@ -632,7 +683,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
632
683
|
}
|
|
633
684
|
|
|
634
685
|
get #buttons() {
|
|
635
|
-
return Array.from(this.querySelectorAll(":scope > button"))
|
|
686
|
+
return Array.from(this.querySelectorAll(":scope > button:not([data-prevent-overflow='true'])"))
|
|
636
687
|
}
|
|
637
688
|
|
|
638
689
|
get #focusableItems() {
|
|
@@ -645,6 +696,14 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
645
696
|
|
|
646
697
|
static get defaultTemplate() {
|
|
647
698
|
return `
|
|
699
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="image" data-command="uploadAttachments" data-prevent-overflow="true" title="Add images">
|
|
700
|
+
${ToolbarIcons.image}
|
|
701
|
+
</button>
|
|
702
|
+
|
|
703
|
+
<button class="lexxy-editor__toolbar-button lexxy-editor__toolbar-group-end" type="button" name="file" data-command="uploadAttachments" title="Upload files">
|
|
704
|
+
${ToolbarIcons.attachment}
|
|
705
|
+
</button>
|
|
706
|
+
|
|
648
707
|
<button class="lexxy-editor__toolbar-button" type="button" name="bold" data-command="bold" title="Bold">
|
|
649
708
|
${ToolbarIcons.bold}
|
|
650
709
|
</button>
|
|
@@ -653,15 +712,49 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
653
712
|
${ToolbarIcons.italic}
|
|
654
713
|
</button>
|
|
655
714
|
|
|
656
|
-
<
|
|
657
|
-
|
|
658
|
-
|
|
715
|
+
<details class="lexxy-editor__toolbar-dropdown lexxy-editor__toolbar-dropdown--chevron" name="lexxy-dropdown">
|
|
716
|
+
<summary class="lexxy-editor__toolbar-button" name="format" title="Text formatting">
|
|
717
|
+
${ToolbarIcons.heading}
|
|
718
|
+
</summary>
|
|
719
|
+
<div class="lexxy-editor__toolbar-dropdown-list">
|
|
720
|
+
<button type="button" name="paragraph" data-command="setFormatParagraph" title="Paragraph">
|
|
721
|
+
${ToolbarIcons.paragraph} <span>Normal</span>
|
|
722
|
+
</button>
|
|
723
|
+
<button type="button" name="heading-large" data-command="setFormatHeadingLarge" title="Large heading">
|
|
724
|
+
${ToolbarIcons.h2} <span>Large Heading</span>
|
|
725
|
+
</button>
|
|
726
|
+
<button type="button" name="heading-medium" data-command="setFormatHeadingMedium" title="Medium heading">
|
|
727
|
+
${ToolbarIcons.h3} <span>Medium Heading</span>
|
|
728
|
+
</button>
|
|
729
|
+
<button type="button" name="heading-small" data-command="setFormatHeadingSmall" title="Small heading">
|
|
730
|
+
${ToolbarIcons.h4} <span>Small Heading</span>
|
|
731
|
+
</button>
|
|
732
|
+
<div class="separator" role="separator"></div>
|
|
733
|
+
<button type="button" name="strikethrough" data-command="strikethrough" title="Strikethrough">
|
|
734
|
+
${ToolbarIcons.strikethrough} <span>Strikethrough</span>
|
|
735
|
+
</button>
|
|
736
|
+
<button type="button" name="underline" data-command="underline" title="Underline">
|
|
737
|
+
${ToolbarIcons.underline} <span>Underline</span>
|
|
738
|
+
</button>
|
|
739
|
+
</div>
|
|
740
|
+
</details>
|
|
659
741
|
|
|
660
|
-
<button class="lexxy-editor__toolbar-button" type="button" name="heading" data-command="rotateHeadingFormat" title="Heading">
|
|
661
|
-
${ToolbarIcons.heading}
|
|
662
|
-
</button>
|
|
663
742
|
|
|
664
|
-
<details class="lexxy-editor__toolbar-dropdown" name="lexxy-dropdown">
|
|
743
|
+
<details class="lexxy-editor__toolbar-dropdown lexxy-editor__toolbar-dropdown--chevron" name="lexxy-dropdown">
|
|
744
|
+
<summary class="lexxy-editor__toolbar-button" name="lists" title="Lists">
|
|
745
|
+
${ToolbarIcons.ul}
|
|
746
|
+
</summary>
|
|
747
|
+
<div class="lexxy-editor__toolbar-dropdown-list">
|
|
748
|
+
<button type="button" name="unordered-list" data-command="insertUnorderedList" title="Bullet list">
|
|
749
|
+
${ToolbarIcons.ul} <span>Bullets</span>
|
|
750
|
+
</button>
|
|
751
|
+
<button type="button" name="ordered-list" data-command="insertOrderedList" title="Numbered list">
|
|
752
|
+
${ToolbarIcons.ol} <span>Numbers</span>
|
|
753
|
+
</button>
|
|
754
|
+
</div>
|
|
755
|
+
</details>
|
|
756
|
+
|
|
757
|
+
<details class="lexxy-editor__toolbar-dropdown lexxy-editor__toolbar-dropdown--chevron" name="lexxy-dropdown">
|
|
665
758
|
<summary class="lexxy-editor__toolbar-button" name="highlight" title="Color highlight">
|
|
666
759
|
${ToolbarIcons.highlight}
|
|
667
760
|
</summary>
|
|
@@ -694,18 +787,6 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
694
787
|
${ToolbarIcons.code}
|
|
695
788
|
</button>
|
|
696
789
|
|
|
697
|
-
<button class="lexxy-editor__toolbar-button" type="button" name="unordered-list" data-command="insertUnorderedList" title="Bullet list">
|
|
698
|
-
${ToolbarIcons.ul}
|
|
699
|
-
</button>
|
|
700
|
-
|
|
701
|
-
<button class="lexxy-editor__toolbar-button lexxy-editor__toolbar-group-end" type="button" name="ordered-list" data-command="insertOrderedList" title="Numbered list">
|
|
702
|
-
${ToolbarIcons.ol}
|
|
703
|
-
</button>
|
|
704
|
-
|
|
705
|
-
<button class="lexxy-editor__toolbar-button" type="button" name="upload" data-command="uploadAttachments" title="Upload file">
|
|
706
|
-
${ToolbarIcons.attachment}
|
|
707
|
-
</button>
|
|
708
|
-
|
|
709
790
|
<button class="lexxy-editor__toolbar-button" type="button" name="table" data-command="insertTable" title="Insert a table">
|
|
710
791
|
${ToolbarIcons.table}
|
|
711
792
|
</button>
|
|
@@ -713,9 +794,9 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
713
794
|
<button class="lexxy-editor__toolbar-button" type="button" name="divider" data-command="insertHorizontalDivider" title="Insert a divider">
|
|
714
795
|
${ToolbarIcons.hr}
|
|
715
796
|
</button>
|
|
716
|
-
|
|
797
|
+
|
|
717
798
|
<div class="lexxy-editor__toolbar-spacer" role="separator"></div>
|
|
718
|
-
|
|
799
|
+
|
|
719
800
|
<button class="lexxy-editor__toolbar-button" type="button" name="undo" data-command="undo" title="Undo">
|
|
720
801
|
${ToolbarIcons.undo}
|
|
721
802
|
</button>
|
|
@@ -1032,6 +1113,149 @@ class HorizontalDividerNode extends DecoratorNode {
|
|
|
1032
1113
|
}
|
|
1033
1114
|
}
|
|
1034
1115
|
|
|
1116
|
+
function bytesToHumanSize(bytes) {
|
|
1117
|
+
if (bytes === 0) return "0 B"
|
|
1118
|
+
const sizes = [ "B", "KB", "MB", "GB", "TB", "PB" ];
|
|
1119
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
1120
|
+
const value = bytes / Math.pow(1024, i);
|
|
1121
|
+
return `${ value.toFixed(2) } ${ sizes[i] }`
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function extractFileName(string) {
|
|
1125
|
+
return string.split("/").pop()
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// The content attribute is raw HTML (matching Trix/ActionText). Older Lexxy
|
|
1129
|
+
// versions JSON-encoded it, so try JSON.parse first for backward compatibility.
|
|
1130
|
+
function parseAttachmentContent(content) {
|
|
1131
|
+
try {
|
|
1132
|
+
return JSON.parse(content)
|
|
1133
|
+
} catch {
|
|
1134
|
+
return content
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
1139
|
+
static getType() {
|
|
1140
|
+
return "custom_action_text_attachment"
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
static clone(node) {
|
|
1144
|
+
return new CustomActionTextAttachmentNode({ ...node }, node.__key)
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
static importJSON(serializedNode) {
|
|
1148
|
+
return new CustomActionTextAttachmentNode({ ...serializedNode })
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
static importDOM() {
|
|
1152
|
+
return {
|
|
1153
|
+
[this.TAG_NAME]: (element) => {
|
|
1154
|
+
if (!element.getAttribute("content")) {
|
|
1155
|
+
return null
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
return {
|
|
1159
|
+
conversion: (attachment) => {
|
|
1160
|
+
// Preserve initial space if present since Lexical removes it
|
|
1161
|
+
const nodes = [];
|
|
1162
|
+
const previousSibling = attachment.previousSibling;
|
|
1163
|
+
if (previousSibling && previousSibling.nodeType === Node.TEXT_NODE && /\s$/.test(previousSibling.textContent)) {
|
|
1164
|
+
nodes.push($createTextNode(" "));
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const innerHtml = parseAttachmentContent(attachment.getAttribute("content"));
|
|
1168
|
+
|
|
1169
|
+
nodes.push(new CustomActionTextAttachmentNode({
|
|
1170
|
+
sgid: attachment.getAttribute("sgid"),
|
|
1171
|
+
innerHtml,
|
|
1172
|
+
plainText: attachment.textContent.trim() || extractPlainTextFromHtml(innerHtml),
|
|
1173
|
+
contentType: attachment.getAttribute("content-type")
|
|
1174
|
+
}));
|
|
1175
|
+
|
|
1176
|
+
const nextSibling = attachment.nextSibling;
|
|
1177
|
+
if (nextSibling && nextSibling.nodeType === Node.TEXT_NODE && /^\s/.test(nextSibling.textContent)) {
|
|
1178
|
+
nodes.push($createTextNode(" "));
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
return { node: nodes }
|
|
1182
|
+
},
|
|
1183
|
+
priority: 2
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
static get TAG_NAME() {
|
|
1190
|
+
return Lexxy.global.get("attachmentTagName")
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
constructor({ tagName, sgid, contentType, innerHtml, plainText }, key) {
|
|
1194
|
+
super(key);
|
|
1195
|
+
|
|
1196
|
+
const contentTypeNamespace = Lexxy.global.get("attachmentContentTypeNamespace");
|
|
1197
|
+
|
|
1198
|
+
this.tagName = tagName || CustomActionTextAttachmentNode.TAG_NAME;
|
|
1199
|
+
this.sgid = sgid;
|
|
1200
|
+
this.contentType = contentType || `application/vnd.${contentTypeNamespace}.unknown`;
|
|
1201
|
+
this.innerHtml = innerHtml;
|
|
1202
|
+
this.plainText = plainText ?? extractPlainTextFromHtml(innerHtml);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
createDOM() {
|
|
1206
|
+
const figure = createElement(this.tagName, { "content-type": this.contentType, "data-lexxy-decorator": true });
|
|
1207
|
+
|
|
1208
|
+
figure.insertAdjacentHTML("beforeend", this.innerHtml);
|
|
1209
|
+
|
|
1210
|
+
const deleteButton = createElement("lexxy-node-delete-button");
|
|
1211
|
+
figure.appendChild(deleteButton);
|
|
1212
|
+
|
|
1213
|
+
return figure
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
updateDOM() {
|
|
1217
|
+
return false
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
getTextContent() {
|
|
1221
|
+
return "\ufeff"
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
getReadableTextContent() {
|
|
1225
|
+
return this.plainText || `[${this.contentType}]`
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
isInline() {
|
|
1229
|
+
return true
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
exportDOM() {
|
|
1233
|
+
const attachment = createElement(this.tagName, {
|
|
1234
|
+
sgid: this.sgid,
|
|
1235
|
+
content: this.innerHtml,
|
|
1236
|
+
"content-type": this.contentType
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
return { element: attachment }
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
exportJSON() {
|
|
1243
|
+
return {
|
|
1244
|
+
type: "custom_action_text_attachment",
|
|
1245
|
+
version: 1,
|
|
1246
|
+
tagName: this.tagName,
|
|
1247
|
+
sgid: this.sgid,
|
|
1248
|
+
contentType: this.contentType,
|
|
1249
|
+
innerHtml: this.innerHtml,
|
|
1250
|
+
plainText: this.plainText
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
decorate() {
|
|
1255
|
+
return null
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1035
1259
|
const SILENT_UPDATE_TAGS = [ HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG ];
|
|
1036
1260
|
|
|
1037
1261
|
function $createNodeSelectionWith(...nodes) {
|
|
@@ -1099,6 +1323,71 @@ function extendConversion(nodeKlass, conversionName, callback = (output => outpu
|
|
|
1099
1323
|
}
|
|
1100
1324
|
}
|
|
1101
1325
|
|
|
1326
|
+
function $isCursorOnLastLine(selection) {
|
|
1327
|
+
const anchorNode = selection.anchor.getNode();
|
|
1328
|
+
const elementNode = $isElementNode(anchorNode) ? anchorNode : anchorNode.getParentOrThrow();
|
|
1329
|
+
const children = elementNode.getChildren();
|
|
1330
|
+
if (children.length === 0) return true
|
|
1331
|
+
|
|
1332
|
+
const lastChild = children[children.length - 1];
|
|
1333
|
+
|
|
1334
|
+
if (anchorNode === elementNode.getLatest() && selection.anchor.offset === children.length) return true
|
|
1335
|
+
if (anchorNode === lastChild) return true
|
|
1336
|
+
|
|
1337
|
+
const lastLineBreakIndex = children.findLastIndex(child => $isLineBreakNode(child));
|
|
1338
|
+
if (lastLineBreakIndex === -1) return true
|
|
1339
|
+
|
|
1340
|
+
const anchorIndex = children.indexOf(anchorNode);
|
|
1341
|
+
return anchorIndex > lastLineBreakIndex
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function $isBlankNode(node) {
|
|
1345
|
+
if (node.getTextContent().trim() !== "") return false
|
|
1346
|
+
|
|
1347
|
+
const children = node.getChildren?.();
|
|
1348
|
+
if (!children || children.length === 0) return true
|
|
1349
|
+
|
|
1350
|
+
return children.every(child => {
|
|
1351
|
+
if ($isLineBreakNode(child)) return true
|
|
1352
|
+
return $isBlankNode(child)
|
|
1353
|
+
})
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function $trimTrailingBlankNodes(parent) {
|
|
1357
|
+
for (const child of $lastToFirstIterator(parent)) {
|
|
1358
|
+
if ($isBlankNode(child)) {
|
|
1359
|
+
child.remove();
|
|
1360
|
+
} else {
|
|
1361
|
+
break
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// A list item is structurally empty if it contains no meaningful content.
|
|
1367
|
+
// Unlike getTextContent().trim() === "", this walks descendants to ensure
|
|
1368
|
+
// decorator nodes (mentions, attachments whose getTextContent() may return
|
|
1369
|
+
// invisible characters like \ufeff) are treated as non-empty content.
|
|
1370
|
+
function $isListItemStructurallyEmpty(listItem) {
|
|
1371
|
+
const children = listItem.getChildren();
|
|
1372
|
+
for (const child of children) {
|
|
1373
|
+
if ($isDecoratorNode(child)) return false
|
|
1374
|
+
if ($isLineBreakNode(child)) continue
|
|
1375
|
+
if ($isTextNode(child)) {
|
|
1376
|
+
if (child.getTextContent().trim() !== "") return false
|
|
1377
|
+
} else if ($isElementNode(child)) {
|
|
1378
|
+
if (child.getTextContent().trim() !== "") return false
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return true
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function isAttachmentSpacerTextNode(node, previousNode, index, childCount) {
|
|
1385
|
+
return $isTextNode(node)
|
|
1386
|
+
&& node.getTextContent() === " "
|
|
1387
|
+
&& index === childCount - 1
|
|
1388
|
+
&& previousNode instanceof CustomActionTextAttachmentNode
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1102
1391
|
function isSelectionHighlighted(selection) {
|
|
1103
1392
|
if (!$isRangeSelection(selection)) return false
|
|
1104
1393
|
|
|
@@ -1209,6 +1498,12 @@ const hasPastedStylesState = createState("hasPastedStyles", {
|
|
|
1209
1498
|
parse: (value) => value || false
|
|
1210
1499
|
});
|
|
1211
1500
|
|
|
1501
|
+
// Stores pending highlight ranges extracted during HTML import, keyed by CodeNode key.
|
|
1502
|
+
// After the code retokenizer creates fresh CodeHighlightNodes, a mutation listener
|
|
1503
|
+
// reads this map and re-applies the highlight styles. Scoped per editor instance
|
|
1504
|
+
// so entries don't leak across editors or outlive a torn-down editor.
|
|
1505
|
+
const pendingCodeHighlights = new WeakMap();
|
|
1506
|
+
|
|
1212
1507
|
class HighlightExtension extends LexxyExtension {
|
|
1213
1508
|
get enabled() {
|
|
1214
1509
|
return this.editorElement.supportsRichText
|
|
@@ -1231,12 +1526,20 @@ class HighlightExtension extends LexxyExtension {
|
|
|
1231
1526
|
// keep the ref to the canonicalizers for optimized css conversion
|
|
1232
1527
|
const canonicalizers = buildCanonicalizers(config);
|
|
1233
1528
|
|
|
1529
|
+
// Register the <pre> converter directly in the conversion cache so it
|
|
1530
|
+
// coexists with other extensions' "pre" converters (the extension-level
|
|
1531
|
+
// html.import uses Object.assign, which means only one "pre" per key).
|
|
1532
|
+
$registerPreConversion(editor);
|
|
1533
|
+
|
|
1234
1534
|
return mergeRegister(
|
|
1235
1535
|
editor.registerCommand(TOGGLE_HIGHLIGHT_COMMAND, (styles) => $toggleSelectionStyles(editor, styles), COMMAND_PRIORITY_NORMAL),
|
|
1236
1536
|
editor.registerCommand(REMOVE_HIGHLIGHT_COMMAND, () => $toggleSelectionStyles(editor, BLANK_STYLES), COMMAND_PRIORITY_NORMAL),
|
|
1237
1537
|
editor.registerNodeTransform(TextNode, $syncHighlightWithStyle),
|
|
1238
1538
|
editor.registerNodeTransform(CodeHighlightNode, $syncHighlightWithCodeHighlightNode),
|
|
1239
|
-
editor.registerNodeTransform(TextNode, (textNode) => $canonicalizePastedStyles(textNode, canonicalizers))
|
|
1539
|
+
editor.registerNodeTransform(TextNode, (textNode) => $canonicalizePastedStyles(textNode, canonicalizers)),
|
|
1540
|
+
editor.registerMutationListener(CodeNode, (mutations) => {
|
|
1541
|
+
$applyPendingCodeHighlights(editor, mutations);
|
|
1542
|
+
}, { skipInitialization: true })
|
|
1240
1543
|
)
|
|
1241
1544
|
}
|
|
1242
1545
|
});
|
|
@@ -1266,48 +1569,247 @@ function $markConversion() {
|
|
|
1266
1569
|
}
|
|
1267
1570
|
}
|
|
1268
1571
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1572
|
+
// Register a custom <pre> converter directly in the editor's HTML conversion
|
|
1573
|
+
// cache. We can't use the extension-level html.import because Object.assign
|
|
1574
|
+
// merges all extensions' converters by tag, and a later extension (e.g.
|
|
1575
|
+
// TrixContentExtension) would overwrite ours.
|
|
1576
|
+
function $registerPreConversion(editor) {
|
|
1577
|
+
if (!editor._htmlConversions) return
|
|
1578
|
+
|
|
1579
|
+
let preEntries = editor._htmlConversions.get("pre");
|
|
1580
|
+
if (!preEntries) {
|
|
1581
|
+
preEntries = [];
|
|
1582
|
+
editor._htmlConversions.set("pre", preEntries);
|
|
1583
|
+
}
|
|
1584
|
+
preEntries.push($preConversionWithHighlightsFactory(editor));
|
|
1274
1585
|
}
|
|
1275
1586
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1587
|
+
// Returns a <pre> converter factory scoped to a specific editor instance.
|
|
1588
|
+
// The factory extracts highlight ranges from <mark> elements before the code
|
|
1589
|
+
// retokenizer can destroy them. The ranges are stored in pendingCodeHighlights
|
|
1590
|
+
// and applied after retokenization via a mutation listener.
|
|
1591
|
+
function $preConversionWithHighlightsFactory(editor) {
|
|
1592
|
+
return function $preConversionWithHighlights(domNode) {
|
|
1593
|
+
const highlights = extractHighlightRanges(domNode);
|
|
1594
|
+
if (highlights.length === 0) return null
|
|
1279
1595
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1596
|
+
return {
|
|
1597
|
+
conversion: (domNode) => {
|
|
1598
|
+
const language = domNode.getAttribute("data-language");
|
|
1599
|
+
const codeNode = $createCodeNode(language);
|
|
1600
|
+
$getPendingHighlights(editor).set(codeNode.getKey(), highlights);
|
|
1601
|
+
return { node: codeNode }
|
|
1602
|
+
},
|
|
1603
|
+
priority: 2
|
|
1604
|
+
}
|
|
1284
1605
|
}
|
|
1606
|
+
}
|
|
1285
1607
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1608
|
+
// Walk the DOM tree inside a <pre> element and build a list of
|
|
1609
|
+
// { start, end, style } ranges for every <mark> element found.
|
|
1610
|
+
function extractHighlightRanges(preElement) {
|
|
1611
|
+
const ranges = [];
|
|
1612
|
+
const codeElement = preElement.querySelector("code") || preElement;
|
|
1613
|
+
|
|
1614
|
+
let offset = 0;
|
|
1615
|
+
|
|
1616
|
+
function walk(node) {
|
|
1617
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1618
|
+
offset += node.textContent.length;
|
|
1619
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1620
|
+
// <br> maps to a LineBreakNode (1 character) in Lexical
|
|
1621
|
+
if (node.tagName === "BR") {
|
|
1622
|
+
offset += 1;
|
|
1623
|
+
return
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
const isMark = node.tagName === "MARK";
|
|
1627
|
+
const start = offset;
|
|
1628
|
+
|
|
1629
|
+
for (const child of node.childNodes) {
|
|
1630
|
+
walk(child);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if (isMark) {
|
|
1634
|
+
const style = extractHighlightStyleFromElement(node);
|
|
1635
|
+
if (style) {
|
|
1636
|
+
ranges.push({ start, end: offset, style });
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1290
1640
|
}
|
|
1641
|
+
|
|
1642
|
+
for (const child of codeElement.childNodes) {
|
|
1643
|
+
walk(child);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
return ranges
|
|
1291
1647
|
}
|
|
1292
1648
|
|
|
1293
|
-
function $
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
}
|
|
1649
|
+
function $getPendingHighlights(editor) {
|
|
1650
|
+
let map = pendingCodeHighlights.get(editor);
|
|
1651
|
+
if (!map) {
|
|
1652
|
+
map = new Map();
|
|
1653
|
+
pendingCodeHighlights.set(editor, map);
|
|
1654
|
+
}
|
|
1655
|
+
return map
|
|
1299
1656
|
}
|
|
1300
1657
|
|
|
1301
|
-
function
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1658
|
+
function extractHighlightStyleFromElement(element) {
|
|
1659
|
+
const styles = {};
|
|
1660
|
+
if (element.style?.color) styles.color = element.style.color;
|
|
1661
|
+
if (element.style?.backgroundColor) styles["background-color"] = element.style.backgroundColor;
|
|
1662
|
+
const css = getCSSFromStyleObject(styles);
|
|
1663
|
+
return css.length > 0 ? css : null
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// Called from the CodeNode mutation listener after the retokenizer has
|
|
1667
|
+
// replaced TextNodes with fresh CodeHighlightNodes.
|
|
1668
|
+
function $applyPendingCodeHighlights(editor, mutations) {
|
|
1669
|
+
const pending = $getPendingHighlights(editor);
|
|
1670
|
+
const keysToProcess = [];
|
|
1671
|
+
|
|
1672
|
+
for (const [ key, type ] of mutations) {
|
|
1673
|
+
if (type !== "destroyed" && pending.has(key)) {
|
|
1674
|
+
keysToProcess.push(key);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
if (keysToProcess.length === 0) return
|
|
1679
|
+
|
|
1680
|
+
// Use a deferred update so the retokenizer has finished its
|
|
1681
|
+
// skipTransforms update before we touch the nodes.
|
|
1682
|
+
editor.update(() => {
|
|
1683
|
+
for (const key of keysToProcess) {
|
|
1684
|
+
const highlights = pending.get(key);
|
|
1685
|
+
pending.delete(key);
|
|
1686
|
+
if (!highlights) continue
|
|
1687
|
+
|
|
1688
|
+
const codeNode = $getNodeByKey(key);
|
|
1689
|
+
if (!codeNode || !$isCodeNode(codeNode)) continue
|
|
1690
|
+
|
|
1691
|
+
$applyHighlightRangesToCodeNode(codeNode, highlights);
|
|
1692
|
+
}
|
|
1693
|
+
}, { skipTransforms: true, discrete: true });
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// Apply saved highlight ranges to the CodeHighlightNode children
|
|
1697
|
+
// of a CodeNode, splitting nodes at range boundaries as needed.
|
|
1698
|
+
// We can't use TextNode.splitText() because it creates TextNode
|
|
1699
|
+
// instances (not CodeHighlightNodes) for the split parts. Instead,
|
|
1700
|
+
// we manually create CodeHighlightNode replacements.
|
|
1701
|
+
function $applyHighlightRangesToCodeNode(codeNode, highlights) {
|
|
1702
|
+
if (highlights.length === 0) return
|
|
1703
|
+
|
|
1704
|
+
for (const { start: hlStart, end: hlEnd, style } of highlights) {
|
|
1705
|
+
// Rebuild the child-to-offset mapping for each highlight range because
|
|
1706
|
+
// earlier ranges may have split nodes, invalidating previous mappings.
|
|
1707
|
+
const childRanges = $buildChildRanges(codeNode);
|
|
1708
|
+
|
|
1709
|
+
for (const { node, start: nodeStart, end: nodeEnd } of childRanges) {
|
|
1710
|
+
// Check if this child overlaps with the highlight range
|
|
1711
|
+
const overlapStart = Math.max(hlStart, nodeStart);
|
|
1712
|
+
const overlapEnd = Math.min(hlEnd, nodeEnd);
|
|
1713
|
+
|
|
1714
|
+
if (overlapStart >= overlapEnd) continue
|
|
1715
|
+
|
|
1716
|
+
// Calculate offsets relative to this node
|
|
1717
|
+
const relStart = overlapStart - nodeStart;
|
|
1718
|
+
const relEnd = overlapEnd - nodeStart;
|
|
1719
|
+
const nodeLength = nodeEnd - nodeStart;
|
|
1720
|
+
|
|
1721
|
+
if (relStart === 0 && relEnd === nodeLength) {
|
|
1722
|
+
// Entire node is highlighted - apply style directly
|
|
1723
|
+
node.setStyle(style);
|
|
1724
|
+
$setCodeHighlightFormat(node, true);
|
|
1725
|
+
} else {
|
|
1726
|
+
// Need to split: replace the node with 2 or 3 CodeHighlightNodes
|
|
1727
|
+
const text = node.getTextContent();
|
|
1728
|
+
const highlightType = node.getHighlightType();
|
|
1729
|
+
const replacements = [];
|
|
1730
|
+
|
|
1731
|
+
if (relStart > 0) {
|
|
1732
|
+
replacements.push($createCodeHighlightNode(text.slice(0, relStart), highlightType));
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
const styledNode = $createCodeHighlightNode(text.slice(relStart, relEnd), highlightType);
|
|
1736
|
+
styledNode.setStyle(style);
|
|
1737
|
+
$setCodeHighlightFormat(styledNode, true);
|
|
1738
|
+
replacements.push(styledNode);
|
|
1739
|
+
|
|
1740
|
+
if (relEnd < nodeLength) {
|
|
1741
|
+
replacements.push($createCodeHighlightNode(text.slice(relEnd), highlightType));
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
for (const replacement of replacements) {
|
|
1745
|
+
node.insertBefore(replacement);
|
|
1746
|
+
}
|
|
1747
|
+
node.remove();
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function $buildChildRanges(codeNode) {
|
|
1754
|
+
const childRanges = [];
|
|
1755
|
+
let charOffset = 0;
|
|
1756
|
+
|
|
1757
|
+
for (const child of codeNode.getChildren()) {
|
|
1758
|
+
if ($isCodeHighlightNode(child)) {
|
|
1759
|
+
const text = child.getTextContent();
|
|
1760
|
+
childRanges.push({ node: child, start: charOffset, end: charOffset + text.length });
|
|
1761
|
+
charOffset += text.length;
|
|
1762
|
+
} else {
|
|
1763
|
+
// LineBreakNode, TabNode - count as 1 character each (\n, \t)
|
|
1764
|
+
charOffset += 1;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
return childRanges
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function buildCanonicalizers(config) {
|
|
1772
|
+
return [
|
|
1773
|
+
new StyleCanonicalizer("color", [ ...config.buttons.color, ...config.permit.color ]),
|
|
1774
|
+
new StyleCanonicalizer("background-color", [ ...config.buttons["background-color"], ...config.permit["background-color"] ])
|
|
1775
|
+
]
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
function $toggleSelectionStyles(editor, styles) {
|
|
1779
|
+
const selection = $getSelection();
|
|
1780
|
+
if (!$isRangeSelection(selection)) return
|
|
1781
|
+
|
|
1782
|
+
const patch = {};
|
|
1783
|
+
for (const property in styles) {
|
|
1784
|
+
const oldValue = $getSelectionStyleValueForProperty(selection, property);
|
|
1785
|
+
patch[property] = toggleOrReplace(oldValue, styles[property]);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
if ($selectionIsInCodeBlock(selection)) {
|
|
1789
|
+
$patchCodeHighlightStyles(editor, selection, patch);
|
|
1790
|
+
} else {
|
|
1791
|
+
$patchStyleText(selection, patch);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function $selectionIsInCodeBlock(selection) {
|
|
1796
|
+
const nodes = selection.getNodes();
|
|
1797
|
+
return nodes.some((node) => {
|
|
1798
|
+
const parent = $isCodeHighlightNode(node) ? node.getParent() : node;
|
|
1799
|
+
return $isCodeNode(parent)
|
|
1800
|
+
})
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function $patchCodeHighlightStyles(editor, selection, patch) {
|
|
1804
|
+
// Capture selection state and node keys before the nested update
|
|
1805
|
+
const nodeKeys = selection.getNodes()
|
|
1806
|
+
.filter((node) => $isCodeHighlightNode(node))
|
|
1807
|
+
.map((node) => ({
|
|
1808
|
+
key: node.getKey(),
|
|
1809
|
+
startOffset: $getNodeSelectionOffsets(node, selection)[0],
|
|
1810
|
+
endOffset: $getNodeSelectionOffsets(node, selection)[1],
|
|
1811
|
+
textSize: node.getTextContentSize()
|
|
1812
|
+
}));
|
|
1311
1813
|
|
|
1312
1814
|
// Use skipTransforms to prevent the code highlighting system from
|
|
1313
1815
|
// re-tokenizing and wiping out the style changes we apply.
|
|
@@ -1445,11 +1947,15 @@ const COMMANDS = [
|
|
|
1445
1947
|
"bold",
|
|
1446
1948
|
"italic",
|
|
1447
1949
|
"strikethrough",
|
|
1950
|
+
"underline",
|
|
1448
1951
|
"link",
|
|
1449
1952
|
"unlink",
|
|
1450
1953
|
"toggleHighlight",
|
|
1451
1954
|
"removeHighlight",
|
|
1452
|
-
"
|
|
1955
|
+
"setFormatHeadingLarge",
|
|
1956
|
+
"setFormatHeadingMedium",
|
|
1957
|
+
"setFormatHeadingSmall",
|
|
1958
|
+
"setFormatParagraph",
|
|
1453
1959
|
"insertUnorderedList",
|
|
1454
1960
|
"insertOrderedList",
|
|
1455
1961
|
"insertQuoteBlock",
|
|
@@ -1498,6 +2004,10 @@ class CommandDispatcher {
|
|
|
1498
2004
|
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough");
|
|
1499
2005
|
}
|
|
1500
2006
|
|
|
2007
|
+
dispatchUnderline() {
|
|
2008
|
+
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline");
|
|
2009
|
+
}
|
|
2010
|
+
|
|
1501
2011
|
dispatchToggleHighlight(styles) {
|
|
1502
2012
|
this.editor.dispatchCommand(TOGGLE_HIGHLIGHT_COMMAND, styles);
|
|
1503
2013
|
}
|
|
@@ -1533,7 +2043,7 @@ class CommandDispatcher {
|
|
|
1533
2043
|
const anchorNode = selection.anchor.getNode();
|
|
1534
2044
|
|
|
1535
2045
|
if (this.selection.isInsideList && anchorNode && getListType(anchorNode) === "bullet") {
|
|
1536
|
-
this.contents.
|
|
2046
|
+
this.contents.applyParagraphFormat();
|
|
1537
2047
|
} else {
|
|
1538
2048
|
this.editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
1539
2049
|
}
|
|
@@ -1546,26 +2056,72 @@ class CommandDispatcher {
|
|
|
1546
2056
|
const anchorNode = selection.anchor.getNode();
|
|
1547
2057
|
|
|
1548
2058
|
if (this.selection.isInsideList && anchorNode && getListType(anchorNode) === "number") {
|
|
1549
|
-
this.contents.
|
|
2059
|
+
this.contents.applyParagraphFormat();
|
|
1550
2060
|
} else {
|
|
1551
2061
|
this.editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
1552
2062
|
}
|
|
1553
2063
|
}
|
|
1554
2064
|
|
|
1555
2065
|
dispatchInsertQuoteBlock() {
|
|
1556
|
-
|
|
1557
|
-
this.contents.toggleNodeWrappingAllSelectedNodes((node) => $isQuoteNode(node), () => $createQuoteNode());
|
|
1558
|
-
}
|
|
2066
|
+
this.contents.toggleBlockquote();
|
|
1559
2067
|
}
|
|
1560
2068
|
|
|
1561
2069
|
dispatchInsertCodeBlock() {
|
|
1562
|
-
this.
|
|
1563
|
-
|
|
1564
|
-
|
|
2070
|
+
if (this.selection.hasSelectedWordsInSingleLine) {
|
|
2071
|
+
this.#toggleInlineCode();
|
|
2072
|
+
} else {
|
|
2073
|
+
this.contents.toggleCodeBlock();
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
#toggleInlineCode() {
|
|
2078
|
+
const selection = $getSelection();
|
|
2079
|
+
if (!$isRangeSelection(selection)) return
|
|
2080
|
+
|
|
2081
|
+
if (!selection.isCollapsed()) {
|
|
2082
|
+
const textNodes = selection.getNodes().filter($isTextNode);
|
|
2083
|
+
const applyingCode = !textNodes.every((node) => node.hasFormat("code"));
|
|
2084
|
+
|
|
2085
|
+
if (applyingCode) {
|
|
2086
|
+
this.#stripInlineFormattingFromSelection(selection, textNodes);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code");
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// Strip all inline formatting (bold, italic, etc.) from the selected text
|
|
2094
|
+
// nodes so that applying code produces a single merged <code> element instead
|
|
2095
|
+
// of one per differently-formatted span.
|
|
2096
|
+
#stripInlineFormattingFromSelection(selection, textNodes) {
|
|
2097
|
+
const isBackward = selection.isBackward();
|
|
2098
|
+
const startPoint = isBackward ? selection.focus : selection.anchor;
|
|
2099
|
+
const endPoint = isBackward ? selection.anchor : selection.focus;
|
|
2100
|
+
|
|
2101
|
+
for (let i = 0; i < textNodes.length; i++) {
|
|
2102
|
+
const node = textNodes[i];
|
|
2103
|
+
if (node.getFormat() === 0) continue
|
|
2104
|
+
|
|
2105
|
+
const isFirst = i === 0;
|
|
2106
|
+
const isLast = i === textNodes.length - 1;
|
|
2107
|
+
const startOffset = isFirst && startPoint.type === "text" ? startPoint.offset : 0;
|
|
2108
|
+
const endOffset = isLast && endPoint.type === "text" ? endPoint.offset : node.getTextContentSize();
|
|
2109
|
+
|
|
2110
|
+
if (startOffset === 0 && endOffset === node.getTextContentSize()) {
|
|
2111
|
+
node.setFormat(0);
|
|
1565
2112
|
} else {
|
|
1566
|
-
|
|
2113
|
+
const splits = node.splitText(startOffset, endOffset);
|
|
2114
|
+
const target = startOffset === 0 ? splits[0] : splits[1];
|
|
2115
|
+
target.setFormat(0);
|
|
2116
|
+
|
|
2117
|
+
if (isFirst && startPoint.type === "text") {
|
|
2118
|
+
startPoint.set(target.getKey(), 0, "text");
|
|
2119
|
+
}
|
|
2120
|
+
if (isLast && endPoint.type === "text") {
|
|
2121
|
+
endPoint.set(target.getKey(), endOffset - startOffset, "text");
|
|
2122
|
+
}
|
|
1567
2123
|
}
|
|
1568
|
-
}
|
|
2124
|
+
}
|
|
1569
2125
|
}
|
|
1570
2126
|
|
|
1571
2127
|
dispatchInsertHorizontalDivider() {
|
|
@@ -1573,35 +2129,20 @@ class CommandDispatcher {
|
|
|
1573
2129
|
this.editor.focus();
|
|
1574
2130
|
}
|
|
1575
2131
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
2132
|
+
dispatchSetFormatHeadingLarge() {
|
|
2133
|
+
this.contents.applyHeadingFormat("h2");
|
|
2134
|
+
}
|
|
1579
2135
|
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
}
|
|
2136
|
+
dispatchSetFormatHeadingMedium() {
|
|
2137
|
+
this.contents.applyHeadingFormat("h3");
|
|
2138
|
+
}
|
|
1584
2139
|
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
const currentTag = topLevelElement.getTag();
|
|
1589
|
-
if (currentTag === "h2") {
|
|
1590
|
-
nextTag = "h3";
|
|
1591
|
-
} else if (currentTag === "h3") {
|
|
1592
|
-
nextTag = "h4";
|
|
1593
|
-
} else if (currentTag === "h4") {
|
|
1594
|
-
nextTag = null;
|
|
1595
|
-
} else {
|
|
1596
|
-
nextTag = "h2";
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
2140
|
+
dispatchSetFormatHeadingSmall() {
|
|
2141
|
+
this.contents.applyHeadingFormat("h4");
|
|
2142
|
+
}
|
|
1599
2143
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
} else {
|
|
1603
|
-
this.contents.removeFormattingFromSelectedLines();
|
|
1604
|
-
}
|
|
2144
|
+
dispatchSetFormatParagraph() {
|
|
2145
|
+
this.contents.applyParagraphFormat();
|
|
1605
2146
|
}
|
|
1606
2147
|
|
|
1607
2148
|
dispatchUploadAttachments() {
|
|
@@ -1675,6 +2216,8 @@ class CommandDispatcher {
|
|
|
1675
2216
|
}
|
|
1676
2217
|
|
|
1677
2218
|
#handleDragEnter(event) {
|
|
2219
|
+
if (this.#isInternalDrag(event)) return
|
|
2220
|
+
|
|
1678
2221
|
this.dragCounter++;
|
|
1679
2222
|
if (this.dragCounter === 1) {
|
|
1680
2223
|
this.#saveSelectionBeforeDrag();
|
|
@@ -1683,6 +2226,8 @@ class CommandDispatcher {
|
|
|
1683
2226
|
}
|
|
1684
2227
|
|
|
1685
2228
|
#handleDragLeave(event) {
|
|
2229
|
+
if (this.#isInternalDrag(event)) return
|
|
2230
|
+
|
|
1686
2231
|
this.dragCounter--;
|
|
1687
2232
|
if (this.dragCounter === 0) {
|
|
1688
2233
|
this.#selectionBeforeDrag = null;
|
|
@@ -1691,10 +2236,14 @@ class CommandDispatcher {
|
|
|
1691
2236
|
}
|
|
1692
2237
|
|
|
1693
2238
|
#handleDragOver(event) {
|
|
2239
|
+
if (this.#isInternalDrag(event)) return
|
|
2240
|
+
|
|
1694
2241
|
event.preventDefault();
|
|
1695
2242
|
}
|
|
1696
2243
|
|
|
1697
2244
|
#handleDrop(event) {
|
|
2245
|
+
if (this.#isInternalDrag(event)) return
|
|
2246
|
+
|
|
1698
2247
|
event.preventDefault();
|
|
1699
2248
|
|
|
1700
2249
|
this.dragCounter = 0;
|
|
@@ -1728,6 +2277,10 @@ class CommandDispatcher {
|
|
|
1728
2277
|
this.#selectionBeforeDrag = null;
|
|
1729
2278
|
}
|
|
1730
2279
|
|
|
2280
|
+
#isInternalDrag(event) {
|
|
2281
|
+
return event.dataTransfer?.types.includes("application/x-lexxy-node-key")
|
|
2282
|
+
}
|
|
2283
|
+
|
|
1731
2284
|
#handleTabKey(event) {
|
|
1732
2285
|
if (this.selection.isInsideList) {
|
|
1733
2286
|
return this.#handleTabForList(event)
|
|
@@ -1789,28 +2342,40 @@ function nextFrame() {
|
|
|
1789
2342
|
return new Promise(requestAnimationFrame)
|
|
1790
2343
|
}
|
|
1791
2344
|
|
|
1792
|
-
function
|
|
1793
|
-
|
|
1794
|
-
const sizes = [ "B", "KB", "MB", "GB", "TB", "PB" ];
|
|
1795
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
1796
|
-
const value = bytes / Math.pow(1024, i);
|
|
1797
|
-
return `${ value.toFixed(2) } ${ sizes[i] }`
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
function extractFileName(string) {
|
|
1801
|
-
return string.split("/").pop()
|
|
2345
|
+
function dasherize(value) {
|
|
2346
|
+
return value.replace(/([A-Z])/g, (_, char) => `-${char.toLowerCase()}`)
|
|
1802
2347
|
}
|
|
1803
2348
|
|
|
1804
|
-
|
|
1805
|
-
// but Trix/ActionText stores it as raw HTML. Try JSON first, fall back to raw.
|
|
1806
|
-
function parseAttachmentContent(content) {
|
|
2349
|
+
function isUrl(string) {
|
|
1807
2350
|
try {
|
|
1808
|
-
|
|
2351
|
+
new URL(string);
|
|
2352
|
+
return true
|
|
1809
2353
|
} catch {
|
|
1810
|
-
return
|
|
2354
|
+
return false
|
|
1811
2355
|
}
|
|
1812
2356
|
}
|
|
1813
2357
|
|
|
2358
|
+
function normalizeFilteredText(string) {
|
|
2359
|
+
return string
|
|
2360
|
+
.toLowerCase()
|
|
2361
|
+
.normalize("NFD").replace(/[\u0300-\u036f]/g, "") // Remove diacritics
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
function filterMatches(text, potentialMatch) {
|
|
2365
|
+
return normalizeFilteredText(text).includes(normalizeFilteredText(potentialMatch))
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
function upcaseFirst(string) {
|
|
2369
|
+
return string.charAt(0).toUpperCase() + string.slice(1)
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
// Parses a value that may arrive as a boolean or as a string (e.g. from DOM
|
|
2373
|
+
// getAttribute) into a proper boolean. Ensures "false" doesn't evaluate as truthy.
|
|
2374
|
+
function parseBoolean(value) {
|
|
2375
|
+
if (typeof value === "string") return value === "true"
|
|
2376
|
+
return Boolean(value)
|
|
2377
|
+
}
|
|
2378
|
+
|
|
1814
2379
|
class ActionTextAttachmentNode extends DecoratorNode {
|
|
1815
2380
|
static getType() {
|
|
1816
2381
|
return "action_text_attachment"
|
|
@@ -1891,7 +2456,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
1891
2456
|
this.tagName = tagName || ActionTextAttachmentNode.TAG_NAME;
|
|
1892
2457
|
this.sgid = sgid;
|
|
1893
2458
|
this.src = src;
|
|
1894
|
-
this.previewable = previewable;
|
|
2459
|
+
this.previewable = parseBoolean(previewable);
|
|
1895
2460
|
this.altText = altText || "";
|
|
1896
2461
|
this.caption = caption || "";
|
|
1897
2462
|
this.contentType = contentType || "";
|
|
@@ -1976,6 +2541,8 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
1976
2541
|
|
|
1977
2542
|
createAttachmentFigure() {
|
|
1978
2543
|
const figure = createAttachmentFigure(this.contentType, this.isPreviewableAttachment, this.fileName);
|
|
2544
|
+
figure.draggable = true;
|
|
2545
|
+
figure.dataset.lexicalNodeKey = this.__key;
|
|
1979
2546
|
|
|
1980
2547
|
const deleteButton = createElement("lexxy-node-delete-button");
|
|
1981
2548
|
figure.appendChild(deleteButton);
|
|
@@ -1993,7 +2560,30 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
1993
2560
|
|
|
1994
2561
|
#createDOMForImage(options = {}) {
|
|
1995
2562
|
const img = createElement("img", { src: this.src, draggable: false, alt: this.altText, ...this.#imageDimensions, ...options });
|
|
1996
|
-
|
|
2563
|
+
|
|
2564
|
+
if (this.previewable && !this.isPreviewableImage) {
|
|
2565
|
+
img.onerror = () => this.#swapPreviewToFileDOM(img);
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
const container = createElement("div", { className: "attachment__container" });
|
|
2569
|
+
container.appendChild(img);
|
|
2570
|
+
return container
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
#swapPreviewToFileDOM(img) {
|
|
2574
|
+
const figure = img.closest("figure.attachment");
|
|
2575
|
+
if (!figure) return
|
|
2576
|
+
|
|
2577
|
+
figure.className = figure.className.replace("attachment--preview", "attachment--file");
|
|
2578
|
+
|
|
2579
|
+
const container = figure.querySelector(".attachment__container");
|
|
2580
|
+
if (container) container.remove();
|
|
2581
|
+
|
|
2582
|
+
const caption = figure.querySelector("figcaption");
|
|
2583
|
+
if (caption) caption.remove();
|
|
2584
|
+
|
|
2585
|
+
figure.appendChild(this.#createDOMForFile());
|
|
2586
|
+
figure.appendChild(this.#createDOMForNotImage());
|
|
1997
2587
|
}
|
|
1998
2588
|
|
|
1999
2589
|
get #imageDimensions() {
|
|
@@ -2093,6 +2683,7 @@ class Selection {
|
|
|
2093
2683
|
this.#listenForNodeSelections();
|
|
2094
2684
|
this.#processSelectionChangeCommands();
|
|
2095
2685
|
this.#containEditorFocus();
|
|
2686
|
+
this.#clearStaleInlineCodeFormat();
|
|
2096
2687
|
}
|
|
2097
2688
|
|
|
2098
2689
|
set current(selection) {
|
|
@@ -2192,16 +2783,19 @@ class Selection {
|
|
|
2192
2783
|
|
|
2193
2784
|
const topLevelElement = anchorNode.getTopLevelElementOrThrow();
|
|
2194
2785
|
const listType = getListType(anchorNode);
|
|
2786
|
+
const headingNode = this.#getNearestHeadingNode(anchorNode);
|
|
2195
2787
|
|
|
2196
2788
|
return {
|
|
2197
2789
|
isBold: selection.hasFormat("bold"),
|
|
2198
2790
|
isItalic: selection.hasFormat("italic"),
|
|
2199
2791
|
isStrikethrough: selection.hasFormat("strikethrough"),
|
|
2792
|
+
isUnderline: selection.hasFormat("underline"),
|
|
2200
2793
|
isHighlight: isSelectionHighlighted(selection),
|
|
2201
2794
|
isInLink: $getNearestNodeOfType(anchorNode, LinkNode) !== null,
|
|
2202
2795
|
isInQuote: $isQuoteNode(topLevelElement),
|
|
2203
|
-
isInHeading:
|
|
2204
|
-
isInCode: selection
|
|
2796
|
+
isInHeading: headingNode !== null,
|
|
2797
|
+
isInCode: this.#isInCode(selection, anchorNode),
|
|
2798
|
+
headingTag: headingNode?.getTag() ?? null,
|
|
2205
2799
|
isInList: listType !== null,
|
|
2206
2800
|
listType,
|
|
2207
2801
|
isInTable: $getTableCellNodeFromLexicalNode(anchorNode) !== null
|
|
@@ -2286,8 +2880,12 @@ class Selection {
|
|
|
2286
2880
|
if (!anchorNode) return null
|
|
2287
2881
|
|
|
2288
2882
|
if ($isTextNode(anchorNode)) {
|
|
2289
|
-
if (offset
|
|
2290
|
-
|
|
2883
|
+
if (offset === anchorNode.getTextContentSize()) return this.#getNextNodeFromTextEnd(anchorNode)
|
|
2884
|
+
if (this.#isCursorOnLastVisualLineOfBlock(anchorNode)) {
|
|
2885
|
+
const topLevelElement = anchorNode.getTopLevelElement();
|
|
2886
|
+
return topLevelElement ? topLevelElement.getNextSibling() : null
|
|
2887
|
+
}
|
|
2888
|
+
return null
|
|
2291
2889
|
}
|
|
2292
2890
|
|
|
2293
2891
|
if ($isElementNode(anchorNode)) {
|
|
@@ -2317,8 +2915,12 @@ class Selection {
|
|
|
2317
2915
|
if (!anchorNode) return null
|
|
2318
2916
|
|
|
2319
2917
|
if ($isTextNode(anchorNode)) {
|
|
2320
|
-
if (offset
|
|
2321
|
-
|
|
2918
|
+
if (offset === 0) return this.#getPreviousNodeFromTextStart(anchorNode)
|
|
2919
|
+
if (this.#isCursorOnFirstVisualLineOfBlock(anchorNode)) {
|
|
2920
|
+
const topLevelElement = anchorNode.getTopLevelElement();
|
|
2921
|
+
return topLevelElement ? topLevelElement.getPreviousSibling() : null
|
|
2922
|
+
}
|
|
2923
|
+
return null
|
|
2322
2924
|
}
|
|
2323
2925
|
|
|
2324
2926
|
if ($isElementNode(anchorNode)) {
|
|
@@ -2328,6 +2930,53 @@ class Selection {
|
|
|
2328
2930
|
return this.#findPreviousSiblingUp(anchorNode)
|
|
2329
2931
|
}
|
|
2330
2932
|
|
|
2933
|
+
// When all inline code text is deleted, Lexical's selection retains the stale
|
|
2934
|
+
// code format flag. Verify the flag is backed by actual code-formatted content:
|
|
2935
|
+
// a code block ancestor or a text node that carries the code format.
|
|
2936
|
+
#isInCode(selection, anchorNode) {
|
|
2937
|
+
if ($getNearestNodeOfType(anchorNode, CodeNode) !== null) return true
|
|
2938
|
+
if (!selection.hasFormat("code")) return false
|
|
2939
|
+
|
|
2940
|
+
return $isTextNode(anchorNode) && anchorNode.hasFormat("code")
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
// After deleting all inline code text, Lexical preserves the code format on
|
|
2944
|
+
// the selection even though no code-formatted content remains. This listener
|
|
2945
|
+
// detects that stale state and clears it so newly typed text won't be
|
|
2946
|
+
// code-formatted.
|
|
2947
|
+
#clearStaleInlineCodeFormat() {
|
|
2948
|
+
this.editor.registerUpdateListener(({ editorState, tags }) => {
|
|
2949
|
+
if (tags.has("history-merge") || tags.has("skip-dom-selection")) return
|
|
2950
|
+
|
|
2951
|
+
let isStale = false;
|
|
2952
|
+
|
|
2953
|
+
editorState.read(() => {
|
|
2954
|
+
const selection = $getSelection();
|
|
2955
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return
|
|
2956
|
+
if (!selection.hasFormat("code")) return
|
|
2957
|
+
|
|
2958
|
+
const anchorNode = selection.anchor.getNode();
|
|
2959
|
+
if (this.#isInCode(selection, anchorNode)) return
|
|
2960
|
+
|
|
2961
|
+
isStale = true;
|
|
2962
|
+
});
|
|
2963
|
+
|
|
2964
|
+
if (isStale) {
|
|
2965
|
+
setTimeout(() => {
|
|
2966
|
+
this.editor.update(() => {
|
|
2967
|
+
const selection = $getSelection();
|
|
2968
|
+
if (!$isRangeSelection(selection) || !selection.hasFormat("code")) return
|
|
2969
|
+
|
|
2970
|
+
const anchorNode = selection.anchor.getNode();
|
|
2971
|
+
if (this.#isInCode(selection, anchorNode)) return
|
|
2972
|
+
|
|
2973
|
+
selection.toggleFormat("code");
|
|
2974
|
+
});
|
|
2975
|
+
}, 0);
|
|
2976
|
+
}
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2331
2980
|
get #currentlySelectedKeys() {
|
|
2332
2981
|
if (this.currentlySelectedKeys) { return this.currentlySelectedKeys }
|
|
2333
2982
|
|
|
@@ -2511,6 +3160,24 @@ class Selection {
|
|
|
2511
3160
|
return anchorNode.getTopLevelElement()
|
|
2512
3161
|
}
|
|
2513
3162
|
|
|
3163
|
+
#getNearestHeadingNode(anchorNode) {
|
|
3164
|
+
const topLevelElement = anchorNode.getTopLevelElementOrThrow();
|
|
3165
|
+
|
|
3166
|
+
let headingNode = $isHeadingNode(topLevelElement) ? topLevelElement : null;
|
|
3167
|
+
if (!headingNode) {
|
|
3168
|
+
let current = anchorNode.getParent();
|
|
3169
|
+
while (current) {
|
|
3170
|
+
if ($isHeadingNode(current)) {
|
|
3171
|
+
headingNode = current;
|
|
3172
|
+
break
|
|
3173
|
+
}
|
|
3174
|
+
current = current.getParent();
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
return headingNode
|
|
3179
|
+
}
|
|
3180
|
+
|
|
2514
3181
|
#moveToOrCreateNextLine(topLevelElement) {
|
|
2515
3182
|
const nextSibling = topLevelElement.getNextSibling();
|
|
2516
3183
|
|
|
@@ -2539,10 +3206,12 @@ class Selection {
|
|
|
2539
3206
|
}
|
|
2540
3207
|
|
|
2541
3208
|
#selectDecoratorNodeBeforeDeletion(backwards) {
|
|
3209
|
+
if (backwards && this.#removeEmptyListItem()) return true
|
|
3210
|
+
|
|
2542
3211
|
const node = backwards ? this.nodeBeforeCursor : this.nodeAfterCursor;
|
|
2543
3212
|
if (!$isDecoratorNode(node)) return false
|
|
2544
3213
|
|
|
2545
|
-
if (this.#collapseListItemToParagraph()) return true
|
|
3214
|
+
if (this.#collapseListItemToParagraph(node)) return true
|
|
2546
3215
|
|
|
2547
3216
|
this.#removeEmptyElementAnchorNode();
|
|
2548
3217
|
|
|
@@ -2550,15 +3219,51 @@ class Selection {
|
|
|
2550
3219
|
return Boolean(selection)
|
|
2551
3220
|
}
|
|
2552
3221
|
|
|
3222
|
+
// When backspace is pressed on an empty list item that has siblings,
|
|
3223
|
+
// remove the empty item and place the cursor appropriately. Without this,
|
|
3224
|
+
// Lexical's default collapseAtStart converts the empty item into a paragraph
|
|
3225
|
+
// above the list, causing the cursor to jump away from the list content.
|
|
3226
|
+
//
|
|
3227
|
+
// This only applies when there IS a next sibling — if the empty item is the
|
|
3228
|
+
// last one in the list, Lexical's default (convert to paragraph) provides
|
|
3229
|
+
// the standard "exit list" behavior.
|
|
3230
|
+
#removeEmptyListItem() {
|
|
3231
|
+
const selection = $getSelection();
|
|
3232
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false
|
|
3233
|
+
|
|
3234
|
+
const anchorNode = selection.anchor.getNode();
|
|
3235
|
+
const listItem = $getNearestNodeOfType(anchorNode, ListItemNode);
|
|
3236
|
+
if (!listItem) return false
|
|
3237
|
+
|
|
3238
|
+
if (!$isListItemStructurallyEmpty(listItem)) return false
|
|
3239
|
+
|
|
3240
|
+
const nextSibling = listItem.getNextSibling();
|
|
3241
|
+
if (!nextSibling) return false
|
|
3242
|
+
|
|
3243
|
+
const previousSibling = listItem.getPreviousSibling();
|
|
3244
|
+
if (previousSibling) {
|
|
3245
|
+
previousSibling.selectEnd();
|
|
3246
|
+
} else {
|
|
3247
|
+
nextSibling.selectStart();
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
listItem.remove();
|
|
3251
|
+
return true
|
|
3252
|
+
}
|
|
3253
|
+
|
|
2553
3254
|
// When the cursor is inside a list item, collapse the list item into a
|
|
2554
3255
|
// paragraph instead of selecting the decorator. This lets the user
|
|
2555
3256
|
// delete a list that immediately follows an attachment without the
|
|
2556
|
-
// attachment becoming selected.
|
|
2557
|
-
|
|
3257
|
+
// attachment becoming selected. Only applies when the decorator is
|
|
3258
|
+
// outside the list item (e.g. a block attachment before the list),
|
|
3259
|
+
// not when it's an inline mention inside the list item.
|
|
3260
|
+
#collapseListItemToParagraph(decoratorNode) {
|
|
2558
3261
|
const anchorNode = $getSelection()?.anchor?.getNode();
|
|
2559
3262
|
const listItem = anchorNode && $getNearestNodeOfType(anchorNode, ListItemNode);
|
|
2560
3263
|
if (!listItem) return false
|
|
2561
3264
|
|
|
3265
|
+
if (listItem.isParentOf(decoratorNode)) return false
|
|
3266
|
+
|
|
2562
3267
|
const listNode = $getNearestNodeOfType(listItem, ListNode);
|
|
2563
3268
|
if (!listNode) return false
|
|
2564
3269
|
|
|
@@ -2743,53 +3448,83 @@ class Selection {
|
|
|
2743
3448
|
}
|
|
2744
3449
|
return current ? current.getPreviousSibling() : null
|
|
2745
3450
|
}
|
|
2746
|
-
}
|
|
2747
|
-
|
|
2748
|
-
function sanitize(html) {
|
|
2749
|
-
return DOMPurify.sanitize(html, buildConfig())
|
|
2750
|
-
}
|
|
2751
3451
|
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
}
|
|
3452
|
+
#isCursorOnFirstVisualLineOfBlock(anchorNode) {
|
|
3453
|
+
return this.#isCursorOnEdgeLineOfBlock(anchorNode, "first")
|
|
3454
|
+
}
|
|
2755
3455
|
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
new URL(string);
|
|
2759
|
-
return true
|
|
2760
|
-
} catch {
|
|
2761
|
-
return false
|
|
3456
|
+
#isCursorOnLastVisualLineOfBlock(anchorNode) {
|
|
3457
|
+
return this.#isCursorOnEdgeLineOfBlock(anchorNode, "last")
|
|
2762
3458
|
}
|
|
2763
|
-
}
|
|
2764
3459
|
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
3460
|
+
// Check whether the cursor sits on the first or last visual line of its
|
|
3461
|
+
// top-level block by comparing the Y position of the cursor with the Y
|
|
3462
|
+
// position of the block's start (first line) or end (last line).
|
|
3463
|
+
#isCursorOnEdgeLineOfBlock(anchorNode, edge) {
|
|
3464
|
+
const topLevelElement = anchorNode.getTopLevelElement();
|
|
3465
|
+
if (!topLevelElement) return false
|
|
2770
3466
|
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
}
|
|
3467
|
+
const domElement = this.editor.getElementByKey(topLevelElement.getKey());
|
|
3468
|
+
if (!domElement) return false
|
|
2774
3469
|
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
}
|
|
3470
|
+
const nativeSelection = window.getSelection();
|
|
3471
|
+
if (!nativeSelection?.rangeCount) return false
|
|
2778
3472
|
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
#config
|
|
3473
|
+
const cursorRect = this.#getReliableRectFromRange(nativeSelection.getRangeAt(0));
|
|
3474
|
+
if (!cursorRect || this.#isRectUnreliable(cursorRect)) return false
|
|
2782
3475
|
|
|
2783
|
-
|
|
2784
|
-
this.#
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
this.#overrides
|
|
2789
|
-
);
|
|
3476
|
+
const edgeRect = this.#getEdgeCharRect(domElement, edge);
|
|
3477
|
+
if (!edgeRect || this.#isRectUnreliable(edgeRect)) return false
|
|
3478
|
+
|
|
3479
|
+
const tolerance = edgeRect.height > 0 ? edgeRect.height * 0.5 : 5;
|
|
3480
|
+
return Math.abs(cursorRect.top - edgeRect.top) < tolerance
|
|
2790
3481
|
}
|
|
2791
3482
|
|
|
2792
|
-
|
|
3483
|
+
// Get a reliable bounding rect for the first or last character in a DOM
|
|
3484
|
+
// element by creating a non-collapsed range around it.
|
|
3485
|
+
#getEdgeCharRect(element, edge) {
|
|
3486
|
+
const walker = document.createTreeWalker(element, 4 /* NodeFilter.SHOW_TEXT */);
|
|
3487
|
+
let textNode;
|
|
3488
|
+
|
|
3489
|
+
if (edge === "first") {
|
|
3490
|
+
textNode = walker.nextNode();
|
|
3491
|
+
} else {
|
|
3492
|
+
while (walker.nextNode()) textNode = walker.currentNode;
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
if (!textNode || textNode.length === 0) return null
|
|
3496
|
+
|
|
3497
|
+
const range = document.createRange();
|
|
3498
|
+
if (edge === "first") {
|
|
3499
|
+
range.setStart(textNode, 0);
|
|
3500
|
+
range.setEnd(textNode, 1);
|
|
3501
|
+
} else {
|
|
3502
|
+
range.setStart(textNode, textNode.length - 1);
|
|
3503
|
+
range.setEnd(textNode, textNode.length);
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
return range.getBoundingClientRect()
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
function sanitize(html) {
|
|
3511
|
+
return DOMPurify.sanitize(html, buildConfig())
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
class EditorConfiguration {
|
|
3515
|
+
#editorElement
|
|
3516
|
+
#config
|
|
3517
|
+
|
|
3518
|
+
constructor(editorElement) {
|
|
3519
|
+
this.#editorElement = editorElement;
|
|
3520
|
+
this.#config = new Configuration(
|
|
3521
|
+
Lexxy.presets.get("default"),
|
|
3522
|
+
Lexxy.presets.get(editorElement.preset),
|
|
3523
|
+
this.#overrides
|
|
3524
|
+
);
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
get(path) {
|
|
2793
3528
|
return this.#config.get(path)
|
|
2794
3529
|
}
|
|
2795
3530
|
|
|
@@ -2818,504 +3553,6 @@ class EditorConfiguration {
|
|
|
2818
3553
|
}
|
|
2819
3554
|
}
|
|
2820
3555
|
|
|
2821
|
-
class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
2822
|
-
static getType() {
|
|
2823
|
-
return "custom_action_text_attachment"
|
|
2824
|
-
}
|
|
2825
|
-
|
|
2826
|
-
static clone(node) {
|
|
2827
|
-
return new CustomActionTextAttachmentNode({ ...node }, node.__key)
|
|
2828
|
-
}
|
|
2829
|
-
|
|
2830
|
-
static importJSON(serializedNode) {
|
|
2831
|
-
return new CustomActionTextAttachmentNode({ ...serializedNode })
|
|
2832
|
-
}
|
|
2833
|
-
|
|
2834
|
-
static importDOM() {
|
|
2835
|
-
|
|
2836
|
-
return {
|
|
2837
|
-
[this.TAG_NAME]: (element) => {
|
|
2838
|
-
if (!element.getAttribute("content")) {
|
|
2839
|
-
return null
|
|
2840
|
-
}
|
|
2841
|
-
|
|
2842
|
-
return {
|
|
2843
|
-
conversion: (attachment) => {
|
|
2844
|
-
// Preserve initial space if present since Lexical removes it
|
|
2845
|
-
const nodes = [];
|
|
2846
|
-
const previousSibling = attachment.previousSibling;
|
|
2847
|
-
if (previousSibling && previousSibling.nodeType === Node.TEXT_NODE && /\s$/.test(previousSibling.textContent)) {
|
|
2848
|
-
nodes.push($createTextNode(" "));
|
|
2849
|
-
}
|
|
2850
|
-
|
|
2851
|
-
nodes.push(new CustomActionTextAttachmentNode({
|
|
2852
|
-
sgid: attachment.getAttribute("sgid"),
|
|
2853
|
-
innerHtml: parseAttachmentContent(attachment.getAttribute("content")),
|
|
2854
|
-
contentType: attachment.getAttribute("content-type")
|
|
2855
|
-
}));
|
|
2856
|
-
|
|
2857
|
-
nodes.push($createTextNode("\u2060"));
|
|
2858
|
-
|
|
2859
|
-
return { node: nodes }
|
|
2860
|
-
},
|
|
2861
|
-
priority: 2
|
|
2862
|
-
}
|
|
2863
|
-
}
|
|
2864
|
-
}
|
|
2865
|
-
}
|
|
2866
|
-
|
|
2867
|
-
static get TAG_NAME() {
|
|
2868
|
-
return Lexxy.global.get("attachmentTagName")
|
|
2869
|
-
}
|
|
2870
|
-
|
|
2871
|
-
constructor({ tagName, sgid, contentType, innerHtml }, key) {
|
|
2872
|
-
super(key);
|
|
2873
|
-
|
|
2874
|
-
const contentTypeNamespace = Lexxy.global.get("attachmentContentTypeNamespace");
|
|
2875
|
-
|
|
2876
|
-
this.tagName = tagName || CustomActionTextAttachmentNode.TAG_NAME;
|
|
2877
|
-
this.sgid = sgid;
|
|
2878
|
-
this.contentType = contentType || `application/vnd.${contentTypeNamespace}.unknown`;
|
|
2879
|
-
this.innerHtml = innerHtml;
|
|
2880
|
-
}
|
|
2881
|
-
|
|
2882
|
-
createDOM() {
|
|
2883
|
-
const figure = createElement(this.tagName, { "content-type": this.contentType, "data-lexxy-decorator": true });
|
|
2884
|
-
|
|
2885
|
-
figure.insertAdjacentHTML("beforeend", this.innerHtml);
|
|
2886
|
-
|
|
2887
|
-
const deleteButton = createElement("lexxy-node-delete-button");
|
|
2888
|
-
figure.appendChild(deleteButton);
|
|
2889
|
-
|
|
2890
|
-
return figure
|
|
2891
|
-
}
|
|
2892
|
-
|
|
2893
|
-
updateDOM() {
|
|
2894
|
-
return false
|
|
2895
|
-
}
|
|
2896
|
-
|
|
2897
|
-
getTextContent() {
|
|
2898
|
-
return "\ufeff"
|
|
2899
|
-
}
|
|
2900
|
-
|
|
2901
|
-
getReadableTextContent() {
|
|
2902
|
-
return this.createDOM().textContent.trim() || `[${this.contentType}]`
|
|
2903
|
-
}
|
|
2904
|
-
|
|
2905
|
-
isInline() {
|
|
2906
|
-
return true
|
|
2907
|
-
}
|
|
2908
|
-
|
|
2909
|
-
exportDOM() {
|
|
2910
|
-
const attachment = createElement(this.tagName, {
|
|
2911
|
-
sgid: this.sgid,
|
|
2912
|
-
content: JSON.stringify(this.innerHtml),
|
|
2913
|
-
"content-type": this.contentType
|
|
2914
|
-
});
|
|
2915
|
-
|
|
2916
|
-
return { element: attachment }
|
|
2917
|
-
}
|
|
2918
|
-
|
|
2919
|
-
exportJSON() {
|
|
2920
|
-
return {
|
|
2921
|
-
type: "custom_action_text_attachment",
|
|
2922
|
-
version: 1,
|
|
2923
|
-
tagName: this.tagName,
|
|
2924
|
-
sgid: this.sgid,
|
|
2925
|
-
contentType: this.contentType,
|
|
2926
|
-
innerHtml: this.innerHtml
|
|
2927
|
-
}
|
|
2928
|
-
}
|
|
2929
|
-
|
|
2930
|
-
decorate() {
|
|
2931
|
-
return null
|
|
2932
|
-
}
|
|
2933
|
-
|
|
2934
|
-
}
|
|
2935
|
-
|
|
2936
|
-
class FormatEscaper {
|
|
2937
|
-
constructor(editorElement) {
|
|
2938
|
-
this.editorElement = editorElement;
|
|
2939
|
-
this.editor = editorElement.editor;
|
|
2940
|
-
}
|
|
2941
|
-
|
|
2942
|
-
monitor() {
|
|
2943
|
-
this.editor.registerCommand(
|
|
2944
|
-
KEY_ENTER_COMMAND,
|
|
2945
|
-
(event) => this.#handleEnterKey(event),
|
|
2946
|
-
COMMAND_PRIORITY_HIGH
|
|
2947
|
-
);
|
|
2948
|
-
|
|
2949
|
-
this.editor.registerCommand(
|
|
2950
|
-
KEY_ARROW_DOWN_COMMAND,
|
|
2951
|
-
(event) => this.#handleArrowDownInCodeBlock(event),
|
|
2952
|
-
COMMAND_PRIORITY_NORMAL
|
|
2953
|
-
);
|
|
2954
|
-
}
|
|
2955
|
-
|
|
2956
|
-
#handleEnterKey(event) {
|
|
2957
|
-
const selection = $getSelection();
|
|
2958
|
-
if (!$isRangeSelection(selection)) return false
|
|
2959
|
-
|
|
2960
|
-
if (this.#handleCodeBlocks(event, selection)) return true
|
|
2961
|
-
|
|
2962
|
-
const anchorNode = selection.anchor.getNode();
|
|
2963
|
-
|
|
2964
|
-
if (!this.#isInsideBlockquote(anchorNode)) return false
|
|
2965
|
-
|
|
2966
|
-
return this.#handleLists(event, anchorNode)
|
|
2967
|
-
|| this.#handleBlockquotes(event, anchorNode)
|
|
2968
|
-
}
|
|
2969
|
-
|
|
2970
|
-
#handleLists(event, anchorNode) {
|
|
2971
|
-
if (this.#shouldEscapeFromEmptyListItem(anchorNode) || this.#shouldEscapeFromEmptyParagraphInListItem(anchorNode)) {
|
|
2972
|
-
event.preventDefault();
|
|
2973
|
-
this.#escapeFromList(anchorNode);
|
|
2974
|
-
return true
|
|
2975
|
-
}
|
|
2976
|
-
|
|
2977
|
-
return false
|
|
2978
|
-
}
|
|
2979
|
-
|
|
2980
|
-
#handleBlockquotes(event, anchorNode) {
|
|
2981
|
-
if (this.#shouldEscapeFromEmptyParagraphInBlockquote(anchorNode)) {
|
|
2982
|
-
event.preventDefault();
|
|
2983
|
-
this.#escapeFromBlockquote(anchorNode);
|
|
2984
|
-
return true
|
|
2985
|
-
}
|
|
2986
|
-
|
|
2987
|
-
return false
|
|
2988
|
-
}
|
|
2989
|
-
|
|
2990
|
-
#isInsideBlockquote(node) {
|
|
2991
|
-
let currentNode = node;
|
|
2992
|
-
|
|
2993
|
-
while (currentNode) {
|
|
2994
|
-
if ($isQuoteNode(currentNode)) {
|
|
2995
|
-
return true
|
|
2996
|
-
}
|
|
2997
|
-
currentNode = currentNode.getParent();
|
|
2998
|
-
}
|
|
2999
|
-
|
|
3000
|
-
return false
|
|
3001
|
-
}
|
|
3002
|
-
|
|
3003
|
-
#shouldEscapeFromEmptyListItem(node) {
|
|
3004
|
-
const listItem = this.#getListItemNode(node);
|
|
3005
|
-
if (!listItem) return false
|
|
3006
|
-
|
|
3007
|
-
return this.#isNodeEmpty(listItem)
|
|
3008
|
-
}
|
|
3009
|
-
|
|
3010
|
-
#shouldEscapeFromEmptyParagraphInListItem(node) {
|
|
3011
|
-
const paragraph = this.#getParagraphNode(node);
|
|
3012
|
-
if (!paragraph) return false
|
|
3013
|
-
|
|
3014
|
-
if (!this.#isNodeEmpty(paragraph)) return false
|
|
3015
|
-
|
|
3016
|
-
const parent = paragraph.getParent();
|
|
3017
|
-
return parent && $isListItemNode(parent)
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
|
-
#isNodeEmpty(node) {
|
|
3021
|
-
if (node.getTextContent().trim() !== "") return false
|
|
3022
|
-
|
|
3023
|
-
const children = node.getChildren();
|
|
3024
|
-
if (children.length === 0) return true
|
|
3025
|
-
|
|
3026
|
-
return children.every(child => {
|
|
3027
|
-
if ($isLineBreakNode(child)) return true
|
|
3028
|
-
return this.#isNodeEmpty(child)
|
|
3029
|
-
})
|
|
3030
|
-
}
|
|
3031
|
-
|
|
3032
|
-
#getListItemNode(node) {
|
|
3033
|
-
let currentNode = node;
|
|
3034
|
-
|
|
3035
|
-
while (currentNode) {
|
|
3036
|
-
if ($isListItemNode(currentNode)) {
|
|
3037
|
-
return currentNode
|
|
3038
|
-
}
|
|
3039
|
-
currentNode = currentNode.getParent();
|
|
3040
|
-
}
|
|
3041
|
-
|
|
3042
|
-
return null
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
#escapeFromList(anchorNode) {
|
|
3046
|
-
const listItem = this.#getListItemNode(anchorNode);
|
|
3047
|
-
if (!listItem) return
|
|
3048
|
-
|
|
3049
|
-
const parentList = listItem.getParent();
|
|
3050
|
-
if (!parentList || !$isListNode(parentList)) return
|
|
3051
|
-
|
|
3052
|
-
const blockquote = parentList.getParent();
|
|
3053
|
-
const isInBlockquote = blockquote && $isQuoteNode(blockquote);
|
|
3054
|
-
|
|
3055
|
-
if (isInBlockquote) {
|
|
3056
|
-
const listItemsAfter = this.#getListItemSiblingsAfter(listItem);
|
|
3057
|
-
const nonEmptyListItems = listItemsAfter.filter(item => !this.#isNodeEmpty(item));
|
|
3058
|
-
|
|
3059
|
-
if (nonEmptyListItems.length > 0) {
|
|
3060
|
-
this.#splitBlockquoteWithList(blockquote, parentList, listItem, nonEmptyListItems);
|
|
3061
|
-
return
|
|
3062
|
-
}
|
|
3063
|
-
}
|
|
3064
|
-
|
|
3065
|
-
const paragraph = $createParagraphNode();
|
|
3066
|
-
parentList.insertAfter(paragraph);
|
|
3067
|
-
|
|
3068
|
-
listItem.remove();
|
|
3069
|
-
paragraph.selectStart();
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
|
-
#shouldEscapeFromEmptyParagraphInBlockquote(node) {
|
|
3073
|
-
const paragraph = this.#getParagraphNode(node);
|
|
3074
|
-
if (!paragraph) return false
|
|
3075
|
-
|
|
3076
|
-
if (!this.#isNodeEmpty(paragraph)) return false
|
|
3077
|
-
|
|
3078
|
-
const parent = paragraph.getParent();
|
|
3079
|
-
return parent && $isQuoteNode(parent)
|
|
3080
|
-
}
|
|
3081
|
-
|
|
3082
|
-
#getParagraphNode(node) {
|
|
3083
|
-
let currentNode = node;
|
|
3084
|
-
|
|
3085
|
-
while (currentNode) {
|
|
3086
|
-
if ($isParagraphNode(currentNode)) {
|
|
3087
|
-
return currentNode
|
|
3088
|
-
}
|
|
3089
|
-
currentNode = currentNode.getParent();
|
|
3090
|
-
}
|
|
3091
|
-
|
|
3092
|
-
return null
|
|
3093
|
-
}
|
|
3094
|
-
|
|
3095
|
-
#escapeFromBlockquote(anchorNode) {
|
|
3096
|
-
const paragraph = this.#getParagraphNode(anchorNode);
|
|
3097
|
-
if (!paragraph) return
|
|
3098
|
-
|
|
3099
|
-
const blockquote = paragraph.getParent();
|
|
3100
|
-
if (!blockquote || !$isQuoteNode(blockquote)) return
|
|
3101
|
-
|
|
3102
|
-
const siblingsAfter = this.#getSiblingsAfter(paragraph);
|
|
3103
|
-
const nonEmptySiblings = siblingsAfter.filter(sibling => !this.#isNodeEmpty(sibling));
|
|
3104
|
-
|
|
3105
|
-
if (nonEmptySiblings.length > 0) {
|
|
3106
|
-
this.#splitBlockquote(blockquote, paragraph, nonEmptySiblings);
|
|
3107
|
-
} else {
|
|
3108
|
-
const newParagraph = $createParagraphNode();
|
|
3109
|
-
blockquote.insertAfter(newParagraph);
|
|
3110
|
-
paragraph.remove();
|
|
3111
|
-
newParagraph.selectStart();
|
|
3112
|
-
}
|
|
3113
|
-
}
|
|
3114
|
-
|
|
3115
|
-
#getSiblingsAfter(node) {
|
|
3116
|
-
const siblings = [];
|
|
3117
|
-
let sibling = node.getNextSibling();
|
|
3118
|
-
|
|
3119
|
-
while (sibling) {
|
|
3120
|
-
siblings.push(sibling);
|
|
3121
|
-
sibling = sibling.getNextSibling();
|
|
3122
|
-
}
|
|
3123
|
-
|
|
3124
|
-
return siblings
|
|
3125
|
-
}
|
|
3126
|
-
|
|
3127
|
-
#getListItemSiblingsAfter(listItem) {
|
|
3128
|
-
const siblings = [];
|
|
3129
|
-
let sibling = listItem.getNextSibling();
|
|
3130
|
-
|
|
3131
|
-
while (sibling) {
|
|
3132
|
-
if ($isListItemNode(sibling)) {
|
|
3133
|
-
siblings.push(sibling);
|
|
3134
|
-
}
|
|
3135
|
-
sibling = sibling.getNextSibling();
|
|
3136
|
-
}
|
|
3137
|
-
|
|
3138
|
-
return siblings
|
|
3139
|
-
}
|
|
3140
|
-
|
|
3141
|
-
#splitBlockquoteWithList(blockquote, parentList, emptyListItem, listItemsAfter) {
|
|
3142
|
-
const blockquoteSiblingsAfterList = this.#getSiblingsAfter(parentList);
|
|
3143
|
-
const nonEmptyBlockquoteSiblings = blockquoteSiblingsAfterList.filter(sibling => !this.#isNodeEmpty(sibling));
|
|
3144
|
-
|
|
3145
|
-
const middleParagraph = $createParagraphNode();
|
|
3146
|
-
blockquote.insertAfter(middleParagraph);
|
|
3147
|
-
|
|
3148
|
-
const newList = $createListNode(parentList.getListType());
|
|
3149
|
-
|
|
3150
|
-
const newBlockquote = $createQuoteNode();
|
|
3151
|
-
middleParagraph.insertAfter(newBlockquote);
|
|
3152
|
-
newBlockquote.append(newList);
|
|
3153
|
-
|
|
3154
|
-
listItemsAfter.forEach(item => {
|
|
3155
|
-
newList.append(item);
|
|
3156
|
-
});
|
|
3157
|
-
|
|
3158
|
-
nonEmptyBlockquoteSiblings.forEach(sibling => {
|
|
3159
|
-
newBlockquote.append(sibling);
|
|
3160
|
-
});
|
|
3161
|
-
|
|
3162
|
-
emptyListItem.remove();
|
|
3163
|
-
|
|
3164
|
-
this.#removeTrailingEmptyListItems(parentList);
|
|
3165
|
-
this.#removeTrailingEmptyNodes(newBlockquote);
|
|
3166
|
-
|
|
3167
|
-
if (parentList.getChildrenSize() === 0) {
|
|
3168
|
-
parentList.remove();
|
|
3169
|
-
|
|
3170
|
-
if (blockquote.getChildrenSize() === 0) {
|
|
3171
|
-
blockquote.remove();
|
|
3172
|
-
}
|
|
3173
|
-
} else {
|
|
3174
|
-
this.#removeTrailingEmptyNodes(blockquote);
|
|
3175
|
-
}
|
|
3176
|
-
|
|
3177
|
-
middleParagraph.selectStart();
|
|
3178
|
-
}
|
|
3179
|
-
|
|
3180
|
-
#removeTrailingEmptyListItems(list) {
|
|
3181
|
-
const items = list.getChildren();
|
|
3182
|
-
for (let i = items.length - 1; i >= 0; i--) {
|
|
3183
|
-
const item = items[i];
|
|
3184
|
-
if ($isListItemNode(item) && this.#isNodeEmpty(item)) {
|
|
3185
|
-
item.remove();
|
|
3186
|
-
} else {
|
|
3187
|
-
break
|
|
3188
|
-
}
|
|
3189
|
-
}
|
|
3190
|
-
}
|
|
3191
|
-
|
|
3192
|
-
#removeTrailingEmptyNodes(blockquote) {
|
|
3193
|
-
const children = blockquote.getChildren();
|
|
3194
|
-
for (let i = children.length - 1; i >= 0; i--) {
|
|
3195
|
-
const child = children[i];
|
|
3196
|
-
if (this.#isNodeEmpty(child)) {
|
|
3197
|
-
child.remove();
|
|
3198
|
-
} else {
|
|
3199
|
-
break
|
|
3200
|
-
}
|
|
3201
|
-
}
|
|
3202
|
-
}
|
|
3203
|
-
|
|
3204
|
-
#splitBlockquote(blockquote, emptyParagraph, siblingsAfter) {
|
|
3205
|
-
const newParagraph = $createParagraphNode();
|
|
3206
|
-
blockquote.insertAfter(newParagraph);
|
|
3207
|
-
|
|
3208
|
-
const newBlockquote = $createQuoteNode();
|
|
3209
|
-
newParagraph.insertAfter(newBlockquote);
|
|
3210
|
-
|
|
3211
|
-
siblingsAfter.forEach(sibling => {
|
|
3212
|
-
newBlockquote.append(sibling);
|
|
3213
|
-
});
|
|
3214
|
-
|
|
3215
|
-
emptyParagraph.remove();
|
|
3216
|
-
|
|
3217
|
-
this.#removeTrailingEmptyNodes(blockquote);
|
|
3218
|
-
this.#removeTrailingEmptyNodes(newBlockquote);
|
|
3219
|
-
|
|
3220
|
-
newParagraph.selectStart();
|
|
3221
|
-
}
|
|
3222
|
-
|
|
3223
|
-
// Code blocks
|
|
3224
|
-
|
|
3225
|
-
#handleCodeBlocks(event, selection) {
|
|
3226
|
-
if (!selection.isCollapsed()) return false
|
|
3227
|
-
|
|
3228
|
-
const codeNode = this.#getCodeNodeFromSelection(selection);
|
|
3229
|
-
if (!codeNode) return false
|
|
3230
|
-
|
|
3231
|
-
if (this.#isCursorOnEmptyLastLineOfCodeBlock(selection, codeNode)) {
|
|
3232
|
-
event?.preventDefault();
|
|
3233
|
-
this.#exitCodeBlock(codeNode);
|
|
3234
|
-
return true
|
|
3235
|
-
}
|
|
3236
|
-
|
|
3237
|
-
return false
|
|
3238
|
-
}
|
|
3239
|
-
|
|
3240
|
-
#handleArrowDownInCodeBlock(event) {
|
|
3241
|
-
const selection = $getSelection();
|
|
3242
|
-
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false
|
|
3243
|
-
|
|
3244
|
-
const codeNode = this.#getCodeNodeFromSelection(selection);
|
|
3245
|
-
if (!codeNode) return false
|
|
3246
|
-
|
|
3247
|
-
if (this.#isCursorOnLastLineOfCodeBlock(selection, codeNode) && !codeNode.getNextSibling()) {
|
|
3248
|
-
event?.preventDefault();
|
|
3249
|
-
const paragraph = $createParagraphNode();
|
|
3250
|
-
codeNode.insertAfter(paragraph);
|
|
3251
|
-
paragraph.selectStart();
|
|
3252
|
-
return true
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
return false
|
|
3256
|
-
}
|
|
3257
|
-
|
|
3258
|
-
#getCodeNodeFromSelection(selection) {
|
|
3259
|
-
const anchorNode = selection.anchor.getNode();
|
|
3260
|
-
return $getNearestNodeOfType(anchorNode, CodeNode) || ($isCodeNode(anchorNode) ? anchorNode : null)
|
|
3261
|
-
}
|
|
3262
|
-
|
|
3263
|
-
#isCursorOnEmptyLastLineOfCodeBlock(selection, codeNode) {
|
|
3264
|
-
const children = codeNode.getChildren();
|
|
3265
|
-
if (children.length === 0) return true
|
|
3266
|
-
|
|
3267
|
-
const anchorNode = selection.anchor.getNode();
|
|
3268
|
-
const anchorOffset = selection.anchor.offset;
|
|
3269
|
-
|
|
3270
|
-
// Chromium: cursor on the CodeNode element after the last child (a line break)
|
|
3271
|
-
if ($isCodeNode(anchorNode) && anchorOffset === children.length) {
|
|
3272
|
-
return $isLineBreakNode(children[children.length - 1])
|
|
3273
|
-
}
|
|
3274
|
-
|
|
3275
|
-
// Firefox: cursor on an empty text node that follows a line break at the end
|
|
3276
|
-
if ($isTextNode(anchorNode) && anchorNode.getTextContentSize() === 0 && anchorOffset === 0) {
|
|
3277
|
-
const previousSibling = anchorNode.getPreviousSibling();
|
|
3278
|
-
return $isLineBreakNode(previousSibling) && anchorNode.getNextSibling() === null
|
|
3279
|
-
}
|
|
3280
|
-
|
|
3281
|
-
return false
|
|
3282
|
-
}
|
|
3283
|
-
|
|
3284
|
-
#isCursorOnLastLineOfCodeBlock(selection, codeNode) {
|
|
3285
|
-
const anchorNode = selection.anchor.getNode();
|
|
3286
|
-
const children = codeNode.getChildren();
|
|
3287
|
-
if (children.length === 0) return true
|
|
3288
|
-
|
|
3289
|
-
const lastChild = children[children.length - 1];
|
|
3290
|
-
|
|
3291
|
-
if ($isCodeNode(anchorNode) && selection.anchor.offset === children.length) return true
|
|
3292
|
-
if (anchorNode === lastChild) return true
|
|
3293
|
-
|
|
3294
|
-
const lastLineBreakIndex = children.findLastIndex(child => $isLineBreakNode(child));
|
|
3295
|
-
if (lastLineBreakIndex === -1) return true
|
|
3296
|
-
|
|
3297
|
-
const anchorIndex = children.indexOf(anchorNode);
|
|
3298
|
-
return anchorIndex > lastLineBreakIndex
|
|
3299
|
-
}
|
|
3300
|
-
|
|
3301
|
-
#exitCodeBlock(codeNode) {
|
|
3302
|
-
const children = codeNode.getChildren();
|
|
3303
|
-
const lastChild = children[children.length - 1];
|
|
3304
|
-
|
|
3305
|
-
if ($isTextNode(lastChild) && lastChild.getTextContentSize() === 0) {
|
|
3306
|
-
const previousSibling = lastChild.getPreviousSibling();
|
|
3307
|
-
lastChild.remove();
|
|
3308
|
-
if ($isLineBreakNode(previousSibling)) previousSibling.remove();
|
|
3309
|
-
} else if ($isLineBreakNode(lastChild)) {
|
|
3310
|
-
lastChild.remove();
|
|
3311
|
-
}
|
|
3312
|
-
|
|
3313
|
-
const paragraph = $createParagraphNode();
|
|
3314
|
-
codeNode.insertAfter(paragraph);
|
|
3315
|
-
paragraph.selectStart();
|
|
3316
|
-
}
|
|
3317
|
-
}
|
|
3318
|
-
|
|
3319
3556
|
async function loadFileIntoImage(file, image) {
|
|
3320
3557
|
return new Promise((resolve) => {
|
|
3321
3558
|
const reader = new FileReader();
|
|
@@ -3537,14 +3774,11 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
3537
3774
|
const editorHasFocus = this.#editorHasFocus;
|
|
3538
3775
|
|
|
3539
3776
|
this.editor.update(() => {
|
|
3540
|
-
const shouldTransferNodeSelection = editorHasFocus && this.isSelected();
|
|
3541
|
-
|
|
3542
3777
|
const replacementNode = this.#toActionTextAttachmentNodeWith(blob);
|
|
3543
3778
|
this.replace(replacementNode);
|
|
3544
3779
|
|
|
3545
|
-
if (
|
|
3546
|
-
|
|
3547
|
-
$setSelection(nodeSelection);
|
|
3780
|
+
if (editorHasFocus && $isRootOrShadowRoot(replacementNode.getParent())) {
|
|
3781
|
+
replacementNode.selectNext();
|
|
3548
3782
|
}
|
|
3549
3783
|
}, { tag: this.#backgroundUpdateTags });
|
|
3550
3784
|
}
|
|
@@ -3636,14 +3870,12 @@ class ImageGalleryNode extends ElementNode {
|
|
|
3636
3870
|
static importDOM() {
|
|
3637
3871
|
return {
|
|
3638
3872
|
div: (element) => {
|
|
3639
|
-
|
|
3640
|
-
if (!containsAttachment) return null
|
|
3873
|
+
if (!this.#isGalleryElement(element)) return null
|
|
3641
3874
|
|
|
3642
3875
|
return {
|
|
3643
3876
|
conversion: () => {
|
|
3644
3877
|
return {
|
|
3645
|
-
node: $createImageGalleryNode()
|
|
3646
|
-
after: children => children
|
|
3878
|
+
node: $createImageGalleryNode()
|
|
3647
3879
|
}
|
|
3648
3880
|
},
|
|
3649
3881
|
priority: 2
|
|
@@ -3660,6 +3892,13 @@ class ImageGalleryNode extends ElementNode {
|
|
|
3660
3892
|
return $isActionTextAttachmentNode(node) && node.isPreviewableImage
|
|
3661
3893
|
}
|
|
3662
3894
|
|
|
3895
|
+
static #isGalleryElement(element) {
|
|
3896
|
+
const attachmentChildren = element.querySelectorAll(`:scope > :is(${this.#attachmentTags.join()})`);
|
|
3897
|
+
return element.textContent.trim() === ""
|
|
3898
|
+
&& attachmentChildren.length > 0
|
|
3899
|
+
&& element.children.length === attachmentChildren.length
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3663
3902
|
static get #attachmentTags() {
|
|
3664
3903
|
return Object.keys(ActionTextAttachmentNode.importDOM())
|
|
3665
3904
|
}
|
|
@@ -3912,7 +4151,6 @@ class Contents {
|
|
|
3912
4151
|
this.editorElement = editorElement;
|
|
3913
4152
|
this.editor = editorElement.editor;
|
|
3914
4153
|
|
|
3915
|
-
new FormatEscaper(editorElement).monitor();
|
|
3916
4154
|
}
|
|
3917
4155
|
|
|
3918
4156
|
insertHtml(html, { tag } = {}) {
|
|
@@ -3921,6 +4159,7 @@ class Contents {
|
|
|
3921
4159
|
|
|
3922
4160
|
insertDOM(doc, { tag } = {}) {
|
|
3923
4161
|
this.#unwrapPlaceholderAnchors(doc);
|
|
4162
|
+
if (tag === PASTE_TAG) this.#stripTableCellColorStyles(doc);
|
|
3924
4163
|
|
|
3925
4164
|
this.editor.update(() => {
|
|
3926
4165
|
const selection = $getSelection();
|
|
@@ -3958,127 +4197,77 @@ class Contents {
|
|
|
3958
4197
|
this.#insertLineBelowIfLastNode(node);
|
|
3959
4198
|
}
|
|
3960
4199
|
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
const selectedNodes = selection.extract();
|
|
3967
|
-
|
|
3968
|
-
selectedNodes.forEach((node) => {
|
|
3969
|
-
const parent = node.getParent();
|
|
3970
|
-
if (!parent) { return }
|
|
3971
|
-
|
|
3972
|
-
const topLevelElement = node.getTopLevelElementOrThrow();
|
|
3973
|
-
const wrappingNode = newNodeFn();
|
|
3974
|
-
wrappingNode.append(...topLevelElement.getChildren());
|
|
3975
|
-
topLevelElement.replace(wrappingNode);
|
|
3976
|
-
});
|
|
3977
|
-
});
|
|
3978
|
-
}
|
|
3979
|
-
|
|
3980
|
-
toggleNodeWrappingAllSelectedLines(isFormatAppliedFn, newNodeFn) {
|
|
3981
|
-
this.editor.update(() => {
|
|
3982
|
-
const selection = $getSelection();
|
|
3983
|
-
if (!$isRangeSelection(selection)) return
|
|
3984
|
-
|
|
3985
|
-
const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow();
|
|
3986
|
-
|
|
3987
|
-
// Check if format is already applied
|
|
3988
|
-
if (isFormatAppliedFn(topLevelElement)) {
|
|
3989
|
-
this.removeFormattingFromSelectedLines();
|
|
3990
|
-
} else {
|
|
3991
|
-
this.#insertNodeWrappingAllSelectedLines(newNodeFn);
|
|
3992
|
-
}
|
|
3993
|
-
});
|
|
3994
|
-
}
|
|
3995
|
-
|
|
3996
|
-
toggleNodeWrappingAllSelectedNodes(isFormatAppliedFn, newNodeFn) {
|
|
3997
|
-
this.editor.update(() => {
|
|
3998
|
-
const selection = $getSelection();
|
|
3999
|
-
if (!$isRangeSelection(selection)) return
|
|
4000
|
-
|
|
4001
|
-
const topLevelElement = selection.anchor.getNode().getTopLevelElement();
|
|
4002
|
-
|
|
4003
|
-
// Check if format is already applied
|
|
4004
|
-
if (topLevelElement && isFormatAppliedFn(topLevelElement)) {
|
|
4005
|
-
this.#unwrap(topLevelElement);
|
|
4006
|
-
} else {
|
|
4007
|
-
this.#insertNodeWrappingAllSelectedNodes(newNodeFn);
|
|
4008
|
-
}
|
|
4009
|
-
});
|
|
4010
|
-
}
|
|
4011
|
-
|
|
4012
|
-
removeFormattingFromSelectedLines() {
|
|
4013
|
-
this.editor.update(() => {
|
|
4014
|
-
const selection = $getSelection();
|
|
4015
|
-
if (!$isRangeSelection(selection)) return
|
|
4016
|
-
|
|
4017
|
-
const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow();
|
|
4018
|
-
const paragraph = $createParagraphNode();
|
|
4019
|
-
paragraph.append(...topLevelElement.getChildren());
|
|
4020
|
-
topLevelElement.replace(paragraph);
|
|
4021
|
-
});
|
|
4200
|
+
applyParagraphFormat() {
|
|
4201
|
+
const selection = $getSelection();
|
|
4202
|
+
if (!$isRangeSelection(selection)) return
|
|
4203
|
+
|
|
4204
|
+
$setBlocksType(selection, () => $createParagraphNode());
|
|
4022
4205
|
}
|
|
4023
4206
|
|
|
4024
|
-
|
|
4025
|
-
|
|
4207
|
+
applyHeadingFormat(tag) {
|
|
4208
|
+
const selection = $getSelection();
|
|
4209
|
+
if (!$isRangeSelection(selection)) return
|
|
4026
4210
|
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
result = $isRangeSelection(selection) && !selection.isCollapsed();
|
|
4030
|
-
});
|
|
4211
|
+
$setBlocksType(selection, () => $createHeadingNode(tag));
|
|
4212
|
+
}
|
|
4031
4213
|
|
|
4032
|
-
|
|
4214
|
+
#applyCodeBlockFormat() {
|
|
4215
|
+
const selection = $getSelection();
|
|
4216
|
+
if (!$isRangeSelection(selection)) return
|
|
4217
|
+
|
|
4218
|
+
$setBlocksType(selection, () => $createCodeNode("plain"));
|
|
4033
4219
|
}
|
|
4034
4220
|
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4221
|
+
toggleCodeBlock() {
|
|
4222
|
+
const selection = $getSelection();
|
|
4223
|
+
if (!$isRangeSelection(selection)) return
|
|
4224
|
+
|
|
4225
|
+
if (this.#insertNodeIfRoot($createCodeNode("plain"))) return
|
|
4038
4226
|
|
|
4039
|
-
|
|
4040
|
-
const selection = $getSelection();
|
|
4041
|
-
if (!$isRangeSelection(selection) || selection.isCollapsed()) return
|
|
4227
|
+
const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow();
|
|
4042
4228
|
|
|
4043
|
-
|
|
4044
|
-
|
|
4229
|
+
if (topLevelElement && !$isCodeNode(topLevelElement)) {
|
|
4230
|
+
this.#applyCodeBlockFormat();
|
|
4231
|
+
} else {
|
|
4232
|
+
this.applyParagraphFormat();
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4045
4235
|
|
|
4046
|
-
|
|
4047
|
-
|
|
4236
|
+
toggleBlockquote() {
|
|
4237
|
+
const selection = $getSelection();
|
|
4238
|
+
if (!$isRangeSelection(selection)) return
|
|
4048
4239
|
|
|
4049
|
-
|
|
4240
|
+
if (this.#insertNodeIfRoot($createQuoteNode())) return
|
|
4050
4241
|
|
|
4051
|
-
|
|
4052
|
-
if (start === 0 && end === lines.length - 1) return
|
|
4242
|
+
const topLevelElements = this.#topLevelElementsInSelection(selection);
|
|
4053
4243
|
|
|
4054
|
-
|
|
4055
|
-
});
|
|
4244
|
+
const allQuoted = topLevelElements.length > 0 && topLevelElements.every($isQuoteNode);
|
|
4056
4245
|
|
|
4057
|
-
if (
|
|
4246
|
+
if (allQuoted) {
|
|
4247
|
+
topLevelElements.forEach(node => this.#unwrap(node));
|
|
4248
|
+
} else {
|
|
4249
|
+
topLevelElements.filter($isQuoteNode).forEach(node => this.#unwrap(node));
|
|
4058
4250
|
|
|
4059
|
-
|
|
4060
|
-
const paragraph = $getNodeByKey(paragraphKey);
|
|
4061
|
-
if (!paragraph || !$isParagraphNode(paragraph)) return
|
|
4251
|
+
this.#splitParagraphsAtLineBreaks(selection);
|
|
4062
4252
|
|
|
4063
|
-
const
|
|
4064
|
-
|
|
4065
|
-
});
|
|
4253
|
+
const elements = this.#topLevelElementsInSelection(selection);
|
|
4254
|
+
if (elements.length === 0) return
|
|
4066
4255
|
|
|
4067
|
-
|
|
4256
|
+
const blockquote = $createQuoteNode();
|
|
4257
|
+
elements[0].insertBefore(blockquote);
|
|
4258
|
+
elements.forEach((element) => blockquote.append(element));
|
|
4259
|
+
}
|
|
4068
4260
|
}
|
|
4069
4261
|
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
const selection = $getSelection();
|
|
4073
|
-
if (!$isRangeSelection(selection)) return
|
|
4262
|
+
hasSelectedText() {
|
|
4263
|
+
let result = false;
|
|
4074
4264
|
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
this.#removeEmptyParentLists(parentLists);
|
|
4079
|
-
this.#selectNewParagraphs(newParagraphs);
|
|
4080
|
-
}
|
|
4265
|
+
this.editor.read(() => {
|
|
4266
|
+
const selection = $getSelection();
|
|
4267
|
+
result = $isRangeSelection(selection) && !selection.isCollapsed();
|
|
4081
4268
|
});
|
|
4269
|
+
|
|
4270
|
+
return result
|
|
4082
4271
|
}
|
|
4083
4272
|
|
|
4084
4273
|
createLink(url) {
|
|
@@ -4170,30 +4359,6 @@ class Contents {
|
|
|
4170
4359
|
this.#performTextReplacement(anchorNode, selection, offset, lastIndex, replacementNodes);
|
|
4171
4360
|
}
|
|
4172
4361
|
|
|
4173
|
-
createParagraphAfterNode(node, text) {
|
|
4174
|
-
const newParagraph = $createParagraphNode();
|
|
4175
|
-
node.insertAfter(newParagraph);
|
|
4176
|
-
newParagraph.selectStart();
|
|
4177
|
-
|
|
4178
|
-
// Insert the typed text
|
|
4179
|
-
if (text) {
|
|
4180
|
-
newParagraph.append($createTextNode(text));
|
|
4181
|
-
newParagraph.select(1, 1); // Place cursor after the text
|
|
4182
|
-
}
|
|
4183
|
-
}
|
|
4184
|
-
|
|
4185
|
-
createParagraphBeforeNode(node, text) {
|
|
4186
|
-
const newParagraph = $createParagraphNode();
|
|
4187
|
-
node.insertBefore(newParagraph);
|
|
4188
|
-
newParagraph.selectStart();
|
|
4189
|
-
|
|
4190
|
-
// Insert the typed text
|
|
4191
|
-
if (text) {
|
|
4192
|
-
newParagraph.append($createTextNode(text));
|
|
4193
|
-
newParagraph.select(1, 1); // Place cursor after the text
|
|
4194
|
-
}
|
|
4195
|
-
}
|
|
4196
|
-
|
|
4197
4362
|
uploadFiles(files, { selectLast } = {}) {
|
|
4198
4363
|
if (!this.editorElement.supportsAttachments) {
|
|
4199
4364
|
console.warn("This editor does not supports attachments (it's configured with [attachments=false])");
|
|
@@ -4251,6 +4416,62 @@ class Contents {
|
|
|
4251
4416
|
});
|
|
4252
4417
|
}
|
|
4253
4418
|
|
|
4419
|
+
#insertNodeIfRoot(node) {
|
|
4420
|
+
const selection = $getSelection();
|
|
4421
|
+
if (!$isRangeSelection(selection)) return false
|
|
4422
|
+
|
|
4423
|
+
const anchorNode = selection.anchor.getNode();
|
|
4424
|
+
if ($isRootOrShadowRoot(anchorNode)) {
|
|
4425
|
+
anchorNode.append(node);
|
|
4426
|
+
node.selectEnd();
|
|
4427
|
+
|
|
4428
|
+
return true
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
return false
|
|
4432
|
+
}
|
|
4433
|
+
|
|
4434
|
+
#splitParagraphsAtLineBreaks(selection) {
|
|
4435
|
+
const selectedNodeKeys = new Set(selection.getNodes().map(n => n.getKey()));
|
|
4436
|
+
const topLevelElements = this.#topLevelElementsInSelection(selection);
|
|
4437
|
+
|
|
4438
|
+
for (const element of topLevelElements) {
|
|
4439
|
+
if (!$isParagraphNode(element)) continue
|
|
4440
|
+
|
|
4441
|
+
const children = element.getChildren();
|
|
4442
|
+
if (!children.some($isLineBreakNode)) continue
|
|
4443
|
+
|
|
4444
|
+
const groups = [ [] ];
|
|
4445
|
+
for (const child of children) {
|
|
4446
|
+
if ($isLineBreakNode(child)) {
|
|
4447
|
+
groups.push([]);
|
|
4448
|
+
child.remove();
|
|
4449
|
+
} else {
|
|
4450
|
+
groups[groups.length - 1].push(child);
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
if (groups.every(group => group.some(child => selectedNodeKeys.has(child.getKey())))) continue
|
|
4455
|
+
|
|
4456
|
+
for (const group of groups) {
|
|
4457
|
+
if (group.length === 0) continue
|
|
4458
|
+
const paragraph = $createParagraphNode();
|
|
4459
|
+
group.forEach(child => paragraph.append(child));
|
|
4460
|
+
element.insertBefore(paragraph);
|
|
4461
|
+
}
|
|
4462
|
+
if (groups.some(group => group.length > 0)) element.remove();
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
|
|
4466
|
+
#topLevelElementsInSelection(selection) {
|
|
4467
|
+
const elements = new Set();
|
|
4468
|
+
for (const node of selection.getNodes()) {
|
|
4469
|
+
const topLevel = node.getTopLevelElement();
|
|
4470
|
+
if (topLevel) elements.add(topLevel);
|
|
4471
|
+
}
|
|
4472
|
+
return Array.from(elements)
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4254
4475
|
#insertUploadNodes(nodes) {
|
|
4255
4476
|
if (nodes.every($isActionTextAttachmentNode)) {
|
|
4256
4477
|
const uploader = Uploader.for(this.editorElement, []);
|
|
@@ -4304,359 +4525,14 @@ class Contents {
|
|
|
4304
4525
|
}
|
|
4305
4526
|
}
|
|
4306
4527
|
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
}
|
|
4316
|
-
|
|
4317
|
-
const topLevelElements = new Set();
|
|
4318
|
-
selectedNodes.forEach((node) => {
|
|
4319
|
-
const topLevel = node.getTopLevelElementOrThrow();
|
|
4320
|
-
topLevelElements.add(topLevel);
|
|
4321
|
-
});
|
|
4322
|
-
|
|
4323
|
-
const elements = this.#withoutTrailingEmptyParagraphs(Array.from(topLevelElements));
|
|
4324
|
-
if (elements.length === 0) {
|
|
4325
|
-
this.#removeStandaloneEmptyParagraph();
|
|
4326
|
-
this.insertAtCursor(newNodeFn());
|
|
4327
|
-
return
|
|
4328
|
-
}
|
|
4329
|
-
|
|
4330
|
-
const wrappingNode = newNodeFn();
|
|
4331
|
-
elements[0].insertBefore(wrappingNode);
|
|
4332
|
-
elements.forEach((element) => {
|
|
4333
|
-
wrappingNode.append(element);
|
|
4334
|
-
});
|
|
4335
|
-
});
|
|
4336
|
-
}
|
|
4337
|
-
|
|
4338
|
-
#withoutTrailingEmptyParagraphs(elements) {
|
|
4339
|
-
let lastNonEmptyIndex = elements.length - 1;
|
|
4340
|
-
|
|
4341
|
-
// Find the last non-empty paragraph
|
|
4342
|
-
while (lastNonEmptyIndex >= 0) {
|
|
4343
|
-
const element = elements[lastNonEmptyIndex];
|
|
4344
|
-
if (!$isParagraphNode(element) || !this.#isElementEmpty(element)) {
|
|
4345
|
-
break
|
|
4346
|
-
}
|
|
4347
|
-
lastNonEmptyIndex--;
|
|
4348
|
-
}
|
|
4349
|
-
|
|
4350
|
-
return elements.slice(0, lastNonEmptyIndex + 1)
|
|
4351
|
-
}
|
|
4352
|
-
|
|
4353
|
-
#isElementEmpty(element) {
|
|
4354
|
-
// Check text content first
|
|
4355
|
-
if (element.getTextContent().trim() !== "") return false
|
|
4356
|
-
|
|
4357
|
-
// Check if it only contains line breaks
|
|
4358
|
-
const children = element.getChildren();
|
|
4359
|
-
return children.length === 0 || children.every(child => $isLineBreakNode(child))
|
|
4360
|
-
}
|
|
4361
|
-
|
|
4362
|
-
#removeStandaloneEmptyParagraph() {
|
|
4363
|
-
const root = $getRoot();
|
|
4364
|
-
if (root.getChildrenSize() === 1) {
|
|
4365
|
-
const firstChild = root.getFirstChild();
|
|
4366
|
-
if (firstChild && $isParagraphNode(firstChild) && this.#isElementEmpty(firstChild)) {
|
|
4367
|
-
firstChild.remove();
|
|
4368
|
-
}
|
|
4369
|
-
}
|
|
4370
|
-
}
|
|
4371
|
-
|
|
4372
|
-
#insertNodeWrappingAllSelectedLines(newNodeFn) {
|
|
4373
|
-
this.editor.update(() => {
|
|
4374
|
-
const selection = $getSelection();
|
|
4375
|
-
if (!$isRangeSelection(selection)) return
|
|
4376
|
-
|
|
4377
|
-
if (selection.isCollapsed()) {
|
|
4378
|
-
this.#wrapCurrentLine(selection, newNodeFn);
|
|
4379
|
-
} else {
|
|
4380
|
-
this.#wrapMultipleSelectedLines(selection, newNodeFn);
|
|
4381
|
-
}
|
|
4382
|
-
});
|
|
4383
|
-
}
|
|
4384
|
-
|
|
4385
|
-
#wrapCurrentLine(selection, newNodeFn) {
|
|
4386
|
-
const anchorNode = selection.anchor.getNode();
|
|
4387
|
-
|
|
4388
|
-
const topLevelElement = anchorNode.getTopLevelElementOrThrow();
|
|
4389
|
-
|
|
4390
|
-
if (topLevelElement.getTextContent()) {
|
|
4391
|
-
const wrappingNode = newNodeFn();
|
|
4392
|
-
wrappingNode.append(...topLevelElement.getChildren());
|
|
4393
|
-
topLevelElement.replace(wrappingNode);
|
|
4394
|
-
} else {
|
|
4395
|
-
selection.insertNodes([ newNodeFn() ]);
|
|
4396
|
-
}
|
|
4397
|
-
}
|
|
4398
|
-
|
|
4399
|
-
#wrapMultipleSelectedLines(selection, newNodeFn) {
|
|
4400
|
-
const selectedParagraphs = this.#extractSelectedParagraphs(selection);
|
|
4401
|
-
if (selectedParagraphs.length === 0) return
|
|
4402
|
-
|
|
4403
|
-
const { lineSet, nodesToDelete } = this.#extractUniqueLines(selectedParagraphs);
|
|
4404
|
-
if (lineSet.size === 0) return
|
|
4405
|
-
|
|
4406
|
-
const wrappingNode = this.#createWrappingNodeWithLines(newNodeFn, lineSet);
|
|
4407
|
-
this.#replaceWithWrappingNode(selection, wrappingNode);
|
|
4408
|
-
this.#removeNodes(nodesToDelete);
|
|
4409
|
-
}
|
|
4410
|
-
|
|
4411
|
-
#extractSelectedParagraphs(selection) {
|
|
4412
|
-
const selectedNodes = selection.extract();
|
|
4413
|
-
const selectedParagraphs = selectedNodes
|
|
4414
|
-
.map((node) => this.#getParagraphFromNode(node))
|
|
4415
|
-
.filter(Boolean);
|
|
4416
|
-
|
|
4417
|
-
$setSelection(null);
|
|
4418
|
-
return selectedParagraphs
|
|
4419
|
-
}
|
|
4420
|
-
|
|
4421
|
-
#getParagraphFromNode(node) {
|
|
4422
|
-
if ($isParagraphNode(node)) return node
|
|
4423
|
-
if ($isTextNode(node) && node.getParent() && $isParagraphNode(node.getParent())) {
|
|
4424
|
-
return node.getParent()
|
|
4425
|
-
}
|
|
4426
|
-
return null
|
|
4427
|
-
}
|
|
4428
|
-
|
|
4429
|
-
#extractUniqueLines(selectedParagraphs) {
|
|
4430
|
-
const lineSet = new Set();
|
|
4431
|
-
const nodesToDelete = new Set();
|
|
4432
|
-
|
|
4433
|
-
selectedParagraphs.forEach((paragraphNode) => {
|
|
4434
|
-
const textContent = paragraphNode.getTextContent();
|
|
4435
|
-
if (textContent) {
|
|
4436
|
-
textContent.split("\n").forEach((line) => {
|
|
4437
|
-
if (line.trim()) lineSet.add(line);
|
|
4438
|
-
});
|
|
4439
|
-
}
|
|
4440
|
-
nodesToDelete.add(paragraphNode);
|
|
4441
|
-
});
|
|
4442
|
-
|
|
4443
|
-
return { lineSet, nodesToDelete }
|
|
4444
|
-
}
|
|
4445
|
-
|
|
4446
|
-
#createWrappingNodeWithLines(newNodeFn, lineSet) {
|
|
4447
|
-
const wrappingNode = newNodeFn();
|
|
4448
|
-
const lines = Array.from(lineSet);
|
|
4449
|
-
|
|
4450
|
-
lines.forEach((lineText, index) => {
|
|
4451
|
-
wrappingNode.append($createTextNode(lineText));
|
|
4452
|
-
if (index < lines.length - 1) {
|
|
4453
|
-
wrappingNode.append($createLineBreakNode());
|
|
4454
|
-
}
|
|
4455
|
-
});
|
|
4456
|
-
|
|
4457
|
-
return wrappingNode
|
|
4458
|
-
}
|
|
4459
|
-
|
|
4460
|
-
#replaceWithWrappingNode(selection, wrappingNode) {
|
|
4461
|
-
const anchorNode = selection.anchor.getNode();
|
|
4462
|
-
const parent = anchorNode.getParent();
|
|
4463
|
-
if (parent) {
|
|
4464
|
-
parent.replace(wrappingNode);
|
|
4465
|
-
}
|
|
4466
|
-
}
|
|
4467
|
-
|
|
4468
|
-
#removeNodes(nodesToDelete) {
|
|
4469
|
-
nodesToDelete.forEach((node) => node.remove());
|
|
4470
|
-
}
|
|
4471
|
-
|
|
4472
|
-
#getSelectedParagraphWithSoftLineBreaks(selection) {
|
|
4473
|
-
const anchorParagraph = this.#getParagraphFromNode(selection.anchor.getNode());
|
|
4474
|
-
const focusParagraph = this.#getParagraphFromNode(selection.focus.getNode());
|
|
4475
|
-
|
|
4476
|
-
if (!anchorParagraph || anchorParagraph !== focusParagraph) return null
|
|
4477
|
-
if ($isQuoteNode(anchorParagraph.getParent())) return null
|
|
4478
|
-
|
|
4479
|
-
return this.#paragraphHasSoftLineBreaks(anchorParagraph) ? anchorParagraph : null
|
|
4480
|
-
}
|
|
4481
|
-
|
|
4482
|
-
#paragraphHasSoftLineBreaks(paragraph) {
|
|
4483
|
-
return paragraph.getChildren().some((child) => $isLineBreakNode(child))
|
|
4484
|
-
}
|
|
4485
|
-
|
|
4486
|
-
#splitParagraphIntoLines(paragraph) {
|
|
4487
|
-
const lines = [ [] ];
|
|
4488
|
-
|
|
4489
|
-
paragraph.getChildren().forEach((child) => {
|
|
4490
|
-
if ($isLineBreakNode(child)) {
|
|
4491
|
-
lines.push([]);
|
|
4492
|
-
} else {
|
|
4493
|
-
lines[lines.length - 1].push(child);
|
|
4494
|
-
}
|
|
4495
|
-
});
|
|
4496
|
-
|
|
4497
|
-
return lines
|
|
4498
|
-
}
|
|
4499
|
-
|
|
4500
|
-
#getSelectedLineRange(lines, selection) {
|
|
4501
|
-
const selectedNodeKeys = new Set(
|
|
4502
|
-
selection.getNodes().map((node) => node.getKey())
|
|
4503
|
-
);
|
|
4504
|
-
|
|
4505
|
-
selectedNodeKeys.add(selection.anchor.getNode().getKey());
|
|
4506
|
-
selectedNodeKeys.add(selection.focus.getNode().getKey());
|
|
4507
|
-
|
|
4508
|
-
const selectedLineIndexes = lines
|
|
4509
|
-
.map((lineNodes, index) => {
|
|
4510
|
-
return lineNodes.some((node) => selectedNodeKeys.has(node.getKey())) ? index : null
|
|
4511
|
-
})
|
|
4512
|
-
.filter((index) => index !== null);
|
|
4513
|
-
|
|
4514
|
-
if (selectedLineIndexes.length === 0) return null
|
|
4515
|
-
|
|
4516
|
-
return {
|
|
4517
|
-
start: selectedLineIndexes[0],
|
|
4518
|
-
end: selectedLineIndexes[selectedLineIndexes.length - 1]
|
|
4519
|
-
}
|
|
4520
|
-
}
|
|
4521
|
-
|
|
4522
|
-
#replaceParagraphWithWrappedSelectedLines(paragraph, lines, { start, end }, newNodeFn) {
|
|
4523
|
-
const insertedNodes = [];
|
|
4524
|
-
|
|
4525
|
-
this.#appendParagraphsForLines(insertedNodes, lines.slice(0, start));
|
|
4526
|
-
|
|
4527
|
-
const wrappingNode = newNodeFn();
|
|
4528
|
-
lines.slice(start, end + 1).forEach((lineNodes) => {
|
|
4529
|
-
wrappingNode.append(this.#createParagraphFromLine(lineNodes));
|
|
4530
|
-
});
|
|
4531
|
-
insertedNodes.push(wrappingNode);
|
|
4532
|
-
|
|
4533
|
-
this.#appendParagraphsForLines(insertedNodes, lines.slice(end + 1));
|
|
4534
|
-
|
|
4535
|
-
let previousNode = null;
|
|
4536
|
-
insertedNodes.forEach((node) => {
|
|
4537
|
-
if (previousNode) {
|
|
4538
|
-
previousNode.insertAfter(node);
|
|
4539
|
-
} else {
|
|
4540
|
-
paragraph.insertBefore(node);
|
|
4541
|
-
}
|
|
4542
|
-
|
|
4543
|
-
previousNode = node;
|
|
4544
|
-
});
|
|
4545
|
-
|
|
4546
|
-
paragraph.remove();
|
|
4547
|
-
}
|
|
4548
|
-
|
|
4549
|
-
#appendParagraphsForLines(insertedNodes, lines) {
|
|
4550
|
-
lines.forEach((lineNodes) => {
|
|
4551
|
-
insertedNodes.push(this.#createParagraphFromLine(lineNodes));
|
|
4552
|
-
});
|
|
4553
|
-
}
|
|
4554
|
-
|
|
4555
|
-
#createParagraphFromLine(lineNodes) {
|
|
4556
|
-
const paragraph = $createParagraphNode();
|
|
4557
|
-
|
|
4558
|
-
if (lineNodes.length === 0) {
|
|
4559
|
-
paragraph.append($createLineBreakNode());
|
|
4560
|
-
} else {
|
|
4561
|
-
paragraph.append(...lineNodes);
|
|
4562
|
-
}
|
|
4563
|
-
|
|
4564
|
-
return paragraph
|
|
4565
|
-
}
|
|
4566
|
-
|
|
4567
|
-
#collectSelectedListItems(selection) {
|
|
4568
|
-
const nodes = selection.getNodes();
|
|
4569
|
-
const listItems = new Set();
|
|
4570
|
-
const parentLists = new Set();
|
|
4571
|
-
|
|
4572
|
-
for (const node of nodes) {
|
|
4573
|
-
const listItem = $getNearestNodeOfType(node, ListItemNode);
|
|
4574
|
-
if (listItem) {
|
|
4575
|
-
listItems.add(listItem);
|
|
4576
|
-
const parentList = listItem.getParent();
|
|
4577
|
-
if (parentList && $isListNode(parentList)) {
|
|
4578
|
-
parentLists.add(parentList);
|
|
4579
|
-
}
|
|
4580
|
-
}
|
|
4581
|
-
}
|
|
4582
|
-
|
|
4583
|
-
return { listItems, parentLists }
|
|
4584
|
-
}
|
|
4585
|
-
|
|
4586
|
-
#convertListItemsToParagraphs(listItems) {
|
|
4587
|
-
const newParagraphs = [];
|
|
4588
|
-
|
|
4589
|
-
for (const listItem of listItems) {
|
|
4590
|
-
const paragraph = this.#convertListItemToParagraph(listItem);
|
|
4591
|
-
if (paragraph) {
|
|
4592
|
-
newParagraphs.push(paragraph);
|
|
4593
|
-
}
|
|
4594
|
-
}
|
|
4595
|
-
|
|
4596
|
-
return newParagraphs
|
|
4597
|
-
}
|
|
4598
|
-
|
|
4599
|
-
#convertListItemToParagraph(listItem) {
|
|
4600
|
-
const parentList = listItem.getParent();
|
|
4601
|
-
if (!parentList || !$isListNode(parentList)) return null
|
|
4602
|
-
|
|
4603
|
-
const paragraph = $createParagraphNode();
|
|
4604
|
-
const sublists = this.#extractSublistsAndContent(listItem, paragraph);
|
|
4605
|
-
|
|
4606
|
-
listItem.insertAfter(paragraph);
|
|
4607
|
-
this.#insertSublists(paragraph, sublists);
|
|
4608
|
-
listItem.remove();
|
|
4609
|
-
|
|
4610
|
-
return paragraph
|
|
4611
|
-
}
|
|
4612
|
-
|
|
4613
|
-
#extractSublistsAndContent(listItem, paragraph) {
|
|
4614
|
-
const sublists = [];
|
|
4615
|
-
|
|
4616
|
-
listItem.getChildren().forEach((child) => {
|
|
4617
|
-
if ($isListNode(child)) {
|
|
4618
|
-
sublists.push(child);
|
|
4619
|
-
} else {
|
|
4620
|
-
paragraph.append(child);
|
|
4621
|
-
}
|
|
4622
|
-
});
|
|
4623
|
-
|
|
4624
|
-
return sublists
|
|
4625
|
-
}
|
|
4626
|
-
|
|
4627
|
-
#insertSublists(paragraph, sublists) {
|
|
4628
|
-
sublists.forEach((sublist) => {
|
|
4629
|
-
paragraph.insertAfter(sublist);
|
|
4630
|
-
});
|
|
4631
|
-
}
|
|
4632
|
-
|
|
4633
|
-
#removeEmptyParentLists(parentLists) {
|
|
4634
|
-
for (const parentList of parentLists) {
|
|
4635
|
-
if ($isListNode(parentList) && parentList.getChildrenSize() === 0) {
|
|
4636
|
-
parentList.remove();
|
|
4637
|
-
}
|
|
4638
|
-
}
|
|
4639
|
-
}
|
|
4640
|
-
|
|
4641
|
-
#selectNewParagraphs(newParagraphs) {
|
|
4642
|
-
if (newParagraphs.length === 0) return
|
|
4643
|
-
|
|
4644
|
-
const firstParagraph = newParagraphs[0];
|
|
4645
|
-
const lastParagraph = newParagraphs[newParagraphs.length - 1];
|
|
4646
|
-
|
|
4647
|
-
if (newParagraphs.length === 1) {
|
|
4648
|
-
firstParagraph.selectEnd();
|
|
4649
|
-
} else {
|
|
4650
|
-
this.#selectParagraphRange(firstParagraph, lastParagraph);
|
|
4651
|
-
}
|
|
4652
|
-
}
|
|
4653
|
-
|
|
4654
|
-
#selectParagraphRange(firstParagraph, lastParagraph) {
|
|
4655
|
-
firstParagraph.selectStart();
|
|
4656
|
-
const currentSelection = $getSelection();
|
|
4657
|
-
if (currentSelection && $isRangeSelection(currentSelection)) {
|
|
4658
|
-
currentSelection.anchor.set(firstParagraph.getKey(), 0, "element");
|
|
4659
|
-
currentSelection.focus.set(lastParagraph.getKey(), lastParagraph.getChildrenSize(), "element");
|
|
4528
|
+
// Table cells copied from a page inherit the source theme's inline color
|
|
4529
|
+
// styles (e.g. dark-mode backgrounds). Strip them so pasted tables adopt
|
|
4530
|
+
// the current theme instead of carrying stale colors.
|
|
4531
|
+
#stripTableCellColorStyles(doc) {
|
|
4532
|
+
for (const cell of doc.querySelectorAll("td, th")) {
|
|
4533
|
+
cell.style.removeProperty("background-color");
|
|
4534
|
+
cell.style.removeProperty("background");
|
|
4535
|
+
cell.style.removeProperty("color");
|
|
4660
4536
|
}
|
|
4661
4537
|
}
|
|
4662
4538
|
|
|
@@ -4683,9 +4559,8 @@ class Contents {
|
|
|
4683
4559
|
const textBeforeString = fullText.slice(0, lastIndex);
|
|
4684
4560
|
const textAfterCursor = fullText.slice(offset);
|
|
4685
4561
|
|
|
4686
|
-
const trailingSpacer = this.#hasInlineDecoratorNode(replacementNodes) ? "\u2060" : " ";
|
|
4687
4562
|
const textNodeBefore = this.#cloneTextNodeFormatting(anchorNode, selection, textBeforeString);
|
|
4688
|
-
const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, selection, textAfterCursor ||
|
|
4563
|
+
const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, selection, textAfterCursor || " ");
|
|
4689
4564
|
|
|
4690
4565
|
anchorNode.replace(textNodeBefore);
|
|
4691
4566
|
|
|
@@ -4697,10 +4572,6 @@ class Contents {
|
|
|
4697
4572
|
textNodeAfter.select(cursorOffset, cursorOffset);
|
|
4698
4573
|
}
|
|
4699
4574
|
|
|
4700
|
-
#hasInlineDecoratorNode(nodes) {
|
|
4701
|
-
return nodes.some(node => node instanceof CustomActionTextAttachmentNode && node.isInline())
|
|
4702
|
-
}
|
|
4703
|
-
|
|
4704
4575
|
#cloneTextNodeFormatting(anchorNode, selection, text) {
|
|
4705
4576
|
const parent = anchorNode.getParent();
|
|
4706
4577
|
const fallbackFormat = parent?.getTextFormat?.() || 0;
|
|
@@ -5074,7 +4945,27 @@ class ProvisionalParagraphNode extends ParagraphNode {
|
|
|
5074
4945
|
// https://github.com/facebook/lexical/blob/f1e4f66014377b1f2595aec2b0ee17f5b7ef4dfc/packages/lexical/src/LexicalNode.ts#L646
|
|
5075
4946
|
isSelected(selection = null) {
|
|
5076
4947
|
const targetSelection = selection || $getSelection();
|
|
5077
|
-
|
|
4948
|
+
if (!targetSelection) return false
|
|
4949
|
+
|
|
4950
|
+
if (targetSelection.getNodes().some(node => node.is(this) || this.isParentOf(node))) return true
|
|
4951
|
+
|
|
4952
|
+
// A collapsed range selection on the parent element at an offset adjacent to
|
|
4953
|
+
// this node means the caret is visually at this paragraph's position. Treat it
|
|
4954
|
+
// as selected so the paragraph is visible and the caret renders correctly.
|
|
4955
|
+
//
|
|
4956
|
+
// Both the offset matching our index (cursor just before us) and index + 1
|
|
4957
|
+
// (cursor just after us) count, because the provisional paragraph is an
|
|
4958
|
+
// invisible spacer: the browser resolves both offsets to the same visual spot.
|
|
4959
|
+
if ($isRangeSelection(targetSelection) && targetSelection.isCollapsed()) {
|
|
4960
|
+
const { anchor } = targetSelection;
|
|
4961
|
+
const parent = this.getParent();
|
|
4962
|
+
if (parent && anchor.getNode().is(parent) && anchor.type === "element") {
|
|
4963
|
+
const index = this.getIndexWithinParent();
|
|
4964
|
+
return anchor.offset === index || anchor.offset === index + 1
|
|
4965
|
+
}
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4968
|
+
return false
|
|
5078
4969
|
}
|
|
5079
4970
|
|
|
5080
4971
|
removeUnlessRequired(self = this.getLatest()) {
|
|
@@ -5355,6 +5246,365 @@ class TablesExtension extends LexxyExtension {
|
|
|
5355
5246
|
}
|
|
5356
5247
|
}
|
|
5357
5248
|
|
|
5249
|
+
const MIME_TYPE = "application/x-lexxy-node-key";
|
|
5250
|
+
|
|
5251
|
+
class AttachmentDragAndDrop {
|
|
5252
|
+
#editor
|
|
5253
|
+
#draggedNodeKey = null
|
|
5254
|
+
#rafId = null
|
|
5255
|
+
#draggingRafId = null
|
|
5256
|
+
#cleanupFns = []
|
|
5257
|
+
|
|
5258
|
+
constructor(editor) {
|
|
5259
|
+
this.#editor = editor;
|
|
5260
|
+
|
|
5261
|
+
// Register Lexical commands at HIGH priority to intercept before the
|
|
5262
|
+
// base @lexical/rich-text handlers (which return true and consume the events).
|
|
5263
|
+
this.#cleanupFns.push(
|
|
5264
|
+
editor.registerCommand(DRAGSTART_COMMAND, (event) => this.#handleDragStart(event), COMMAND_PRIORITY_HIGH),
|
|
5265
|
+
editor.registerCommand(DROP_COMMAND, (event) => this.#handleDrop(event), COMMAND_PRIORITY_HIGH),
|
|
5266
|
+
);
|
|
5267
|
+
|
|
5268
|
+
// Use a root listener to register DOM-level dragover/dragend handlers
|
|
5269
|
+
// (these events need throttled rAF handling that works better as DOM listeners).
|
|
5270
|
+
const unregister = editor.registerRootListener((root, prevRoot) => {
|
|
5271
|
+
if (prevRoot) {
|
|
5272
|
+
prevRoot.removeEventListener("dragover", this.#onDragOver);
|
|
5273
|
+
prevRoot.removeEventListener("dragend", this.#onDragEnd);
|
|
5274
|
+
}
|
|
5275
|
+
if (root) {
|
|
5276
|
+
root.addEventListener("dragover", this.#onDragOver);
|
|
5277
|
+
root.addEventListener("dragend", this.#onDragEnd);
|
|
5278
|
+
}
|
|
5279
|
+
});
|
|
5280
|
+
this.#cleanupFns.push(unregister);
|
|
5281
|
+
}
|
|
5282
|
+
|
|
5283
|
+
destroy() {
|
|
5284
|
+
this.#cleanup();
|
|
5285
|
+
for (const fn of this.#cleanupFns) fn();
|
|
5286
|
+
this.#cleanupFns = [];
|
|
5287
|
+
}
|
|
5288
|
+
|
|
5289
|
+
// -- Event handlers --------------------------------------------------------
|
|
5290
|
+
|
|
5291
|
+
#handleDragStart(event) {
|
|
5292
|
+
if (event.target.closest("textarea")) return false
|
|
5293
|
+
|
|
5294
|
+
const figure = event.target.closest("figure.attachment[data-lexical-node-key]");
|
|
5295
|
+
if (!figure) return false
|
|
5296
|
+
|
|
5297
|
+
this.#draggedNodeKey = figure.dataset.lexicalNodeKey;
|
|
5298
|
+
event.dataTransfer.setData(MIME_TYPE, this.#draggedNodeKey);
|
|
5299
|
+
event.dataTransfer.effectAllowed = "move";
|
|
5300
|
+
|
|
5301
|
+
// Add dragging class after a tick so it doesn't affect the drag image
|
|
5302
|
+
this.#draggingRafId = requestAnimationFrame(() => {
|
|
5303
|
+
this.#draggingRafId = null;
|
|
5304
|
+
figure.classList.add("lexxy-dragging");
|
|
5305
|
+
});
|
|
5306
|
+
|
|
5307
|
+
return true
|
|
5308
|
+
}
|
|
5309
|
+
|
|
5310
|
+
#onDragOver = (event) => {
|
|
5311
|
+
if (!this.#draggedNodeKey) return
|
|
5312
|
+
|
|
5313
|
+
event.preventDefault();
|
|
5314
|
+
event.dataTransfer.dropEffect = "move";
|
|
5315
|
+
|
|
5316
|
+
if (!this.#rafId) {
|
|
5317
|
+
this.#rafId = requestAnimationFrame(() => {
|
|
5318
|
+
this.#rafId = null;
|
|
5319
|
+
this.#updateDropTarget(event);
|
|
5320
|
+
});
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
|
|
5324
|
+
#handleDrop(event) {
|
|
5325
|
+
if (!this.#draggedNodeKey) return false
|
|
5326
|
+
|
|
5327
|
+
event.preventDefault();
|
|
5328
|
+
|
|
5329
|
+
const target = this.#resolveDropTarget(event);
|
|
5330
|
+
const draggedKey = this.#draggedNodeKey;
|
|
5331
|
+
this.#cleanup();
|
|
5332
|
+
|
|
5333
|
+
if (target) {
|
|
5334
|
+
this.#performDrop(draggedKey, target);
|
|
5335
|
+
}
|
|
5336
|
+
return true
|
|
5337
|
+
}
|
|
5338
|
+
|
|
5339
|
+
#onDragEnd = () => {
|
|
5340
|
+
this.#cleanup();
|
|
5341
|
+
}
|
|
5342
|
+
|
|
5343
|
+
// -- Drop target resolution -----------------------------------------------
|
|
5344
|
+
|
|
5345
|
+
#updateDropTarget(event) {
|
|
5346
|
+
this.#clearDropIndicators();
|
|
5347
|
+
|
|
5348
|
+
const target = this.#resolveDropTarget(event);
|
|
5349
|
+
if (!target) return
|
|
5350
|
+
|
|
5351
|
+
if (target.type === "gallery" || target.type === "gallery-reorder") {
|
|
5352
|
+
target.element.classList.add(`lexxy-drop-target--gallery-${target.position}`);
|
|
5353
|
+
} else if (target.type === "list-item") {
|
|
5354
|
+
target.element.classList.add(`lexxy-drop-target--list-${target.position}`);
|
|
5355
|
+
} else {
|
|
5356
|
+
target.element.classList.add(`lexxy-drop-target--block-${target.position}`);
|
|
5357
|
+
}
|
|
5358
|
+
}
|
|
5359
|
+
|
|
5360
|
+
#resolveDropTarget(event) {
|
|
5361
|
+
const element = document.elementFromPoint(event.clientX, event.clientY);
|
|
5362
|
+
if (!element) return null
|
|
5363
|
+
|
|
5364
|
+
const rootElement = this.#editor.getRootElement();
|
|
5365
|
+
if (!rootElement || !rootElement.contains(element)) return null
|
|
5366
|
+
|
|
5367
|
+
// Check if hovering over a previewable image (for gallery merge or reorder)
|
|
5368
|
+
const targetFigure = element.closest("figure.attachment--preview[data-lexical-node-key]");
|
|
5369
|
+
if (targetFigure && targetFigure.dataset.lexicalNodeKey !== this.#draggedNodeKey) {
|
|
5370
|
+
const targetGallery = targetFigure.closest(".attachment-gallery");
|
|
5371
|
+
if (targetGallery) {
|
|
5372
|
+
// If the dragged image is in the same gallery, this is a reorder
|
|
5373
|
+
const draggedFigure = rootElement.querySelector(`[data-lexical-node-key="${this.#draggedNodeKey}"]`);
|
|
5374
|
+
if (draggedFigure && targetGallery.contains(draggedFigure)) {
|
|
5375
|
+
const position = this.#computeHorizontalPosition(targetFigure, event.clientX);
|
|
5376
|
+
return { type: "gallery-reorder", element: targetFigure, nodeKey: targetFigure.dataset.lexicalNodeKey, position }
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
const position = this.#computeHorizontalPosition(targetFigure, event.clientX);
|
|
5380
|
+
return { type: "gallery", element: targetFigure, nodeKey: targetFigure.dataset.lexicalNodeKey, position }
|
|
5381
|
+
}
|
|
5382
|
+
|
|
5383
|
+
// Hovering over the dragged image itself inside a gallery — treat as no-op
|
|
5384
|
+
// to prevent fallthrough to the block handler, which would eject it from the gallery.
|
|
5385
|
+
if (targetFigure && targetFigure.closest(".attachment-gallery")) return null
|
|
5386
|
+
|
|
5387
|
+
// Check if hovering over a gallery's empty space (for reorder within gallery)
|
|
5388
|
+
const targetGallery = element.closest(".attachment-gallery");
|
|
5389
|
+
if (targetGallery) {
|
|
5390
|
+
let galleryFigure = element.closest("figure.attachment[data-lexical-node-key]");
|
|
5391
|
+
if (!galleryFigure) {
|
|
5392
|
+
galleryFigure = this.#findNearestFigureInGallery(targetGallery, event.clientX);
|
|
5393
|
+
}
|
|
5394
|
+
if (galleryFigure && galleryFigure.dataset.lexicalNodeKey !== this.#draggedNodeKey) {
|
|
5395
|
+
const position = this.#computeHorizontalPosition(galleryFigure, event.clientX);
|
|
5396
|
+
return { type: "gallery-reorder", element: galleryFigure, nodeKey: galleryFigure.dataset.lexicalNodeKey, position }
|
|
5397
|
+
}
|
|
5398
|
+
// Nearest figure is the dragged image — no-op to avoid block handler fallthrough
|
|
5399
|
+
if (galleryFigure) return null
|
|
5400
|
+
}
|
|
5401
|
+
|
|
5402
|
+
// Check if hovering over a list item (for list splitting)
|
|
5403
|
+
const listItem = element.closest("li");
|
|
5404
|
+
if (listItem && rootElement.contains(listItem)) {
|
|
5405
|
+
const position = this.#computeVerticalPosition(listItem, event.clientY);
|
|
5406
|
+
return { type: "list-item", element: listItem, position }
|
|
5407
|
+
}
|
|
5408
|
+
|
|
5409
|
+
// Otherwise, find nearest block-level element for between-block insertion.
|
|
5410
|
+
// Normalize so each gap has exactly one indicator: prefer "after" on the
|
|
5411
|
+
// previous sibling, falling back to "before" only for the first block.
|
|
5412
|
+
const block = this.#findNearestBlock(element, rootElement, event.clientY);
|
|
5413
|
+
if (!block) return null
|
|
5414
|
+
|
|
5415
|
+
const position = this.#computeVerticalPosition(block, event.clientY);
|
|
5416
|
+
if (position === "before" && block.previousElementSibling) {
|
|
5417
|
+
return { type: "block", element: block.previousElementSibling, position: "after" }
|
|
5418
|
+
}
|
|
5419
|
+
return { type: "block", element: block, position }
|
|
5420
|
+
}
|
|
5421
|
+
|
|
5422
|
+
#findNearestBlock(element, rootElement, clientY) {
|
|
5423
|
+
let current = element;
|
|
5424
|
+
while (current && current !== rootElement) {
|
|
5425
|
+
if (current.parentElement === rootElement) return current
|
|
5426
|
+
current = current.parentElement;
|
|
5427
|
+
}
|
|
5428
|
+
|
|
5429
|
+
// elementFromPoint landed on the root itself (e.g. a margin gap between
|
|
5430
|
+
// blocks). Fall back to the nearest child by vertical distance.
|
|
5431
|
+
let nearest = null;
|
|
5432
|
+
let minDistance = Infinity;
|
|
5433
|
+
for (const child of rootElement.children) {
|
|
5434
|
+
const rect = child.getBoundingClientRect();
|
|
5435
|
+
const distance = Math.min(Math.abs(clientY - rect.top), Math.abs(clientY - rect.bottom));
|
|
5436
|
+
if (distance < minDistance) {
|
|
5437
|
+
minDistance = distance;
|
|
5438
|
+
nearest = child;
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
return nearest
|
|
5442
|
+
}
|
|
5443
|
+
|
|
5444
|
+
#computeVerticalPosition(element, clientY) {
|
|
5445
|
+
const rect = element.getBoundingClientRect();
|
|
5446
|
+
return clientY < rect.top + rect.height / 2 ? "before" : "after"
|
|
5447
|
+
}
|
|
5448
|
+
|
|
5449
|
+
#computeHorizontalPosition(element, clientX) {
|
|
5450
|
+
const rect = element.getBoundingClientRect();
|
|
5451
|
+
return clientX < rect.left + rect.width / 2 ? "before" : "after"
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5454
|
+
#findNearestFigureInGallery(gallery, clientX) {
|
|
5455
|
+
const figures = gallery.querySelectorAll("figure.attachment[data-lexical-node-key]");
|
|
5456
|
+
let nearest = null;
|
|
5457
|
+
let minDistance = Infinity;
|
|
5458
|
+
for (const figure of figures) {
|
|
5459
|
+
const rect = figure.getBoundingClientRect();
|
|
5460
|
+
const center = rect.left + rect.width / 2;
|
|
5461
|
+
const distance = Math.abs(clientX - center);
|
|
5462
|
+
if (distance < minDistance) {
|
|
5463
|
+
minDistance = distance;
|
|
5464
|
+
nearest = figure;
|
|
5465
|
+
}
|
|
5466
|
+
}
|
|
5467
|
+
return nearest
|
|
5468
|
+
}
|
|
5469
|
+
|
|
5470
|
+
// -- Drop indicator --------------------------------------------------------
|
|
5471
|
+
|
|
5472
|
+
static #DROP_CLASSES = [
|
|
5473
|
+
"lexxy-drop-target--gallery-before", "lexxy-drop-target--gallery-after",
|
|
5474
|
+
"lexxy-drop-target--list-before", "lexxy-drop-target--list-after",
|
|
5475
|
+
"lexxy-drop-target--block-before", "lexxy-drop-target--block-after",
|
|
5476
|
+
]
|
|
5477
|
+
|
|
5478
|
+
#clearDropIndicators() {
|
|
5479
|
+
const rootElement = this.#editor.getRootElement();
|
|
5480
|
+
if (!rootElement) return
|
|
5481
|
+
|
|
5482
|
+
for (const el of rootElement.querySelectorAll("[class*='lexxy-drop-target--']")) {
|
|
5483
|
+
el.classList.remove(...AttachmentDragAndDrop.#DROP_CLASSES);
|
|
5484
|
+
}
|
|
5485
|
+
}
|
|
5486
|
+
|
|
5487
|
+
// -- Node operations -------------------------------------------------------
|
|
5488
|
+
|
|
5489
|
+
#performDrop(draggedKey, target) {
|
|
5490
|
+
const draggedNode = $getNodeByKey(draggedKey);
|
|
5491
|
+
if (!draggedNode || !$isActionTextAttachmentNode(draggedNode)) return
|
|
5492
|
+
|
|
5493
|
+
if (target.type === "gallery") {
|
|
5494
|
+
this.#dropOntoImage(draggedNode, target.nodeKey, target.position);
|
|
5495
|
+
} else if (target.type === "gallery-reorder") {
|
|
5496
|
+
this.#reorderInGallery(draggedNode, target.nodeKey, target.position);
|
|
5497
|
+
} else if (target.type === "list-item") {
|
|
5498
|
+
this.#dropIntoList(draggedNode, target);
|
|
5499
|
+
} else {
|
|
5500
|
+
this.#dropBetweenBlocks(draggedNode, target);
|
|
5501
|
+
}
|
|
5502
|
+
|
|
5503
|
+
// Clear selection to prevent a second history entry. Lexical dispatches
|
|
5504
|
+
// SELECTION_CHANGE_COMMAND during commit for non-range selections, which
|
|
5505
|
+
// creates a separate update. Null selection avoids that dispatch entirely
|
|
5506
|
+
// and also prevents Firefox's follow-up selectionchange from dirtying nodes.
|
|
5507
|
+
$setSelection(null);
|
|
5508
|
+
}
|
|
5509
|
+
|
|
5510
|
+
#dropOntoImage(draggedNode, targetKey, position) {
|
|
5511
|
+
const targetNode = $getNodeByKey(targetKey);
|
|
5512
|
+
if (!targetNode || !$isActionTextAttachmentNode(targetNode)) return
|
|
5513
|
+
if (draggedNode.is(targetNode)) return
|
|
5514
|
+
|
|
5515
|
+
draggedNode.remove();
|
|
5516
|
+
|
|
5517
|
+
const gallery = $findOrCreateGalleryForImage(targetNode);
|
|
5518
|
+
if (gallery) {
|
|
5519
|
+
if (position === "before") {
|
|
5520
|
+
targetNode.insertBefore(draggedNode);
|
|
5521
|
+
} else {
|
|
5522
|
+
targetNode.insertAfter(draggedNode);
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
}
|
|
5526
|
+
|
|
5527
|
+
#reorderInGallery(draggedNode, targetKey, position) {
|
|
5528
|
+
const targetNode = $getNodeByKey(targetKey);
|
|
5529
|
+
if (!targetNode || draggedNode.is(targetNode)) return
|
|
5530
|
+
|
|
5531
|
+
draggedNode.remove();
|
|
5532
|
+
|
|
5533
|
+
if (position === "before") {
|
|
5534
|
+
targetNode.insertBefore(draggedNode);
|
|
5535
|
+
} else {
|
|
5536
|
+
targetNode.insertAfter(draggedNode);
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5539
|
+
|
|
5540
|
+
#dropIntoList(draggedNode, target) {
|
|
5541
|
+
const listItemNode = $getNearestNodeFromDOMNode(target.element);
|
|
5542
|
+
if (!listItemNode || !$isListItemNode(listItemNode)) return
|
|
5543
|
+
|
|
5544
|
+
const listNode = listItemNode.getParent();
|
|
5545
|
+
if (!listNode || !$isListNode(listNode)) return
|
|
5546
|
+
|
|
5547
|
+
const children = listNode.getChildren();
|
|
5548
|
+
const index = children.indexOf(listItemNode);
|
|
5549
|
+
if (index === -1) return
|
|
5550
|
+
|
|
5551
|
+
const splitIndex = target.position === "before" ? index : index + 1;
|
|
5552
|
+
|
|
5553
|
+
draggedNode.remove();
|
|
5554
|
+
|
|
5555
|
+
if (splitIndex === 0) {
|
|
5556
|
+
listNode.insertBefore(draggedNode);
|
|
5557
|
+
} else if (splitIndex >= children.length) {
|
|
5558
|
+
listNode.insertAfter(draggedNode);
|
|
5559
|
+
} else {
|
|
5560
|
+
const [ , listAfter ] = $splitNode(listNode, splitIndex);
|
|
5561
|
+
listAfter.insertBefore(draggedNode);
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
|
|
5565
|
+
#dropBetweenBlocks(draggedNode, target) {
|
|
5566
|
+
const targetNode = $getNearestNodeFromDOMNode(target.element);
|
|
5567
|
+
if (!targetNode) return
|
|
5568
|
+
|
|
5569
|
+
const topLevelTarget = targetNode.getTopLevelElement?.() || targetNode;
|
|
5570
|
+
if (draggedNode.is(topLevelTarget)) return
|
|
5571
|
+
|
|
5572
|
+
draggedNode.remove();
|
|
5573
|
+
|
|
5574
|
+
if (target.position === "before") {
|
|
5575
|
+
topLevelTarget.insertBefore(draggedNode);
|
|
5576
|
+
} else {
|
|
5577
|
+
topLevelTarget.insertAfter(draggedNode);
|
|
5578
|
+
}
|
|
5579
|
+
}
|
|
5580
|
+
|
|
5581
|
+
// -- Lifecycle helpers -----------------------------------------------------
|
|
5582
|
+
|
|
5583
|
+
#cleanup() {
|
|
5584
|
+
this.#clearDropIndicators();
|
|
5585
|
+
|
|
5586
|
+
if (this.#draggedNodeKey) {
|
|
5587
|
+
const rootElement = this.#editor.getRootElement();
|
|
5588
|
+
if (rootElement) {
|
|
5589
|
+
const figure = rootElement.querySelector(`[data-lexical-node-key="${this.#draggedNodeKey}"]`);
|
|
5590
|
+
figure?.classList.remove("lexxy-dragging");
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
|
|
5594
|
+
this.#draggedNodeKey = null;
|
|
5595
|
+
|
|
5596
|
+
if (this.#rafId) {
|
|
5597
|
+
cancelAnimationFrame(this.#rafId);
|
|
5598
|
+
this.#rafId = null;
|
|
5599
|
+
}
|
|
5600
|
+
|
|
5601
|
+
if (this.#draggingRafId) {
|
|
5602
|
+
cancelAnimationFrame(this.#draggingRafId);
|
|
5603
|
+
this.#draggingRafId = null;
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
|
|
5358
5608
|
class AttachmentsExtension extends LexxyExtension {
|
|
5359
5609
|
get enabled() {
|
|
5360
5610
|
return this.editorElement.supportsAttachments
|
|
@@ -5369,14 +5619,37 @@ class AttachmentsExtension extends LexxyExtension {
|
|
|
5369
5619
|
ImageGalleryNode
|
|
5370
5620
|
],
|
|
5371
5621
|
register(editor) {
|
|
5622
|
+
const dragAndDrop = new AttachmentDragAndDrop(editor);
|
|
5623
|
+
|
|
5372
5624
|
return mergeRegister(
|
|
5373
|
-
editor.
|
|
5625
|
+
editor.registerNodeTransform(ActionTextAttachmentNode, $extractAttachmentFromParagraph),
|
|
5626
|
+
editor.registerCommand(DELETE_CHARACTER_COMMAND, $collapseIntoGallery, COMMAND_PRIORITY_NORMAL),
|
|
5627
|
+
() => dragAndDrop.destroy()
|
|
5374
5628
|
)
|
|
5375
5629
|
}
|
|
5376
5630
|
})
|
|
5377
5631
|
}
|
|
5378
5632
|
}
|
|
5379
5633
|
|
|
5634
|
+
// Decorator nodes can be wrapped in a Paragraph Node by Lexical when contained in a <div>
|
|
5635
|
+
// We remove them, splitting the node as needed
|
|
5636
|
+
function $extractAttachmentFromParagraph(attachmentNode) {
|
|
5637
|
+
const parentNode = attachmentNode.getParent();
|
|
5638
|
+
if (!$isParagraphNode(parentNode)) return
|
|
5639
|
+
|
|
5640
|
+
if (parentNode.getChildrenSize() === 1) {
|
|
5641
|
+
parentNode.replace(attachmentNode);
|
|
5642
|
+
} else {
|
|
5643
|
+
const index = attachmentNode.getIndexWithinParent();
|
|
5644
|
+
const [ topParagraph, bottomParagraph ] = $splitNode(parentNode, index);
|
|
5645
|
+
topParagraph.insertAfter(attachmentNode);
|
|
5646
|
+
|
|
5647
|
+
for (const p of [ topParagraph, bottomParagraph ]) {
|
|
5648
|
+
if (p.isEmpty()) p.remove();
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
|
|
5380
5653
|
function $collapseIntoGallery(backwards) {
|
|
5381
5654
|
const anchor = $getSelection()?.anchor;
|
|
5382
5655
|
if (!anchor) return false
|
|
@@ -5442,6 +5715,178 @@ function $moveSelectionBeforeGallery(anchor) {
|
|
|
5442
5715
|
return true
|
|
5443
5716
|
}
|
|
5444
5717
|
|
|
5718
|
+
class EarlyEscapeCodeNode extends CodeNode {
|
|
5719
|
+
$config() {
|
|
5720
|
+
return this.config("early_escape_code", { extends: CodeNode })
|
|
5721
|
+
}
|
|
5722
|
+
|
|
5723
|
+
static $fromSelection(selection) {
|
|
5724
|
+
const anchorNode = selection.anchor.getNode();
|
|
5725
|
+
return $getNearestNodeOfType(anchorNode, EarlyEscapeCodeNode)
|
|
5726
|
+
|| (anchorNode instanceof EarlyEscapeCodeNode ? anchorNode : null)
|
|
5727
|
+
}
|
|
5728
|
+
|
|
5729
|
+
insertNewAfter(selection, restoreSelection) {
|
|
5730
|
+
if (!selection.isCollapsed()) return super.insertNewAfter(selection, restoreSelection)
|
|
5731
|
+
|
|
5732
|
+
if (this.#isCursorOnEmptyLastLine(selection)) {
|
|
5733
|
+
$trimTrailingBlankNodes(this);
|
|
5734
|
+
|
|
5735
|
+
const paragraph = $createParagraphNode();
|
|
5736
|
+
this.insertAfter(paragraph);
|
|
5737
|
+
return paragraph
|
|
5738
|
+
}
|
|
5739
|
+
|
|
5740
|
+
return super.insertNewAfter(selection, restoreSelection)
|
|
5741
|
+
}
|
|
5742
|
+
|
|
5743
|
+
#isCursorOnEmptyLastLine(selection) {
|
|
5744
|
+
if (!$isCursorOnLastLine(selection)) return false
|
|
5745
|
+
|
|
5746
|
+
const textContent = this.getTextContent();
|
|
5747
|
+
return textContent === "" || textContent.endsWith("\n")
|
|
5748
|
+
}
|
|
5749
|
+
|
|
5750
|
+
}
|
|
5751
|
+
|
|
5752
|
+
class EarlyEscapeListItemNode extends ListItemNode {
|
|
5753
|
+
$config() {
|
|
5754
|
+
return this.config("early_escape_listitem", { extends: ListItemNode })
|
|
5755
|
+
}
|
|
5756
|
+
|
|
5757
|
+
insertNewAfter(selection, restoreSelection) {
|
|
5758
|
+
if (this.#shouldEscape(selection)) {
|
|
5759
|
+
return this.#escapeFromList()
|
|
5760
|
+
}
|
|
5761
|
+
|
|
5762
|
+
return super.insertNewAfter(selection, restoreSelection)
|
|
5763
|
+
}
|
|
5764
|
+
|
|
5765
|
+
#shouldEscape(selection) {
|
|
5766
|
+
if (!$getNearestNodeOfType(this, QuoteNode)) return false
|
|
5767
|
+
if ($isBlankNode(this)) return true
|
|
5768
|
+
|
|
5769
|
+
const paragraph = $getNearestNodeOfType(selection.anchor.getNode(), ParagraphNode);
|
|
5770
|
+
return paragraph && $isBlankNode(paragraph) && $isListItemNode(paragraph.getParent())
|
|
5771
|
+
}
|
|
5772
|
+
|
|
5773
|
+
#escapeFromList() {
|
|
5774
|
+
const parentList = this.getParent();
|
|
5775
|
+
if (!parentList || !$isListNode(parentList)) return
|
|
5776
|
+
|
|
5777
|
+
const blockquote = parentList.getParent();
|
|
5778
|
+
const isInBlockquote = blockquote && $isQuoteNode(blockquote);
|
|
5779
|
+
|
|
5780
|
+
if (isInBlockquote) {
|
|
5781
|
+
const hasNonEmptyListItems = this.getNextSiblings().some(
|
|
5782
|
+
sibling => $isListItemNode(sibling) && !$isBlankNode(sibling)
|
|
5783
|
+
);
|
|
5784
|
+
|
|
5785
|
+
if (hasNonEmptyListItems) {
|
|
5786
|
+
return this.#splitBlockquoteWithList()
|
|
5787
|
+
}
|
|
5788
|
+
}
|
|
5789
|
+
|
|
5790
|
+
const paragraph = $createParagraphNode();
|
|
5791
|
+
parentList.insertAfter(paragraph);
|
|
5792
|
+
|
|
5793
|
+
this.remove();
|
|
5794
|
+
return paragraph
|
|
5795
|
+
}
|
|
5796
|
+
|
|
5797
|
+
#splitBlockquoteWithList() {
|
|
5798
|
+
const splitQuotes = $splitNode(this.getParent(), this.getIndexWithinParent());
|
|
5799
|
+
this.remove();
|
|
5800
|
+
|
|
5801
|
+
const middleParagraph = $createParagraphNode();
|
|
5802
|
+
splitQuotes[0].insertAfter(middleParagraph);
|
|
5803
|
+
|
|
5804
|
+
splitQuotes.forEach($trimTrailingBlankNodes);
|
|
5805
|
+
|
|
5806
|
+
return middleParagraph
|
|
5807
|
+
}
|
|
5808
|
+
|
|
5809
|
+
}
|
|
5810
|
+
|
|
5811
|
+
class FormatEscapeExtension extends LexxyExtension {
|
|
5812
|
+
|
|
5813
|
+
get enabled() {
|
|
5814
|
+
return this.editorElement.supportsRichText
|
|
5815
|
+
}
|
|
5816
|
+
|
|
5817
|
+
get lexicalExtension() {
|
|
5818
|
+
return defineExtension({
|
|
5819
|
+
name: "lexxy/format-escape",
|
|
5820
|
+
nodes: [
|
|
5821
|
+
EarlyEscapeCodeNode,
|
|
5822
|
+
{ replace: CodeNode, with: (node) => new EarlyEscapeCodeNode(node.getLanguage()), withKlass: EarlyEscapeCodeNode },
|
|
5823
|
+
EarlyEscapeListItemNode,
|
|
5824
|
+
{ replace: ListItemNode, with: () => new EarlyEscapeListItemNode(), withKlass: EarlyEscapeListItemNode },
|
|
5825
|
+
],
|
|
5826
|
+
register(editor) {
|
|
5827
|
+
return mergeRegister(
|
|
5828
|
+
editor.registerCommand(
|
|
5829
|
+
INSERT_PARAGRAPH_COMMAND,
|
|
5830
|
+
() => $escapeFromBlockquote(),
|
|
5831
|
+
COMMAND_PRIORITY_HIGH
|
|
5832
|
+
),
|
|
5833
|
+
editor.registerCommand(
|
|
5834
|
+
KEY_ARROW_DOWN_COMMAND,
|
|
5835
|
+
(event) => $handleArrowDownInCodeBlock(event),
|
|
5836
|
+
COMMAND_PRIORITY_NORMAL
|
|
5837
|
+
)
|
|
5838
|
+
)
|
|
5839
|
+
}
|
|
5840
|
+
})
|
|
5841
|
+
}
|
|
5842
|
+
}
|
|
5843
|
+
|
|
5844
|
+
function $escapeFromBlockquote() {
|
|
5845
|
+
const anchorNode = $getSelection().anchor.getNode();
|
|
5846
|
+
|
|
5847
|
+
const paragraph = $getNearestNodeOfType(anchorNode, ParagraphNode);
|
|
5848
|
+
if (!paragraph || !$isBlankNode(paragraph)) return false
|
|
5849
|
+
|
|
5850
|
+
const blockquote = paragraph.getParent();
|
|
5851
|
+
if (!blockquote || !$isQuoteNode(blockquote)) return false
|
|
5852
|
+
|
|
5853
|
+
const nonEmptySiblings = paragraph.getNextSiblings().filter(sibling => !$isBlankNode(sibling));
|
|
5854
|
+
|
|
5855
|
+
if (nonEmptySiblings.length > 0) {
|
|
5856
|
+
$splitQuoteNode(blockquote, paragraph);
|
|
5857
|
+
} else {
|
|
5858
|
+
blockquote.insertAfter(paragraph);
|
|
5859
|
+
paragraph.selectStart();
|
|
5860
|
+
}
|
|
5861
|
+
|
|
5862
|
+
return true
|
|
5863
|
+
}
|
|
5864
|
+
|
|
5865
|
+
function $splitQuoteNode(node, paragraph) {
|
|
5866
|
+
const splitQuotes = $splitNode(node, paragraph.getIndexWithinParent());
|
|
5867
|
+
splitQuotes[0].insertAfter(paragraph);
|
|
5868
|
+
splitQuotes.forEach($trimTrailingBlankNodes);
|
|
5869
|
+
paragraph.selectEnd();
|
|
5870
|
+
}
|
|
5871
|
+
|
|
5872
|
+
function $handleArrowDownInCodeBlock(event) {
|
|
5873
|
+
const selection = $getSelection();
|
|
5874
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false
|
|
5875
|
+
|
|
5876
|
+
const codeNode = EarlyEscapeCodeNode.$fromSelection(selection);
|
|
5877
|
+
if (!codeNode) return false
|
|
5878
|
+
|
|
5879
|
+
if ($isCursorOnLastLine(selection) && !codeNode.getNextSibling()) {
|
|
5880
|
+
event?.preventDefault();
|
|
5881
|
+
const paragraph = $createParagraphNode();
|
|
5882
|
+
codeNode.insertAfter(paragraph);
|
|
5883
|
+
paragraph.selectEnd();
|
|
5884
|
+
return true
|
|
5885
|
+
}
|
|
5886
|
+
|
|
5887
|
+
return false
|
|
5888
|
+
}
|
|
5889
|
+
|
|
5445
5890
|
class LexicalEditorElement extends HTMLElement {
|
|
5446
5891
|
static formAssociated = true
|
|
5447
5892
|
static debug = false
|
|
@@ -5502,7 +5947,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
5502
5947
|
}
|
|
5503
5948
|
|
|
5504
5949
|
toString() {
|
|
5505
|
-
if (
|
|
5950
|
+
if (this.cachedStringValue == null) {
|
|
5506
5951
|
this.editor?.getEditorState().read(() => {
|
|
5507
5952
|
this.cachedStringValue = $getReadableTextContent($getRoot());
|
|
5508
5953
|
});
|
|
@@ -5532,7 +5977,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
5532
5977
|
HighlightExtension,
|
|
5533
5978
|
TrixContentExtension,
|
|
5534
5979
|
TablesExtension,
|
|
5535
|
-
AttachmentsExtension
|
|
5980
|
+
AttachmentsExtension,
|
|
5981
|
+
FormatEscapeExtension
|
|
5536
5982
|
]
|
|
5537
5983
|
}
|
|
5538
5984
|
|
|
@@ -5623,7 +6069,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
5623
6069
|
return nodes
|
|
5624
6070
|
.filter(this.#isNotWhitespaceOnlyNode)
|
|
5625
6071
|
.map(this.#wrapTextNode)
|
|
5626
|
-
.map(this.#unwrapDecoratorNode)
|
|
5627
6072
|
}
|
|
5628
6073
|
|
|
5629
6074
|
// Whitespace-only text nodes (e.g. "\n" between block elements like <div>) and stray line break
|
|
@@ -5645,18 +6090,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
5645
6090
|
return paragraph
|
|
5646
6091
|
}
|
|
5647
6092
|
|
|
5648
|
-
// Custom decorator block elements such as action-text-attachments get wrapped into <p> automatically by Lexical.
|
|
5649
|
-
// We unwrap those.
|
|
5650
|
-
#unwrapDecoratorNode(node) {
|
|
5651
|
-
if ($isParagraphNode(node) && node.getChildrenSize() === 1) {
|
|
5652
|
-
const child = node.getFirstChild();
|
|
5653
|
-
if ($isDecoratorNode(child) && !child.isInline()) {
|
|
5654
|
-
return child
|
|
5655
|
-
}
|
|
5656
|
-
}
|
|
5657
|
-
return node
|
|
5658
|
-
}
|
|
5659
|
-
|
|
5660
6093
|
#initialize() {
|
|
5661
6094
|
this.#synchronizeWithChanges();
|
|
5662
6095
|
this.#registerComponents();
|
|
@@ -5895,22 +6328,23 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
5895
6328
|
}
|
|
5896
6329
|
|
|
5897
6330
|
#findOrCreateDefaultToolbar() {
|
|
5898
|
-
const
|
|
5899
|
-
if (
|
|
5900
|
-
return document.getElementById(
|
|
6331
|
+
const toolbarConfig = this.config.get("toolbar");
|
|
6332
|
+
if (typeof toolbarConfig === "string") {
|
|
6333
|
+
return document.getElementById(toolbarConfig)
|
|
5901
6334
|
} else {
|
|
5902
6335
|
return this.#createDefaultToolbar()
|
|
5903
6336
|
}
|
|
5904
6337
|
}
|
|
5905
6338
|
|
|
5906
6339
|
get #hasToolbar() {
|
|
5907
|
-
return this.supportsRichText && this.config.get("toolbar")
|
|
6340
|
+
return this.supportsRichText && !!this.config.get("toolbar")
|
|
5908
6341
|
}
|
|
5909
6342
|
|
|
5910
6343
|
#createDefaultToolbar() {
|
|
5911
6344
|
const toolbar = createElement("lexxy-toolbar");
|
|
5912
6345
|
toolbar.innerHTML = LexicalToolbarElement.defaultTemplate;
|
|
5913
6346
|
toolbar.setAttribute("data-attachments", this.supportsAttachments); // Drives toolbar CSS styles
|
|
6347
|
+
toolbar.configure(this.config.get("toolbar"));
|
|
5914
6348
|
this.prepend(toolbar);
|
|
5915
6349
|
return toolbar
|
|
5916
6350
|
}
|
|
@@ -5977,6 +6411,10 @@ function $getReadableTextContent(node) {
|
|
|
5977
6411
|
const children = node.getChildren();
|
|
5978
6412
|
for (let i = 0; i < children.length; i++) {
|
|
5979
6413
|
const child = children[i];
|
|
6414
|
+
const previousChild = children[i - 1];
|
|
6415
|
+
|
|
6416
|
+
if (isAttachmentSpacerTextNode(child, previousChild, i, children.length)) continue
|
|
6417
|
+
|
|
5980
6418
|
text += $getReadableTextContent(child);
|
|
5981
6419
|
if ($isElementNode(child) && i !== children.length - 1 && !child.isInline()) {
|
|
5982
6420
|
text += "\n\n";
|
|
@@ -6361,6 +6799,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
6361
6799
|
constructor() {
|
|
6362
6800
|
super();
|
|
6363
6801
|
this.keyListeners = [];
|
|
6802
|
+
this.showPopoverId = 0;
|
|
6364
6803
|
}
|
|
6365
6804
|
|
|
6366
6805
|
static observedAttributes = [ "connected" ]
|
|
@@ -6506,9 +6945,14 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
6506
6945
|
}
|
|
6507
6946
|
|
|
6508
6947
|
async #showPopover() {
|
|
6948
|
+
const showId = ++this.showPopoverId;
|
|
6509
6949
|
this.popoverElement ??= await this.#buildPopover();
|
|
6950
|
+
if (this.showPopoverId !== showId) return
|
|
6951
|
+
|
|
6510
6952
|
this.#resetPopoverPosition();
|
|
6511
6953
|
await this.#filterOptions();
|
|
6954
|
+
if (this.showPopoverId !== showId) return
|
|
6955
|
+
|
|
6512
6956
|
this.popoverElement.classList.toggle("lexxy-prompt-menu--visible", true);
|
|
6513
6957
|
this.#selectFirstOption();
|
|
6514
6958
|
|
|
@@ -6619,6 +7063,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
6619
7063
|
}
|
|
6620
7064
|
|
|
6621
7065
|
async #hidePopover() {
|
|
7066
|
+
this.showPopoverId++;
|
|
6622
7067
|
this.#clearSelection();
|
|
6623
7068
|
this.popoverElement.classList.toggle("lexxy-prompt-menu--visible", false);
|
|
6624
7069
|
this.#editorElement.removeEventListener("lexxy:change", this.#filterOptions);
|
|
@@ -6644,6 +7089,14 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
6644
7089
|
|
|
6645
7090
|
if (this.#editorContents.containsTextBackUntil(this.trigger)) {
|
|
6646
7091
|
await this.#showFilteredOptions();
|
|
7092
|
+
|
|
7093
|
+
// Re-check after async operation — the trigger may have been consumed
|
|
7094
|
+
// (e.g. markdown heading shortcut converted "# " to h1 during the fetch)
|
|
7095
|
+
if (!this.#editorContents.containsTextBackUntil(this.trigger)) {
|
|
7096
|
+
this.#hidePopover();
|
|
7097
|
+
return
|
|
7098
|
+
}
|
|
7099
|
+
|
|
6647
7100
|
await nextFrame();
|
|
6648
7101
|
this.#positionPopover();
|
|
6649
7102
|
} else {
|
|
@@ -6652,8 +7105,12 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
6652
7105
|
}
|
|
6653
7106
|
|
|
6654
7107
|
async #showFilteredOptions() {
|
|
7108
|
+
const showId = this.showPopoverId;
|
|
6655
7109
|
const filter = this.#editorContents.textBackUntil(this.trigger);
|
|
6656
7110
|
const filteredListItems = await this.source.buildListItems(filter);
|
|
7111
|
+
if (this.showPopoverId !== showId) return
|
|
7112
|
+
if (!this.#editorContents.containsTextBackUntil(this.trigger)) return
|
|
7113
|
+
|
|
6657
7114
|
this.popoverElement.innerHTML = "";
|
|
6658
7115
|
|
|
6659
7116
|
if (filteredListItems.length > 0) {
|
|
@@ -6968,21 +7425,16 @@ class NodeDeleteButton extends HTMLElement {
|
|
|
6968
7425
|
this.editor = this.editorElement.editor;
|
|
6969
7426
|
this.classList.add("lexxy-floating-controls");
|
|
6970
7427
|
|
|
6971
|
-
if (!this.
|
|
7428
|
+
if (!this.querySelector(".lexxy-node-delete")) {
|
|
6972
7429
|
this.#attachDeleteButton();
|
|
6973
7430
|
}
|
|
6974
7431
|
}
|
|
6975
7432
|
|
|
6976
7433
|
disconnectedCallback() {
|
|
6977
|
-
if (this.deleteButton && this.handleDeleteClick) {
|
|
6978
|
-
this.deleteButton.removeEventListener("click", this.handleDeleteClick);
|
|
6979
|
-
}
|
|
6980
|
-
|
|
6981
|
-
this.handleDeleteClick = null;
|
|
6982
|
-
this.deleteButton = null;
|
|
6983
7434
|
this.editor = null;
|
|
6984
7435
|
this.editorElement = null;
|
|
6985
7436
|
}
|
|
7437
|
+
|
|
6986
7438
|
#attachDeleteButton() {
|
|
6987
7439
|
const container = createElement("div", { className: "lexxy-floating-controls__group" });
|
|
6988
7440
|
|