@37signals/lexxy 0.8.5-beta → 0.9.0-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 +1253 -1239
- package/dist/lexxy_helpers.esm.js +116 -1
- package/dist/stylesheets/lexxy-content.css +4 -0
- package/dist/stylesheets/lexxy-editor.css +67 -1
- 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();
|
|
@@ -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" data-prevent-overflow="true" 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>
|
|
@@ -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,50 +1569,249 @@ 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
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
for (const child of codeElement.childNodes) {
|
|
1643
|
+
walk(child);
|
|
1290
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
|
-
endOffset: $getNodeSelectionOffsets(node, selection)[1],
|
|
1309
|
-
textSize: node.getTextContentSize()
|
|
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
|
+
}
|
|
1311
1665
|
|
|
1312
|
-
|
|
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
|
+
}));
|
|
1813
|
+
|
|
1814
|
+
// Use skipTransforms to prevent the code highlighting system from
|
|
1313
1815
|
// re-tokenizing and wiping out the style changes we apply.
|
|
1314
1816
|
// Use discrete to force a synchronous commit, ensuring the changes
|
|
1315
1817
|
// are committed before editor.focus() triggers a second update cycle
|
|
@@ -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() {
|
|
@@ -1801,28 +2342,40 @@ function nextFrame() {
|
|
|
1801
2342
|
return new Promise(requestAnimationFrame)
|
|
1802
2343
|
}
|
|
1803
2344
|
|
|
1804
|
-
function
|
|
1805
|
-
|
|
1806
|
-
const sizes = [ "B", "KB", "MB", "GB", "TB", "PB" ];
|
|
1807
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
1808
|
-
const value = bytes / Math.pow(1024, i);
|
|
1809
|
-
return `${ value.toFixed(2) } ${ sizes[i] }`
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
function extractFileName(string) {
|
|
1813
|
-
return string.split("/").pop()
|
|
2345
|
+
function dasherize(value) {
|
|
2346
|
+
return value.replace(/([A-Z])/g, (_, char) => `-${char.toLowerCase()}`)
|
|
1814
2347
|
}
|
|
1815
2348
|
|
|
1816
|
-
|
|
1817
|
-
// but Trix/ActionText stores it as raw HTML. Try JSON first, fall back to raw.
|
|
1818
|
-
function parseAttachmentContent(content) {
|
|
2349
|
+
function isUrl(string) {
|
|
1819
2350
|
try {
|
|
1820
|
-
|
|
2351
|
+
new URL(string);
|
|
2352
|
+
return true
|
|
1821
2353
|
} catch {
|
|
1822
|
-
return
|
|
2354
|
+
return false
|
|
1823
2355
|
}
|
|
1824
2356
|
}
|
|
1825
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
|
+
|
|
1826
2379
|
class ActionTextAttachmentNode extends DecoratorNode {
|
|
1827
2380
|
static getType() {
|
|
1828
2381
|
return "action_text_attachment"
|
|
@@ -1903,7 +2456,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
1903
2456
|
this.tagName = tagName || ActionTextAttachmentNode.TAG_NAME;
|
|
1904
2457
|
this.sgid = sgid;
|
|
1905
2458
|
this.src = src;
|
|
1906
|
-
this.previewable = previewable;
|
|
2459
|
+
this.previewable = parseBoolean(previewable);
|
|
1907
2460
|
this.altText = altText || "";
|
|
1908
2461
|
this.caption = caption || "";
|
|
1909
2462
|
this.contentType = contentType || "";
|
|
@@ -2007,11 +2560,32 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2007
2560
|
|
|
2008
2561
|
#createDOMForImage(options = {}) {
|
|
2009
2562
|
const img = createElement("img", { src: this.src, draggable: false, alt: this.altText, ...this.#imageDimensions, ...options });
|
|
2563
|
+
|
|
2564
|
+
if (this.previewable && !this.isPreviewableImage) {
|
|
2565
|
+
img.onerror = () => this.#swapPreviewToFileDOM(img);
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2010
2568
|
const container = createElement("div", { className: "attachment__container" });
|
|
2011
2569
|
container.appendChild(img);
|
|
2012
2570
|
return container
|
|
2013
2571
|
}
|
|
2014
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());
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2015
2589
|
get #imageDimensions() {
|
|
2016
2590
|
if (this.width && this.height) {
|
|
2017
2591
|
return { width: this.width, height: this.height }
|
|
@@ -2109,6 +2683,7 @@ class Selection {
|
|
|
2109
2683
|
this.#listenForNodeSelections();
|
|
2110
2684
|
this.#processSelectionChangeCommands();
|
|
2111
2685
|
this.#containEditorFocus();
|
|
2686
|
+
this.#clearStaleInlineCodeFormat();
|
|
2112
2687
|
}
|
|
2113
2688
|
|
|
2114
2689
|
set current(selection) {
|
|
@@ -2208,16 +2783,19 @@ class Selection {
|
|
|
2208
2783
|
|
|
2209
2784
|
const topLevelElement = anchorNode.getTopLevelElementOrThrow();
|
|
2210
2785
|
const listType = getListType(anchorNode);
|
|
2786
|
+
const headingNode = this.#getNearestHeadingNode(anchorNode);
|
|
2211
2787
|
|
|
2212
2788
|
return {
|
|
2213
2789
|
isBold: selection.hasFormat("bold"),
|
|
2214
2790
|
isItalic: selection.hasFormat("italic"),
|
|
2215
2791
|
isStrikethrough: selection.hasFormat("strikethrough"),
|
|
2792
|
+
isUnderline: selection.hasFormat("underline"),
|
|
2216
2793
|
isHighlight: isSelectionHighlighted(selection),
|
|
2217
2794
|
isInLink: $getNearestNodeOfType(anchorNode, LinkNode) !== null,
|
|
2218
2795
|
isInQuote: $isQuoteNode(topLevelElement),
|
|
2219
|
-
isInHeading:
|
|
2220
|
-
isInCode: selection
|
|
2796
|
+
isInHeading: headingNode !== null,
|
|
2797
|
+
isInCode: this.#isInCode(selection, anchorNode),
|
|
2798
|
+
headingTag: headingNode?.getTag() ?? null,
|
|
2221
2799
|
isInList: listType !== null,
|
|
2222
2800
|
listType,
|
|
2223
2801
|
isInTable: $getTableCellNodeFromLexicalNode(anchorNode) !== null
|
|
@@ -2352,6 +2930,53 @@ class Selection {
|
|
|
2352
2930
|
return this.#findPreviousSiblingUp(anchorNode)
|
|
2353
2931
|
}
|
|
2354
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
|
+
|
|
2355
2980
|
get #currentlySelectedKeys() {
|
|
2356
2981
|
if (this.currentlySelectedKeys) { return this.currentlySelectedKeys }
|
|
2357
2982
|
|
|
@@ -2535,6 +3160,24 @@ class Selection {
|
|
|
2535
3160
|
return anchorNode.getTopLevelElement()
|
|
2536
3161
|
}
|
|
2537
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
|
+
|
|
2538
3181
|
#moveToOrCreateNextLine(topLevelElement) {
|
|
2539
3182
|
const nextSibling = topLevelElement.getNextSibling();
|
|
2540
3183
|
|
|
@@ -2563,6 +3206,8 @@ class Selection {
|
|
|
2563
3206
|
}
|
|
2564
3207
|
|
|
2565
3208
|
#selectDecoratorNodeBeforeDeletion(backwards) {
|
|
3209
|
+
if (backwards && this.#removeEmptyListItem()) return true
|
|
3210
|
+
|
|
2566
3211
|
const node = backwards ? this.nodeBeforeCursor : this.nodeAfterCursor;
|
|
2567
3212
|
if (!$isDecoratorNode(node)) return false
|
|
2568
3213
|
|
|
@@ -2574,6 +3219,38 @@ class Selection {
|
|
|
2574
3219
|
return Boolean(selection)
|
|
2575
3220
|
}
|
|
2576
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
|
+
|
|
2577
3254
|
// When the cursor is inside a list item, collapse the list item into a
|
|
2578
3255
|
// paragraph instead of selecting the decorator. This lets the user
|
|
2579
3256
|
// delete a list that immediately follows an attachment without the
|
|
@@ -2834,33 +3511,6 @@ function sanitize(html) {
|
|
|
2834
3511
|
return DOMPurify.sanitize(html, buildConfig())
|
|
2835
3512
|
}
|
|
2836
3513
|
|
|
2837
|
-
function dasherize(value) {
|
|
2838
|
-
return value.replace(/([A-Z])/g, (_, char) => `-${char.toLowerCase()}`)
|
|
2839
|
-
}
|
|
2840
|
-
|
|
2841
|
-
function isUrl(string) {
|
|
2842
|
-
try {
|
|
2843
|
-
new URL(string);
|
|
2844
|
-
return true
|
|
2845
|
-
} catch {
|
|
2846
|
-
return false
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
|
|
2850
|
-
function normalizeFilteredText(string) {
|
|
2851
|
-
return string
|
|
2852
|
-
.toLowerCase()
|
|
2853
|
-
.normalize("NFD").replace(/[\u0300-\u036f]/g, "") // Remove diacritics
|
|
2854
|
-
}
|
|
2855
|
-
|
|
2856
|
-
function filterMatches(text, potentialMatch) {
|
|
2857
|
-
return normalizeFilteredText(text).includes(normalizeFilteredText(potentialMatch))
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
function upcaseFirst(string) {
|
|
2861
|
-
return string.charAt(0).toUpperCase() + string.slice(1)
|
|
2862
|
-
}
|
|
2863
|
-
|
|
2864
3514
|
class EditorConfiguration {
|
|
2865
3515
|
#editorElement
|
|
2866
3516
|
#config
|
|
@@ -2903,537 +3553,39 @@ class EditorConfiguration {
|
|
|
2903
3553
|
}
|
|
2904
3554
|
}
|
|
2905
3555
|
|
|
2906
|
-
|
|
3556
|
+
async function loadFileIntoImage(file, image) {
|
|
3557
|
+
return new Promise((resolve) => {
|
|
3558
|
+
const reader = new FileReader();
|
|
3559
|
+
|
|
3560
|
+
image.addEventListener("load", () => {
|
|
3561
|
+
resolve(image);
|
|
3562
|
+
});
|
|
3563
|
+
|
|
3564
|
+
reader.onload = (event) => {
|
|
3565
|
+
image.src = event.target.result || null;
|
|
3566
|
+
};
|
|
3567
|
+
|
|
3568
|
+
reader.readAsDataURL(file);
|
|
3569
|
+
})
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
2907
3573
|
static getType() {
|
|
2908
|
-
return "
|
|
3574
|
+
return "action_text_attachment_upload"
|
|
2909
3575
|
}
|
|
2910
3576
|
|
|
2911
3577
|
static clone(node) {
|
|
2912
|
-
return new
|
|
3578
|
+
return new ActionTextAttachmentUploadNode({ ...node }, node.__key)
|
|
2913
3579
|
}
|
|
2914
3580
|
|
|
2915
3581
|
static importJSON(serializedNode) {
|
|
2916
|
-
return new
|
|
3582
|
+
return new ActionTextAttachmentUploadNode({ ...serializedNode })
|
|
2917
3583
|
}
|
|
2918
3584
|
|
|
3585
|
+
// Should never run since this is a transient node. Defined to remove console warning.
|
|
2919
3586
|
static importDOM() {
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
[this.TAG_NAME]: (element) => {
|
|
2923
|
-
if (!element.getAttribute("content")) {
|
|
2924
|
-
return null
|
|
2925
|
-
}
|
|
2926
|
-
|
|
2927
|
-
return {
|
|
2928
|
-
conversion: (attachment) => {
|
|
2929
|
-
// Preserve initial space if present since Lexical removes it
|
|
2930
|
-
const nodes = [];
|
|
2931
|
-
const previousSibling = attachment.previousSibling;
|
|
2932
|
-
if (previousSibling && previousSibling.nodeType === Node.TEXT_NODE && /\s$/.test(previousSibling.textContent)) {
|
|
2933
|
-
nodes.push($createTextNode(" "));
|
|
2934
|
-
}
|
|
2935
|
-
|
|
2936
|
-
nodes.push(new CustomActionTextAttachmentNode({
|
|
2937
|
-
sgid: attachment.getAttribute("sgid"),
|
|
2938
|
-
innerHtml: parseAttachmentContent(attachment.getAttribute("content")),
|
|
2939
|
-
contentType: attachment.getAttribute("content-type")
|
|
2940
|
-
}));
|
|
2941
|
-
|
|
2942
|
-
nodes.push($createTextNode("\u2060"));
|
|
2943
|
-
|
|
2944
|
-
return { node: nodes }
|
|
2945
|
-
},
|
|
2946
|
-
priority: 2
|
|
2947
|
-
}
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2950
|
-
}
|
|
2951
|
-
|
|
2952
|
-
static get TAG_NAME() {
|
|
2953
|
-
return Lexxy.global.get("attachmentTagName")
|
|
2954
|
-
}
|
|
2955
|
-
|
|
2956
|
-
constructor({ tagName, sgid, contentType, innerHtml }, key) {
|
|
2957
|
-
super(key);
|
|
2958
|
-
|
|
2959
|
-
const contentTypeNamespace = Lexxy.global.get("attachmentContentTypeNamespace");
|
|
2960
|
-
|
|
2961
|
-
this.tagName = tagName || CustomActionTextAttachmentNode.TAG_NAME;
|
|
2962
|
-
this.sgid = sgid;
|
|
2963
|
-
this.contentType = contentType || `application/vnd.${contentTypeNamespace}.unknown`;
|
|
2964
|
-
this.innerHtml = innerHtml;
|
|
2965
|
-
}
|
|
2966
|
-
|
|
2967
|
-
createDOM() {
|
|
2968
|
-
const figure = createElement(this.tagName, { "content-type": this.contentType, "data-lexxy-decorator": true });
|
|
2969
|
-
|
|
2970
|
-
figure.insertAdjacentHTML("beforeend", this.innerHtml);
|
|
2971
|
-
|
|
2972
|
-
const deleteButton = createElement("lexxy-node-delete-button");
|
|
2973
|
-
figure.appendChild(deleteButton);
|
|
2974
|
-
|
|
2975
|
-
return figure
|
|
2976
|
-
}
|
|
2977
|
-
|
|
2978
|
-
updateDOM() {
|
|
2979
|
-
return false
|
|
2980
|
-
}
|
|
2981
|
-
|
|
2982
|
-
getTextContent() {
|
|
2983
|
-
return "\ufeff"
|
|
2984
|
-
}
|
|
2985
|
-
|
|
2986
|
-
getReadableTextContent() {
|
|
2987
|
-
return this.createDOM().textContent.trim() || `[${this.contentType}]`
|
|
2988
|
-
}
|
|
2989
|
-
|
|
2990
|
-
isInline() {
|
|
2991
|
-
return true
|
|
2992
|
-
}
|
|
2993
|
-
|
|
2994
|
-
exportDOM() {
|
|
2995
|
-
const attachment = createElement(this.tagName, {
|
|
2996
|
-
sgid: this.sgid,
|
|
2997
|
-
content: JSON.stringify(this.innerHtml),
|
|
2998
|
-
"content-type": this.contentType
|
|
2999
|
-
});
|
|
3000
|
-
|
|
3001
|
-
return { element: attachment }
|
|
3002
|
-
}
|
|
3003
|
-
|
|
3004
|
-
exportJSON() {
|
|
3005
|
-
return {
|
|
3006
|
-
type: "custom_action_text_attachment",
|
|
3007
|
-
version: 1,
|
|
3008
|
-
tagName: this.tagName,
|
|
3009
|
-
sgid: this.sgid,
|
|
3010
|
-
contentType: this.contentType,
|
|
3011
|
-
innerHtml: this.innerHtml
|
|
3012
|
-
}
|
|
3013
|
-
}
|
|
3014
|
-
|
|
3015
|
-
decorate() {
|
|
3016
|
-
return null
|
|
3017
|
-
}
|
|
3018
|
-
|
|
3019
|
-
}
|
|
3020
|
-
|
|
3021
|
-
class FormatEscaper {
|
|
3022
|
-
constructor(editorElement) {
|
|
3023
|
-
this.editorElement = editorElement;
|
|
3024
|
-
this.editor = editorElement.editor;
|
|
3025
|
-
}
|
|
3026
|
-
|
|
3027
|
-
monitor() {
|
|
3028
|
-
this.editor.registerCommand(
|
|
3029
|
-
KEY_ENTER_COMMAND,
|
|
3030
|
-
(event) => this.#handleEnterKey(event),
|
|
3031
|
-
COMMAND_PRIORITY_HIGH
|
|
3032
|
-
);
|
|
3033
|
-
|
|
3034
|
-
this.editor.registerCommand(
|
|
3035
|
-
KEY_ARROW_DOWN_COMMAND,
|
|
3036
|
-
(event) => this.#handleArrowDownInCodeBlock(event),
|
|
3037
|
-
COMMAND_PRIORITY_NORMAL
|
|
3038
|
-
);
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
|
-
#handleEnterKey(event) {
|
|
3042
|
-
const selection = $getSelection();
|
|
3043
|
-
if (!$isRangeSelection(selection)) return false
|
|
3044
|
-
|
|
3045
|
-
if (this.#handleCodeBlocks(event, selection)) return true
|
|
3046
|
-
|
|
3047
|
-
const anchorNode = selection.anchor.getNode();
|
|
3048
|
-
|
|
3049
|
-
if (!this.#isInsideBlockquote(anchorNode)) return false
|
|
3050
|
-
|
|
3051
|
-
return this.#handleLists(event, anchorNode)
|
|
3052
|
-
|| this.#handleBlockquotes(event, anchorNode)
|
|
3053
|
-
}
|
|
3054
|
-
|
|
3055
|
-
#handleLists(event, anchorNode) {
|
|
3056
|
-
if (this.#shouldEscapeFromEmptyListItem(anchorNode) || this.#shouldEscapeFromEmptyParagraphInListItem(anchorNode)) {
|
|
3057
|
-
event.preventDefault();
|
|
3058
|
-
this.#escapeFromList(anchorNode);
|
|
3059
|
-
return true
|
|
3060
|
-
}
|
|
3061
|
-
|
|
3062
|
-
return false
|
|
3063
|
-
}
|
|
3064
|
-
|
|
3065
|
-
#handleBlockquotes(event, anchorNode) {
|
|
3066
|
-
if (this.#shouldEscapeFromEmptyParagraphInBlockquote(anchorNode)) {
|
|
3067
|
-
event.preventDefault();
|
|
3068
|
-
this.#escapeFromBlockquote(anchorNode);
|
|
3069
|
-
return true
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
|
-
return false
|
|
3073
|
-
}
|
|
3074
|
-
|
|
3075
|
-
#isInsideBlockquote(node) {
|
|
3076
|
-
let currentNode = node;
|
|
3077
|
-
|
|
3078
|
-
while (currentNode) {
|
|
3079
|
-
if ($isQuoteNode(currentNode)) {
|
|
3080
|
-
return true
|
|
3081
|
-
}
|
|
3082
|
-
currentNode = currentNode.getParent();
|
|
3083
|
-
}
|
|
3084
|
-
|
|
3085
|
-
return false
|
|
3086
|
-
}
|
|
3087
|
-
|
|
3088
|
-
#shouldEscapeFromEmptyListItem(node) {
|
|
3089
|
-
const listItem = this.#getListItemNode(node);
|
|
3090
|
-
if (!listItem) return false
|
|
3091
|
-
|
|
3092
|
-
return this.#isNodeEmpty(listItem)
|
|
3093
|
-
}
|
|
3094
|
-
|
|
3095
|
-
#shouldEscapeFromEmptyParagraphInListItem(node) {
|
|
3096
|
-
const paragraph = this.#getParagraphNode(node);
|
|
3097
|
-
if (!paragraph) return false
|
|
3098
|
-
|
|
3099
|
-
if (!this.#isNodeEmpty(paragraph)) return false
|
|
3100
|
-
|
|
3101
|
-
const parent = paragraph.getParent();
|
|
3102
|
-
return parent && $isListItemNode(parent)
|
|
3103
|
-
}
|
|
3104
|
-
|
|
3105
|
-
#isNodeEmpty(node) {
|
|
3106
|
-
if (node.getTextContent().trim() !== "") return false
|
|
3107
|
-
|
|
3108
|
-
const children = node.getChildren();
|
|
3109
|
-
if (children.length === 0) return true
|
|
3110
|
-
|
|
3111
|
-
return children.every(child => {
|
|
3112
|
-
if ($isLineBreakNode(child)) return true
|
|
3113
|
-
return this.#isNodeEmpty(child)
|
|
3114
|
-
})
|
|
3115
|
-
}
|
|
3116
|
-
|
|
3117
|
-
#getListItemNode(node) {
|
|
3118
|
-
let currentNode = node;
|
|
3119
|
-
|
|
3120
|
-
while (currentNode) {
|
|
3121
|
-
if ($isListItemNode(currentNode)) {
|
|
3122
|
-
return currentNode
|
|
3123
|
-
}
|
|
3124
|
-
currentNode = currentNode.getParent();
|
|
3125
|
-
}
|
|
3126
|
-
|
|
3127
|
-
return null
|
|
3128
|
-
}
|
|
3129
|
-
|
|
3130
|
-
#escapeFromList(anchorNode) {
|
|
3131
|
-
const listItem = this.#getListItemNode(anchorNode);
|
|
3132
|
-
if (!listItem) return
|
|
3133
|
-
|
|
3134
|
-
const parentList = listItem.getParent();
|
|
3135
|
-
if (!parentList || !$isListNode(parentList)) return
|
|
3136
|
-
|
|
3137
|
-
const blockquote = parentList.getParent();
|
|
3138
|
-
const isInBlockquote = blockquote && $isQuoteNode(blockquote);
|
|
3139
|
-
|
|
3140
|
-
if (isInBlockquote) {
|
|
3141
|
-
const listItemsAfter = this.#getListItemSiblingsAfter(listItem);
|
|
3142
|
-
const nonEmptyListItems = listItemsAfter.filter(item => !this.#isNodeEmpty(item));
|
|
3143
|
-
|
|
3144
|
-
if (nonEmptyListItems.length > 0) {
|
|
3145
|
-
this.#splitBlockquoteWithList(blockquote, parentList, listItem, nonEmptyListItems);
|
|
3146
|
-
return
|
|
3147
|
-
}
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3150
|
-
const paragraph = $createParagraphNode();
|
|
3151
|
-
parentList.insertAfter(paragraph);
|
|
3152
|
-
|
|
3153
|
-
listItem.remove();
|
|
3154
|
-
paragraph.selectStart();
|
|
3155
|
-
}
|
|
3156
|
-
|
|
3157
|
-
#shouldEscapeFromEmptyParagraphInBlockquote(node) {
|
|
3158
|
-
const paragraph = this.#getParagraphNode(node);
|
|
3159
|
-
if (!paragraph) return false
|
|
3160
|
-
|
|
3161
|
-
if (!this.#isNodeEmpty(paragraph)) return false
|
|
3162
|
-
|
|
3163
|
-
const parent = paragraph.getParent();
|
|
3164
|
-
return parent && $isQuoteNode(parent)
|
|
3165
|
-
}
|
|
3166
|
-
|
|
3167
|
-
#getParagraphNode(node) {
|
|
3168
|
-
let currentNode = node;
|
|
3169
|
-
|
|
3170
|
-
while (currentNode) {
|
|
3171
|
-
if ($isParagraphNode(currentNode)) {
|
|
3172
|
-
return currentNode
|
|
3173
|
-
}
|
|
3174
|
-
currentNode = currentNode.getParent();
|
|
3175
|
-
}
|
|
3176
|
-
|
|
3177
|
-
return null
|
|
3178
|
-
}
|
|
3179
|
-
|
|
3180
|
-
#escapeFromBlockquote(anchorNode) {
|
|
3181
|
-
const paragraph = this.#getParagraphNode(anchorNode);
|
|
3182
|
-
if (!paragraph) return
|
|
3183
|
-
|
|
3184
|
-
const blockquote = paragraph.getParent();
|
|
3185
|
-
if (!blockquote || !$isQuoteNode(blockquote)) return
|
|
3186
|
-
|
|
3187
|
-
const siblingsAfter = this.#getSiblingsAfter(paragraph);
|
|
3188
|
-
const nonEmptySiblings = siblingsAfter.filter(sibling => !this.#isNodeEmpty(sibling));
|
|
3189
|
-
|
|
3190
|
-
if (nonEmptySiblings.length > 0) {
|
|
3191
|
-
this.#splitBlockquote(blockquote, paragraph, nonEmptySiblings);
|
|
3192
|
-
} else {
|
|
3193
|
-
const newParagraph = $createParagraphNode();
|
|
3194
|
-
blockquote.insertAfter(newParagraph);
|
|
3195
|
-
paragraph.remove();
|
|
3196
|
-
newParagraph.selectStart();
|
|
3197
|
-
}
|
|
3198
|
-
}
|
|
3199
|
-
|
|
3200
|
-
#getSiblingsAfter(node) {
|
|
3201
|
-
const siblings = [];
|
|
3202
|
-
let sibling = node.getNextSibling();
|
|
3203
|
-
|
|
3204
|
-
while (sibling) {
|
|
3205
|
-
siblings.push(sibling);
|
|
3206
|
-
sibling = sibling.getNextSibling();
|
|
3207
|
-
}
|
|
3208
|
-
|
|
3209
|
-
return siblings
|
|
3210
|
-
}
|
|
3211
|
-
|
|
3212
|
-
#getListItemSiblingsAfter(listItem) {
|
|
3213
|
-
const siblings = [];
|
|
3214
|
-
let sibling = listItem.getNextSibling();
|
|
3215
|
-
|
|
3216
|
-
while (sibling) {
|
|
3217
|
-
if ($isListItemNode(sibling)) {
|
|
3218
|
-
siblings.push(sibling);
|
|
3219
|
-
}
|
|
3220
|
-
sibling = sibling.getNextSibling();
|
|
3221
|
-
}
|
|
3222
|
-
|
|
3223
|
-
return siblings
|
|
3224
|
-
}
|
|
3225
|
-
|
|
3226
|
-
#splitBlockquoteWithList(blockquote, parentList, emptyListItem, listItemsAfter) {
|
|
3227
|
-
const blockquoteSiblingsAfterList = this.#getSiblingsAfter(parentList);
|
|
3228
|
-
const nonEmptyBlockquoteSiblings = blockquoteSiblingsAfterList.filter(sibling => !this.#isNodeEmpty(sibling));
|
|
3229
|
-
|
|
3230
|
-
const middleParagraph = $createParagraphNode();
|
|
3231
|
-
blockquote.insertAfter(middleParagraph);
|
|
3232
|
-
|
|
3233
|
-
const newList = $createListNode(parentList.getListType());
|
|
3234
|
-
|
|
3235
|
-
const newBlockquote = $createQuoteNode();
|
|
3236
|
-
middleParagraph.insertAfter(newBlockquote);
|
|
3237
|
-
newBlockquote.append(newList);
|
|
3238
|
-
|
|
3239
|
-
listItemsAfter.forEach(item => {
|
|
3240
|
-
newList.append(item);
|
|
3241
|
-
});
|
|
3242
|
-
|
|
3243
|
-
nonEmptyBlockquoteSiblings.forEach(sibling => {
|
|
3244
|
-
newBlockquote.append(sibling);
|
|
3245
|
-
});
|
|
3246
|
-
|
|
3247
|
-
emptyListItem.remove();
|
|
3248
|
-
|
|
3249
|
-
this.#removeTrailingEmptyListItems(parentList);
|
|
3250
|
-
this.#removeTrailingEmptyNodes(newBlockquote);
|
|
3251
|
-
|
|
3252
|
-
if (parentList.getChildrenSize() === 0) {
|
|
3253
|
-
parentList.remove();
|
|
3254
|
-
|
|
3255
|
-
if (blockquote.getChildrenSize() === 0) {
|
|
3256
|
-
blockquote.remove();
|
|
3257
|
-
}
|
|
3258
|
-
} else {
|
|
3259
|
-
this.#removeTrailingEmptyNodes(blockquote);
|
|
3260
|
-
}
|
|
3261
|
-
|
|
3262
|
-
middleParagraph.selectStart();
|
|
3263
|
-
}
|
|
3264
|
-
|
|
3265
|
-
#removeTrailingEmptyListItems(list) {
|
|
3266
|
-
const items = list.getChildren();
|
|
3267
|
-
for (let i = items.length - 1; i >= 0; i--) {
|
|
3268
|
-
const item = items[i];
|
|
3269
|
-
if ($isListItemNode(item) && this.#isNodeEmpty(item)) {
|
|
3270
|
-
item.remove();
|
|
3271
|
-
} else {
|
|
3272
|
-
break
|
|
3273
|
-
}
|
|
3274
|
-
}
|
|
3275
|
-
}
|
|
3276
|
-
|
|
3277
|
-
#removeTrailingEmptyNodes(blockquote) {
|
|
3278
|
-
const children = blockquote.getChildren();
|
|
3279
|
-
for (let i = children.length - 1; i >= 0; i--) {
|
|
3280
|
-
const child = children[i];
|
|
3281
|
-
if (this.#isNodeEmpty(child)) {
|
|
3282
|
-
child.remove();
|
|
3283
|
-
} else {
|
|
3284
|
-
break
|
|
3285
|
-
}
|
|
3286
|
-
}
|
|
3287
|
-
}
|
|
3288
|
-
|
|
3289
|
-
#splitBlockquote(blockquote, emptyParagraph, siblingsAfter) {
|
|
3290
|
-
const newParagraph = $createParagraphNode();
|
|
3291
|
-
blockquote.insertAfter(newParagraph);
|
|
3292
|
-
|
|
3293
|
-
const newBlockquote = $createQuoteNode();
|
|
3294
|
-
newParagraph.insertAfter(newBlockquote);
|
|
3295
|
-
|
|
3296
|
-
siblingsAfter.forEach(sibling => {
|
|
3297
|
-
newBlockquote.append(sibling);
|
|
3298
|
-
});
|
|
3299
|
-
|
|
3300
|
-
emptyParagraph.remove();
|
|
3301
|
-
|
|
3302
|
-
this.#removeTrailingEmptyNodes(blockquote);
|
|
3303
|
-
this.#removeTrailingEmptyNodes(newBlockquote);
|
|
3304
|
-
|
|
3305
|
-
newParagraph.selectStart();
|
|
3306
|
-
}
|
|
3307
|
-
|
|
3308
|
-
// Code blocks
|
|
3309
|
-
|
|
3310
|
-
#handleCodeBlocks(event, selection) {
|
|
3311
|
-
if (!selection.isCollapsed()) return false
|
|
3312
|
-
|
|
3313
|
-
const codeNode = this.#getCodeNodeFromSelection(selection);
|
|
3314
|
-
if (!codeNode) return false
|
|
3315
|
-
|
|
3316
|
-
if (this.#isCursorOnEmptyLastLineOfCodeBlock(selection, codeNode)) {
|
|
3317
|
-
event?.preventDefault();
|
|
3318
|
-
this.#exitCodeBlock(codeNode);
|
|
3319
|
-
return true
|
|
3320
|
-
}
|
|
3321
|
-
|
|
3322
|
-
return false
|
|
3323
|
-
}
|
|
3324
|
-
|
|
3325
|
-
#handleArrowDownInCodeBlock(event) {
|
|
3326
|
-
const selection = $getSelection();
|
|
3327
|
-
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false
|
|
3328
|
-
|
|
3329
|
-
const codeNode = this.#getCodeNodeFromSelection(selection);
|
|
3330
|
-
if (!codeNode) return false
|
|
3331
|
-
|
|
3332
|
-
if (this.#isCursorOnLastLineOfCodeBlock(selection, codeNode) && !codeNode.getNextSibling()) {
|
|
3333
|
-
event?.preventDefault();
|
|
3334
|
-
const paragraph = $createParagraphNode();
|
|
3335
|
-
codeNode.insertAfter(paragraph);
|
|
3336
|
-
paragraph.selectStart();
|
|
3337
|
-
return true
|
|
3338
|
-
}
|
|
3339
|
-
|
|
3340
|
-
return false
|
|
3341
|
-
}
|
|
3342
|
-
|
|
3343
|
-
#getCodeNodeFromSelection(selection) {
|
|
3344
|
-
const anchorNode = selection.anchor.getNode();
|
|
3345
|
-
return $getNearestNodeOfType(anchorNode, CodeNode) || ($isCodeNode(anchorNode) ? anchorNode : null)
|
|
3346
|
-
}
|
|
3347
|
-
|
|
3348
|
-
#isCursorOnEmptyLastLineOfCodeBlock(selection, codeNode) {
|
|
3349
|
-
const children = codeNode.getChildren();
|
|
3350
|
-
if (children.length === 0) return true
|
|
3351
|
-
|
|
3352
|
-
const anchorNode = selection.anchor.getNode();
|
|
3353
|
-
const anchorOffset = selection.anchor.offset;
|
|
3354
|
-
|
|
3355
|
-
// Chromium: cursor on the CodeNode element after the last child (a line break)
|
|
3356
|
-
if ($isCodeNode(anchorNode) && anchorOffset === children.length) {
|
|
3357
|
-
return $isLineBreakNode(children[children.length - 1])
|
|
3358
|
-
}
|
|
3359
|
-
|
|
3360
|
-
// Firefox: cursor on an empty text node that follows a line break at the end
|
|
3361
|
-
if ($isTextNode(anchorNode) && anchorNode.getTextContentSize() === 0 && anchorOffset === 0) {
|
|
3362
|
-
const previousSibling = anchorNode.getPreviousSibling();
|
|
3363
|
-
return $isLineBreakNode(previousSibling) && anchorNode.getNextSibling() === null
|
|
3364
|
-
}
|
|
3365
|
-
|
|
3366
|
-
return false
|
|
3367
|
-
}
|
|
3368
|
-
|
|
3369
|
-
#isCursorOnLastLineOfCodeBlock(selection, codeNode) {
|
|
3370
|
-
const anchorNode = selection.anchor.getNode();
|
|
3371
|
-
const children = codeNode.getChildren();
|
|
3372
|
-
if (children.length === 0) return true
|
|
3373
|
-
|
|
3374
|
-
const lastChild = children[children.length - 1];
|
|
3375
|
-
|
|
3376
|
-
if ($isCodeNode(anchorNode) && selection.anchor.offset === children.length) return true
|
|
3377
|
-
if (anchorNode === lastChild) return true
|
|
3378
|
-
|
|
3379
|
-
const lastLineBreakIndex = children.findLastIndex(child => $isLineBreakNode(child));
|
|
3380
|
-
if (lastLineBreakIndex === -1) return true
|
|
3381
|
-
|
|
3382
|
-
const anchorIndex = children.indexOf(anchorNode);
|
|
3383
|
-
return anchorIndex > lastLineBreakIndex
|
|
3384
|
-
}
|
|
3385
|
-
|
|
3386
|
-
#exitCodeBlock(codeNode) {
|
|
3387
|
-
const children = codeNode.getChildren();
|
|
3388
|
-
const lastChild = children[children.length - 1];
|
|
3389
|
-
|
|
3390
|
-
if ($isTextNode(lastChild) && lastChild.getTextContentSize() === 0) {
|
|
3391
|
-
const previousSibling = lastChild.getPreviousSibling();
|
|
3392
|
-
lastChild.remove();
|
|
3393
|
-
if ($isLineBreakNode(previousSibling)) previousSibling.remove();
|
|
3394
|
-
} else if ($isLineBreakNode(lastChild)) {
|
|
3395
|
-
lastChild.remove();
|
|
3396
|
-
}
|
|
3397
|
-
|
|
3398
|
-
const paragraph = $createParagraphNode();
|
|
3399
|
-
codeNode.insertAfter(paragraph);
|
|
3400
|
-
paragraph.selectStart();
|
|
3401
|
-
}
|
|
3402
|
-
}
|
|
3403
|
-
|
|
3404
|
-
async function loadFileIntoImage(file, image) {
|
|
3405
|
-
return new Promise((resolve) => {
|
|
3406
|
-
const reader = new FileReader();
|
|
3407
|
-
|
|
3408
|
-
image.addEventListener("load", () => {
|
|
3409
|
-
resolve(image);
|
|
3410
|
-
});
|
|
3411
|
-
|
|
3412
|
-
reader.onload = (event) => {
|
|
3413
|
-
image.src = event.target.result || null;
|
|
3414
|
-
};
|
|
3415
|
-
|
|
3416
|
-
reader.readAsDataURL(file);
|
|
3417
|
-
})
|
|
3418
|
-
}
|
|
3419
|
-
|
|
3420
|
-
class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
3421
|
-
static getType() {
|
|
3422
|
-
return "action_text_attachment_upload"
|
|
3423
|
-
}
|
|
3424
|
-
|
|
3425
|
-
static clone(node) {
|
|
3426
|
-
return new ActionTextAttachmentUploadNode({ ...node }, node.__key)
|
|
3427
|
-
}
|
|
3428
|
-
|
|
3429
|
-
static importJSON(serializedNode) {
|
|
3430
|
-
return new ActionTextAttachmentUploadNode({ ...serializedNode })
|
|
3431
|
-
}
|
|
3432
|
-
|
|
3433
|
-
// Should never run since this is a transient node. Defined to remove console warning.
|
|
3434
|
-
static importDOM() {
|
|
3435
|
-
return null
|
|
3436
|
-
}
|
|
3587
|
+
return null
|
|
3588
|
+
}
|
|
3437
3589
|
|
|
3438
3590
|
constructor(node, key) {
|
|
3439
3591
|
const { file, uploadUrl, blobUrlTemplate, progress, width, height, uploadError } = node;
|
|
@@ -3622,14 +3774,11 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
3622
3774
|
const editorHasFocus = this.#editorHasFocus;
|
|
3623
3775
|
|
|
3624
3776
|
this.editor.update(() => {
|
|
3625
|
-
const shouldTransferNodeSelection = editorHasFocus && this.isSelected();
|
|
3626
|
-
|
|
3627
3777
|
const replacementNode = this.#toActionTextAttachmentNodeWith(blob);
|
|
3628
3778
|
this.replace(replacementNode);
|
|
3629
3779
|
|
|
3630
|
-
if (
|
|
3631
|
-
|
|
3632
|
-
$setSelection(nodeSelection);
|
|
3780
|
+
if (editorHasFocus && $isRootOrShadowRoot(replacementNode.getParent())) {
|
|
3781
|
+
replacementNode.selectNext();
|
|
3633
3782
|
}
|
|
3634
3783
|
}, { tag: this.#backgroundUpdateTags });
|
|
3635
3784
|
}
|
|
@@ -4002,7 +4151,6 @@ class Contents {
|
|
|
4002
4151
|
this.editorElement = editorElement;
|
|
4003
4152
|
this.editor = editorElement.editor;
|
|
4004
4153
|
|
|
4005
|
-
new FormatEscaper(editorElement).monitor();
|
|
4006
4154
|
}
|
|
4007
4155
|
|
|
4008
4156
|
insertHtml(html, { tag } = {}) {
|
|
@@ -4011,6 +4159,7 @@ class Contents {
|
|
|
4011
4159
|
|
|
4012
4160
|
insertDOM(doc, { tag } = {}) {
|
|
4013
4161
|
this.#unwrapPlaceholderAnchors(doc);
|
|
4162
|
+
if (tag === PASTE_TAG) this.#stripTableCellColorStyles(doc);
|
|
4014
4163
|
|
|
4015
4164
|
this.editor.update(() => {
|
|
4016
4165
|
const selection = $getSelection();
|
|
@@ -4048,127 +4197,77 @@ class Contents {
|
|
|
4048
4197
|
this.#insertLineBelowIfLastNode(node);
|
|
4049
4198
|
}
|
|
4050
4199
|
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
if (!$isRangeSelection(selection)) return
|
|
4055
|
-
|
|
4056
|
-
const selectedNodes = selection.extract();
|
|
4057
|
-
|
|
4058
|
-
selectedNodes.forEach((node) => {
|
|
4059
|
-
const parent = node.getParent();
|
|
4060
|
-
if (!parent) { return }
|
|
4061
|
-
|
|
4062
|
-
const topLevelElement = node.getTopLevelElementOrThrow();
|
|
4063
|
-
const wrappingNode = newNodeFn();
|
|
4064
|
-
wrappingNode.append(...topLevelElement.getChildren());
|
|
4065
|
-
topLevelElement.replace(wrappingNode);
|
|
4066
|
-
});
|
|
4067
|
-
});
|
|
4068
|
-
}
|
|
4069
|
-
|
|
4070
|
-
toggleNodeWrappingAllSelectedLines(isFormatAppliedFn, newNodeFn) {
|
|
4071
|
-
this.editor.update(() => {
|
|
4072
|
-
const selection = $getSelection();
|
|
4073
|
-
if (!$isRangeSelection(selection)) return
|
|
4074
|
-
|
|
4075
|
-
const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow();
|
|
4076
|
-
|
|
4077
|
-
// Check if format is already applied
|
|
4078
|
-
if (isFormatAppliedFn(topLevelElement)) {
|
|
4079
|
-
this.removeFormattingFromSelectedLines();
|
|
4080
|
-
} else {
|
|
4081
|
-
this.#insertNodeWrappingAllSelectedLines(newNodeFn);
|
|
4082
|
-
}
|
|
4083
|
-
});
|
|
4084
|
-
}
|
|
4085
|
-
|
|
4086
|
-
toggleNodeWrappingAllSelectedNodes(isFormatAppliedFn, newNodeFn) {
|
|
4087
|
-
this.editor.update(() => {
|
|
4088
|
-
const selection = $getSelection();
|
|
4089
|
-
if (!$isRangeSelection(selection)) return
|
|
4090
|
-
|
|
4091
|
-
const topLevelElement = selection.anchor.getNode().getTopLevelElement();
|
|
4092
|
-
|
|
4093
|
-
// Check if format is already applied
|
|
4094
|
-
if (topLevelElement && isFormatAppliedFn(topLevelElement)) {
|
|
4095
|
-
this.#unwrap(topLevelElement);
|
|
4096
|
-
} else {
|
|
4097
|
-
this.#insertNodeWrappingAllSelectedNodes(newNodeFn);
|
|
4098
|
-
}
|
|
4099
|
-
});
|
|
4100
|
-
}
|
|
4101
|
-
|
|
4102
|
-
removeFormattingFromSelectedLines() {
|
|
4103
|
-
this.editor.update(() => {
|
|
4104
|
-
const selection = $getSelection();
|
|
4105
|
-
if (!$isRangeSelection(selection)) return
|
|
4200
|
+
applyParagraphFormat() {
|
|
4201
|
+
const selection = $getSelection();
|
|
4202
|
+
if (!$isRangeSelection(selection)) return
|
|
4106
4203
|
|
|
4107
|
-
|
|
4108
|
-
const paragraph = $createParagraphNode();
|
|
4109
|
-
paragraph.append(...topLevelElement.getChildren());
|
|
4110
|
-
topLevelElement.replace(paragraph);
|
|
4111
|
-
});
|
|
4204
|
+
$setBlocksType(selection, () => $createParagraphNode());
|
|
4112
4205
|
}
|
|
4113
4206
|
|
|
4114
|
-
|
|
4115
|
-
|
|
4207
|
+
applyHeadingFormat(tag) {
|
|
4208
|
+
const selection = $getSelection();
|
|
4209
|
+
if (!$isRangeSelection(selection)) return
|
|
4116
4210
|
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
result = $isRangeSelection(selection) && !selection.isCollapsed();
|
|
4120
|
-
});
|
|
4211
|
+
$setBlocksType(selection, () => $createHeadingNode(tag));
|
|
4212
|
+
}
|
|
4121
4213
|
|
|
4122
|
-
|
|
4214
|
+
#applyCodeBlockFormat() {
|
|
4215
|
+
const selection = $getSelection();
|
|
4216
|
+
if (!$isRangeSelection(selection)) return
|
|
4217
|
+
|
|
4218
|
+
$setBlocksType(selection, () => $createCodeNode("plain"));
|
|
4123
4219
|
}
|
|
4124
4220
|
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4221
|
+
toggleCodeBlock() {
|
|
4222
|
+
const selection = $getSelection();
|
|
4223
|
+
if (!$isRangeSelection(selection)) return
|
|
4128
4224
|
|
|
4129
|
-
this
|
|
4130
|
-
const selection = $getSelection();
|
|
4131
|
-
if (!$isRangeSelection(selection) || selection.isCollapsed()) return
|
|
4225
|
+
if (this.#insertNodeIfRoot($createCodeNode("plain"))) return
|
|
4132
4226
|
|
|
4133
|
-
|
|
4134
|
-
|
|
4227
|
+
const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow();
|
|
4228
|
+
|
|
4229
|
+
if (topLevelElement && !$isCodeNode(topLevelElement)) {
|
|
4230
|
+
this.#applyCodeBlockFormat();
|
|
4231
|
+
} else {
|
|
4232
|
+
this.applyParagraphFormat();
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4135
4235
|
|
|
4136
|
-
|
|
4137
|
-
|
|
4236
|
+
toggleBlockquote() {
|
|
4237
|
+
const selection = $getSelection();
|
|
4238
|
+
if (!$isRangeSelection(selection)) return
|
|
4138
4239
|
|
|
4139
|
-
|
|
4240
|
+
if (this.#insertNodeIfRoot($createQuoteNode())) return
|
|
4140
4241
|
|
|
4141
|
-
|
|
4142
|
-
if (start === 0 && end === lines.length - 1) return
|
|
4242
|
+
const topLevelElements = this.#topLevelElementsInSelection(selection);
|
|
4143
4243
|
|
|
4144
|
-
|
|
4145
|
-
});
|
|
4244
|
+
const allQuoted = topLevelElements.length > 0 && topLevelElements.every($isQuoteNode);
|
|
4146
4245
|
|
|
4147
|
-
if (
|
|
4246
|
+
if (allQuoted) {
|
|
4247
|
+
topLevelElements.forEach(node => this.#unwrap(node));
|
|
4248
|
+
} else {
|
|
4249
|
+
topLevelElements.filter($isQuoteNode).forEach(node => this.#unwrap(node));
|
|
4148
4250
|
|
|
4149
|
-
|
|
4150
|
-
const paragraph = $getNodeByKey(paragraphKey);
|
|
4151
|
-
if (!paragraph || !$isParagraphNode(paragraph)) return
|
|
4251
|
+
this.#splitParagraphsAtLineBreaks(selection);
|
|
4152
4252
|
|
|
4153
|
-
const
|
|
4154
|
-
|
|
4155
|
-
});
|
|
4253
|
+
const elements = this.#topLevelElementsInSelection(selection);
|
|
4254
|
+
if (elements.length === 0) return
|
|
4156
4255
|
|
|
4157
|
-
|
|
4256
|
+
const blockquote = $createQuoteNode();
|
|
4257
|
+
elements[0].insertBefore(blockquote);
|
|
4258
|
+
elements.forEach((element) => blockquote.append(element));
|
|
4259
|
+
}
|
|
4158
4260
|
}
|
|
4159
4261
|
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
const selection = $getSelection();
|
|
4163
|
-
if (!$isRangeSelection(selection)) return
|
|
4262
|
+
hasSelectedText() {
|
|
4263
|
+
let result = false;
|
|
4164
4264
|
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
this.#removeEmptyParentLists(parentLists);
|
|
4169
|
-
this.#selectNewParagraphs(newParagraphs);
|
|
4170
|
-
}
|
|
4265
|
+
this.editor.read(() => {
|
|
4266
|
+
const selection = $getSelection();
|
|
4267
|
+
result = $isRangeSelection(selection) && !selection.isCollapsed();
|
|
4171
4268
|
});
|
|
4269
|
+
|
|
4270
|
+
return result
|
|
4172
4271
|
}
|
|
4173
4272
|
|
|
4174
4273
|
createLink(url) {
|
|
@@ -4250,503 +4349,197 @@ class Contents {
|
|
|
4250
4349
|
replaceTextBackUntil(stringToReplace, replacementNodes) {
|
|
4251
4350
|
replacementNodes = Array.isArray(replacementNodes) ? replacementNodes : [ replacementNodes ];
|
|
4252
4351
|
|
|
4253
|
-
const selection = $getSelection();
|
|
4254
|
-
const { anchorNode, offset } = this.#getTextAnchorData();
|
|
4255
|
-
if (!anchorNode) return
|
|
4256
|
-
|
|
4257
|
-
const lastIndex = this.#findLastIndexBeforeCursor(anchorNode, offset, stringToReplace);
|
|
4258
|
-
if (lastIndex === -1) return
|
|
4259
|
-
|
|
4260
|
-
this.#performTextReplacement(anchorNode, selection, offset, lastIndex, replacementNodes);
|
|
4261
|
-
}
|
|
4262
|
-
|
|
4263
|
-
createParagraphAfterNode(node, text) {
|
|
4264
|
-
const newParagraph = $createParagraphNode();
|
|
4265
|
-
node.insertAfter(newParagraph);
|
|
4266
|
-
newParagraph.selectStart();
|
|
4267
|
-
|
|
4268
|
-
// Insert the typed text
|
|
4269
|
-
if (text) {
|
|
4270
|
-
newParagraph.append($createTextNode(text));
|
|
4271
|
-
newParagraph.select(1, 1); // Place cursor after the text
|
|
4272
|
-
}
|
|
4273
|
-
}
|
|
4274
|
-
|
|
4275
|
-
createParagraphBeforeNode(node, text) {
|
|
4276
|
-
const newParagraph = $createParagraphNode();
|
|
4277
|
-
node.insertBefore(newParagraph);
|
|
4278
|
-
newParagraph.selectStart();
|
|
4279
|
-
|
|
4280
|
-
// Insert the typed text
|
|
4281
|
-
if (text) {
|
|
4282
|
-
newParagraph.append($createTextNode(text));
|
|
4283
|
-
newParagraph.select(1, 1); // Place cursor after the text
|
|
4284
|
-
}
|
|
4285
|
-
}
|
|
4286
|
-
|
|
4287
|
-
uploadFiles(files, { selectLast } = {}) {
|
|
4288
|
-
if (!this.editorElement.supportsAttachments) {
|
|
4289
|
-
console.warn("This editor does not supports attachments (it's configured with [attachments=false])");
|
|
4290
|
-
return
|
|
4291
|
-
}
|
|
4292
|
-
const validFiles = Array.from(files).filter(this.#shouldUploadFile.bind(this));
|
|
4293
|
-
|
|
4294
|
-
this.editor.update(() => {
|
|
4295
|
-
const uploader = Uploader.for(this.editorElement, validFiles);
|
|
4296
|
-
uploader.$uploadFiles();
|
|
4297
|
-
|
|
4298
|
-
if (selectLast && uploader.nodes?.length) {
|
|
4299
|
-
const lastNode = uploader.nodes.at(-1);
|
|
4300
|
-
lastNode.selectEnd();
|
|
4301
|
-
this.#normalizeSelectionInShadowRoot();
|
|
4302
|
-
}
|
|
4303
|
-
});
|
|
4304
|
-
}
|
|
4305
|
-
|
|
4306
|
-
replaceNodeWithHTML(nodeKey, html, options = {}) {
|
|
4307
|
-
this.editor.update(() => {
|
|
4308
|
-
const node = $getNodeByKey(nodeKey);
|
|
4309
|
-
if (!node) return
|
|
4310
|
-
|
|
4311
|
-
const selection = $getSelection();
|
|
4312
|
-
let wasSelected = false;
|
|
4313
|
-
|
|
4314
|
-
if ($isRangeSelection(selection)) {
|
|
4315
|
-
const selectedNodes = selection.getNodes();
|
|
4316
|
-
wasSelected = selectedNodes.includes(node) || selectedNodes.some(n => n.getParent() === node);
|
|
4317
|
-
|
|
4318
|
-
if (wasSelected) {
|
|
4319
|
-
$setSelection(null);
|
|
4320
|
-
}
|
|
4321
|
-
}
|
|
4322
|
-
|
|
4323
|
-
const replacementNode = options.attachment ? this.#createCustomAttachmentNodeWithHtml(html, options.attachment) : this.#createHtmlNodeWith(html);
|
|
4324
|
-
node.replace(replacementNode);
|
|
4325
|
-
|
|
4326
|
-
if (wasSelected) {
|
|
4327
|
-
replacementNode.selectEnd();
|
|
4328
|
-
}
|
|
4329
|
-
});
|
|
4330
|
-
}
|
|
4331
|
-
|
|
4332
|
-
insertHTMLBelowNode(nodeKey, html, options = {}) {
|
|
4333
|
-
this.editor.update(() => {
|
|
4334
|
-
const node = $getNodeByKey(nodeKey);
|
|
4335
|
-
if (!node) return
|
|
4336
|
-
|
|
4337
|
-
const previousNode = node.getTopLevelElement() || node;
|
|
4338
|
-
|
|
4339
|
-
const newNode = options.attachment ? this.#createCustomAttachmentNodeWithHtml(html, options.attachment) : this.#createHtmlNodeWith(html);
|
|
4340
|
-
previousNode.insertAfter(newNode);
|
|
4341
|
-
});
|
|
4342
|
-
}
|
|
4343
|
-
|
|
4344
|
-
#insertUploadNodes(nodes) {
|
|
4345
|
-
if (nodes.every($isActionTextAttachmentNode)) {
|
|
4346
|
-
const uploader = Uploader.for(this.editorElement, []);
|
|
4347
|
-
uploader.nodes = nodes;
|
|
4348
|
-
uploader.$insertUploadNodes();
|
|
4349
|
-
return true
|
|
4350
|
-
}
|
|
4351
|
-
}
|
|
4352
|
-
|
|
4353
|
-
#insertLineBelowIfLastNode(node) {
|
|
4354
|
-
this.editor.update(() => {
|
|
4355
|
-
const nextSibling = node.getNextSibling();
|
|
4356
|
-
if (!nextSibling) {
|
|
4357
|
-
const newParagraph = $createParagraphNode();
|
|
4358
|
-
node.insertAfter(newParagraph);
|
|
4359
|
-
newParagraph.selectStart();
|
|
4360
|
-
}
|
|
4361
|
-
});
|
|
4362
|
-
}
|
|
4363
|
-
|
|
4364
|
-
#unwrap(node) {
|
|
4365
|
-
const children = node.getChildren();
|
|
4366
|
-
|
|
4367
|
-
if (children.length == 0) {
|
|
4368
|
-
node.insertBefore($createParagraphNode());
|
|
4369
|
-
} else {
|
|
4370
|
-
children.forEach((child) => {
|
|
4371
|
-
if ($isTextNode(child) && child.getTextContent().trim() !== "") {
|
|
4372
|
-
const newParagraph = $createParagraphNode();
|
|
4373
|
-
newParagraph.append(child);
|
|
4374
|
-
node.insertBefore(newParagraph);
|
|
4375
|
-
} else if (!$isLineBreakNode(child)) {
|
|
4376
|
-
node.insertBefore(child);
|
|
4377
|
-
}
|
|
4378
|
-
});
|
|
4379
|
-
}
|
|
4380
|
-
|
|
4381
|
-
node.remove();
|
|
4382
|
-
}
|
|
4383
|
-
|
|
4384
|
-
// Anchors with non-meaningful hrefs (e.g. "#", "") appear in content copied
|
|
4385
|
-
// from rendered views where mentions and interactive elements are wrapped in
|
|
4386
|
-
// <a href="#"> tags. Unwrap them so their text content pastes as plain text
|
|
4387
|
-
// and real links are preserved.
|
|
4388
|
-
#unwrapPlaceholderAnchors(doc) {
|
|
4389
|
-
for (const anchor of doc.querySelectorAll("a")) {
|
|
4390
|
-
const href = anchor.getAttribute("href") || "";
|
|
4391
|
-
if (href === "" || href === "#") {
|
|
4392
|
-
anchor.replaceWith(...anchor.childNodes);
|
|
4393
|
-
}
|
|
4394
|
-
}
|
|
4395
|
-
}
|
|
4396
|
-
|
|
4397
|
-
#insertNodeWrappingAllSelectedNodes(newNodeFn) {
|
|
4398
|
-
this.editor.update(() => {
|
|
4399
|
-
const selection = $getSelection();
|
|
4400
|
-
if (!$isRangeSelection(selection)) return
|
|
4401
|
-
|
|
4402
|
-
const selectedNodes = selection.extract();
|
|
4403
|
-
if (selectedNodes.length === 0) {
|
|
4404
|
-
return
|
|
4405
|
-
}
|
|
4406
|
-
|
|
4407
|
-
const topLevelElements = new Set();
|
|
4408
|
-
selectedNodes.forEach((node) => {
|
|
4409
|
-
const topLevel = node.getTopLevelElementOrThrow();
|
|
4410
|
-
topLevelElements.add(topLevel);
|
|
4411
|
-
});
|
|
4412
|
-
|
|
4413
|
-
const elements = this.#withoutTrailingEmptyParagraphs(Array.from(topLevelElements));
|
|
4414
|
-
if (elements.length === 0) {
|
|
4415
|
-
this.#removeStandaloneEmptyParagraph();
|
|
4416
|
-
this.insertAtCursor(newNodeFn());
|
|
4417
|
-
return
|
|
4418
|
-
}
|
|
4419
|
-
|
|
4420
|
-
const wrappingNode = newNodeFn();
|
|
4421
|
-
elements[0].insertBefore(wrappingNode);
|
|
4422
|
-
elements.forEach((element) => {
|
|
4423
|
-
wrappingNode.append(element);
|
|
4424
|
-
});
|
|
4425
|
-
});
|
|
4426
|
-
}
|
|
4427
|
-
|
|
4428
|
-
#withoutTrailingEmptyParagraphs(elements) {
|
|
4429
|
-
let lastNonEmptyIndex = elements.length - 1;
|
|
4430
|
-
|
|
4431
|
-
// Find the last non-empty paragraph
|
|
4432
|
-
while (lastNonEmptyIndex >= 0) {
|
|
4433
|
-
const element = elements[lastNonEmptyIndex];
|
|
4434
|
-
if (!$isParagraphNode(element) || !this.#isElementEmpty(element)) {
|
|
4435
|
-
break
|
|
4436
|
-
}
|
|
4437
|
-
lastNonEmptyIndex--;
|
|
4438
|
-
}
|
|
4439
|
-
|
|
4440
|
-
return elements.slice(0, lastNonEmptyIndex + 1)
|
|
4441
|
-
}
|
|
4442
|
-
|
|
4443
|
-
#isElementEmpty(element) {
|
|
4444
|
-
// Check text content first
|
|
4445
|
-
if (element.getTextContent().trim() !== "") return false
|
|
4446
|
-
|
|
4447
|
-
// Check if it only contains line breaks
|
|
4448
|
-
const children = element.getChildren();
|
|
4449
|
-
return children.length === 0 || children.every(child => $isLineBreakNode(child))
|
|
4450
|
-
}
|
|
4451
|
-
|
|
4452
|
-
#removeStandaloneEmptyParagraph() {
|
|
4453
|
-
const root = $getRoot();
|
|
4454
|
-
if (root.getChildrenSize() === 1) {
|
|
4455
|
-
const firstChild = root.getFirstChild();
|
|
4456
|
-
if (firstChild && $isParagraphNode(firstChild) && this.#isElementEmpty(firstChild)) {
|
|
4457
|
-
firstChild.remove();
|
|
4458
|
-
}
|
|
4459
|
-
}
|
|
4460
|
-
}
|
|
4461
|
-
|
|
4462
|
-
#insertNodeWrappingAllSelectedLines(newNodeFn) {
|
|
4463
|
-
this.editor.update(() => {
|
|
4464
|
-
const selection = $getSelection();
|
|
4465
|
-
if (!$isRangeSelection(selection)) return
|
|
4466
|
-
|
|
4467
|
-
if (selection.isCollapsed()) {
|
|
4468
|
-
this.#wrapCurrentLine(selection, newNodeFn);
|
|
4469
|
-
} else {
|
|
4470
|
-
this.#wrapMultipleSelectedLines(selection, newNodeFn);
|
|
4471
|
-
}
|
|
4472
|
-
});
|
|
4473
|
-
}
|
|
4474
|
-
|
|
4475
|
-
#wrapCurrentLine(selection, newNodeFn) {
|
|
4476
|
-
const anchorNode = selection.anchor.getNode();
|
|
4477
|
-
|
|
4478
|
-
const topLevelElement = anchorNode.getTopLevelElementOrThrow();
|
|
4479
|
-
|
|
4480
|
-
if (topLevelElement.getTextContent()) {
|
|
4481
|
-
const wrappingNode = newNodeFn();
|
|
4482
|
-
wrappingNode.append(...topLevelElement.getChildren());
|
|
4483
|
-
topLevelElement.replace(wrappingNode);
|
|
4484
|
-
} else {
|
|
4485
|
-
selection.insertNodes([ newNodeFn() ]);
|
|
4486
|
-
}
|
|
4487
|
-
}
|
|
4488
|
-
|
|
4489
|
-
#wrapMultipleSelectedLines(selection, newNodeFn) {
|
|
4490
|
-
const selectedParagraphs = this.#extractSelectedParagraphs(selection);
|
|
4491
|
-
if (selectedParagraphs.length === 0) return
|
|
4492
|
-
|
|
4493
|
-
const { lineSet, nodesToDelete } = this.#extractUniqueLines(selectedParagraphs);
|
|
4494
|
-
if (lineSet.size === 0) return
|
|
4495
|
-
|
|
4496
|
-
const wrappingNode = this.#createWrappingNodeWithLines(newNodeFn, lineSet);
|
|
4497
|
-
this.#replaceWithWrappingNode(selection, wrappingNode);
|
|
4498
|
-
this.#removeNodes(nodesToDelete);
|
|
4499
|
-
}
|
|
4500
|
-
|
|
4501
|
-
#extractSelectedParagraphs(selection) {
|
|
4502
|
-
const selectedNodes = selection.extract();
|
|
4503
|
-
const selectedParagraphs = selectedNodes
|
|
4504
|
-
.map((node) => this.#getParagraphFromNode(node))
|
|
4505
|
-
.filter(Boolean);
|
|
4506
|
-
|
|
4507
|
-
$setSelection(null);
|
|
4508
|
-
return selectedParagraphs
|
|
4509
|
-
}
|
|
4510
|
-
|
|
4511
|
-
#getParagraphFromNode(node) {
|
|
4512
|
-
if ($isParagraphNode(node)) return node
|
|
4513
|
-
if ($isTextNode(node) && node.getParent() && $isParagraphNode(node.getParent())) {
|
|
4514
|
-
return node.getParent()
|
|
4515
|
-
}
|
|
4516
|
-
return null
|
|
4517
|
-
}
|
|
4518
|
-
|
|
4519
|
-
#extractUniqueLines(selectedParagraphs) {
|
|
4520
|
-
const lineSet = new Set();
|
|
4521
|
-
const nodesToDelete = new Set();
|
|
4522
|
-
|
|
4523
|
-
selectedParagraphs.forEach((paragraphNode) => {
|
|
4524
|
-
const textContent = paragraphNode.getTextContent();
|
|
4525
|
-
if (textContent) {
|
|
4526
|
-
textContent.split("\n").forEach((line) => {
|
|
4527
|
-
if (line.trim()) lineSet.add(line);
|
|
4528
|
-
});
|
|
4529
|
-
}
|
|
4530
|
-
nodesToDelete.add(paragraphNode);
|
|
4531
|
-
});
|
|
4532
|
-
|
|
4533
|
-
return { lineSet, nodesToDelete }
|
|
4534
|
-
}
|
|
4535
|
-
|
|
4536
|
-
#createWrappingNodeWithLines(newNodeFn, lineSet) {
|
|
4537
|
-
const wrappingNode = newNodeFn();
|
|
4538
|
-
const lines = Array.from(lineSet);
|
|
4539
|
-
|
|
4540
|
-
lines.forEach((lineText, index) => {
|
|
4541
|
-
wrappingNode.append($createTextNode(lineText));
|
|
4542
|
-
if (index < lines.length - 1) {
|
|
4543
|
-
wrappingNode.append($createLineBreakNode());
|
|
4544
|
-
}
|
|
4545
|
-
});
|
|
4546
|
-
|
|
4547
|
-
return wrappingNode
|
|
4548
|
-
}
|
|
4549
|
-
|
|
4550
|
-
#replaceWithWrappingNode(selection, wrappingNode) {
|
|
4551
|
-
const anchorNode = selection.anchor.getNode();
|
|
4552
|
-
const parent = anchorNode.getParent();
|
|
4553
|
-
if (parent) {
|
|
4554
|
-
parent.replace(wrappingNode);
|
|
4555
|
-
}
|
|
4556
|
-
}
|
|
4557
|
-
|
|
4558
|
-
#removeNodes(nodesToDelete) {
|
|
4559
|
-
nodesToDelete.forEach((node) => node.remove());
|
|
4560
|
-
}
|
|
4561
|
-
|
|
4562
|
-
#getSelectedParagraphWithSoftLineBreaks(selection) {
|
|
4563
|
-
const anchorParagraph = this.#getParagraphFromNode(selection.anchor.getNode());
|
|
4564
|
-
const focusParagraph = this.#getParagraphFromNode(selection.focus.getNode());
|
|
4352
|
+
const selection = $getSelection();
|
|
4353
|
+
const { anchorNode, offset } = this.#getTextAnchorData();
|
|
4354
|
+
if (!anchorNode) return
|
|
4565
4355
|
|
|
4566
|
-
|
|
4567
|
-
if (
|
|
4356
|
+
const lastIndex = this.#findLastIndexBeforeCursor(anchorNode, offset, stringToReplace);
|
|
4357
|
+
if (lastIndex === -1) return
|
|
4568
4358
|
|
|
4569
|
-
|
|
4359
|
+
this.#performTextReplacement(anchorNode, selection, offset, lastIndex, replacementNodes);
|
|
4570
4360
|
}
|
|
4571
4361
|
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4362
|
+
uploadFiles(files, { selectLast } = {}) {
|
|
4363
|
+
if (!this.editorElement.supportsAttachments) {
|
|
4364
|
+
console.warn("This editor does not supports attachments (it's configured with [attachments=false])");
|
|
4365
|
+
return
|
|
4366
|
+
}
|
|
4367
|
+
const validFiles = Array.from(files).filter(this.#shouldUploadFile.bind(this));
|
|
4575
4368
|
|
|
4576
|
-
|
|
4577
|
-
|
|
4369
|
+
this.editor.update(() => {
|
|
4370
|
+
const uploader = Uploader.for(this.editorElement, validFiles);
|
|
4371
|
+
uploader.$uploadFiles();
|
|
4578
4372
|
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
lines[lines.length - 1].push(child);
|
|
4373
|
+
if (selectLast && uploader.nodes?.length) {
|
|
4374
|
+
const lastNode = uploader.nodes.at(-1);
|
|
4375
|
+
lastNode.selectEnd();
|
|
4376
|
+
this.#normalizeSelectionInShadowRoot();
|
|
4584
4377
|
}
|
|
4585
4378
|
});
|
|
4586
|
-
|
|
4587
|
-
return lines
|
|
4588
4379
|
}
|
|
4589
4380
|
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
selectedNodeKeys.add(selection.anchor.getNode().getKey());
|
|
4596
|
-
selectedNodeKeys.add(selection.focus.getNode().getKey());
|
|
4597
|
-
|
|
4598
|
-
const selectedLineIndexes = lines
|
|
4599
|
-
.map((lineNodes, index) => {
|
|
4600
|
-
return lineNodes.some((node) => selectedNodeKeys.has(node.getKey())) ? index : null
|
|
4601
|
-
})
|
|
4602
|
-
.filter((index) => index !== null);
|
|
4381
|
+
replaceNodeWithHTML(nodeKey, html, options = {}) {
|
|
4382
|
+
this.editor.update(() => {
|
|
4383
|
+
const node = $getNodeByKey(nodeKey);
|
|
4384
|
+
if (!node) return
|
|
4603
4385
|
|
|
4604
|
-
|
|
4386
|
+
const selection = $getSelection();
|
|
4387
|
+
let wasSelected = false;
|
|
4605
4388
|
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
}
|
|
4610
|
-
}
|
|
4389
|
+
if ($isRangeSelection(selection)) {
|
|
4390
|
+
const selectedNodes = selection.getNodes();
|
|
4391
|
+
wasSelected = selectedNodes.includes(node) || selectedNodes.some(n => n.getParent() === node);
|
|
4611
4392
|
|
|
4612
|
-
|
|
4613
|
-
|
|
4393
|
+
if (wasSelected) {
|
|
4394
|
+
$setSelection(null);
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4614
4397
|
|
|
4615
|
-
|
|
4398
|
+
const replacementNode = options.attachment ? this.#createCustomAttachmentNodeWithHtml(html, options.attachment) : this.#createHtmlNodeWith(html);
|
|
4399
|
+
node.replace(replacementNode);
|
|
4616
4400
|
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4401
|
+
if (wasSelected) {
|
|
4402
|
+
replacementNode.selectEnd();
|
|
4403
|
+
}
|
|
4620
4404
|
});
|
|
4621
|
-
|
|
4405
|
+
}
|
|
4622
4406
|
|
|
4623
|
-
|
|
4407
|
+
insertHTMLBelowNode(nodeKey, html, options = {}) {
|
|
4408
|
+
this.editor.update(() => {
|
|
4409
|
+
const node = $getNodeByKey(nodeKey);
|
|
4410
|
+
if (!node) return
|
|
4624
4411
|
|
|
4625
|
-
|
|
4626
|
-
insertedNodes.forEach((node) => {
|
|
4627
|
-
if (previousNode) {
|
|
4628
|
-
previousNode.insertAfter(node);
|
|
4629
|
-
} else {
|
|
4630
|
-
paragraph.insertBefore(node);
|
|
4631
|
-
}
|
|
4412
|
+
const previousNode = node.getTopLevelElement() || node;
|
|
4632
4413
|
|
|
4633
|
-
|
|
4414
|
+
const newNode = options.attachment ? this.#createCustomAttachmentNodeWithHtml(html, options.attachment) : this.#createHtmlNodeWith(html);
|
|
4415
|
+
previousNode.insertAfter(newNode);
|
|
4634
4416
|
});
|
|
4635
|
-
|
|
4636
|
-
paragraph.remove();
|
|
4637
4417
|
}
|
|
4638
4418
|
|
|
4639
|
-
#
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
});
|
|
4643
|
-
}
|
|
4419
|
+
#insertNodeIfRoot(node) {
|
|
4420
|
+
const selection = $getSelection();
|
|
4421
|
+
if (!$isRangeSelection(selection)) return false
|
|
4644
4422
|
|
|
4645
|
-
|
|
4646
|
-
|
|
4423
|
+
const anchorNode = selection.anchor.getNode();
|
|
4424
|
+
if ($isRootOrShadowRoot(anchorNode)) {
|
|
4425
|
+
anchorNode.append(node);
|
|
4426
|
+
node.selectEnd();
|
|
4647
4427
|
|
|
4648
|
-
|
|
4649
|
-
paragraph.append($createLineBreakNode());
|
|
4650
|
-
} else {
|
|
4651
|
-
paragraph.append(...lineNodes);
|
|
4428
|
+
return true
|
|
4652
4429
|
}
|
|
4653
4430
|
|
|
4654
|
-
return
|
|
4431
|
+
return false
|
|
4655
4432
|
}
|
|
4656
4433
|
|
|
4657
|
-
#
|
|
4658
|
-
const
|
|
4659
|
-
const
|
|
4660
|
-
const
|
|
4434
|
+
#splitParagraphsAtLineBreaks(selection) {
|
|
4435
|
+
const anchorKey = selection.anchor.getNode().getKey();
|
|
4436
|
+
const focusKey = selection.focus.getNode().getKey();
|
|
4437
|
+
const topLevelElements = this.#topLevelElementsInSelection(selection);
|
|
4661
4438
|
|
|
4662
|
-
for (const
|
|
4663
|
-
|
|
4664
|
-
if (listItem) {
|
|
4665
|
-
listItems.add(listItem);
|
|
4666
|
-
const parentList = listItem.getParent();
|
|
4667
|
-
if (parentList && $isListNode(parentList)) {
|
|
4668
|
-
parentLists.add(parentList);
|
|
4669
|
-
}
|
|
4670
|
-
}
|
|
4671
|
-
}
|
|
4439
|
+
for (const element of topLevelElements) {
|
|
4440
|
+
if (!$isParagraphNode(element)) continue
|
|
4672
4441
|
|
|
4673
|
-
|
|
4674
|
-
|
|
4442
|
+
const children = element.getChildren();
|
|
4443
|
+
if (!children.some($isLineBreakNode)) continue
|
|
4675
4444
|
|
|
4676
|
-
|
|
4677
|
-
|
|
4445
|
+
// Check whether this paragraph needs splitting: skip only if neither
|
|
4446
|
+
// selection endpoint is inside it (meaning it's a middle paragraph
|
|
4447
|
+
// fully between anchor and focus with no partial lines to split off).
|
|
4448
|
+
const hasEndpoint = children.some(child =>
|
|
4449
|
+
child.getKey() === anchorKey || child.getKey() === focusKey
|
|
4450
|
+
);
|
|
4451
|
+
if (!hasEndpoint) continue
|
|
4678
4452
|
|
|
4679
|
-
|
|
4680
|
-
const
|
|
4681
|
-
|
|
4682
|
-
|
|
4453
|
+
const groups = [ [] ];
|
|
4454
|
+
for (const child of children) {
|
|
4455
|
+
if ($isLineBreakNode(child)) {
|
|
4456
|
+
groups.push([]);
|
|
4457
|
+
child.remove();
|
|
4458
|
+
} else {
|
|
4459
|
+
groups[groups.length - 1].push(child);
|
|
4460
|
+
}
|
|
4683
4461
|
}
|
|
4684
|
-
}
|
|
4685
4462
|
|
|
4686
|
-
|
|
4463
|
+
for (const group of groups) {
|
|
4464
|
+
if (group.length === 0) continue
|
|
4465
|
+
const paragraph = $createParagraphNode();
|
|
4466
|
+
group.forEach(child => paragraph.append(child));
|
|
4467
|
+
element.insertBefore(paragraph);
|
|
4468
|
+
}
|
|
4469
|
+
if (groups.some(group => group.length > 0)) element.remove();
|
|
4470
|
+
}
|
|
4687
4471
|
}
|
|
4688
4472
|
|
|
4689
|
-
#
|
|
4690
|
-
const
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
listItem.insertAfter(paragraph);
|
|
4697
|
-
this.#insertSublists(paragraph, sublists);
|
|
4698
|
-
listItem.remove();
|
|
4699
|
-
|
|
4700
|
-
return paragraph
|
|
4473
|
+
#topLevelElementsInSelection(selection) {
|
|
4474
|
+
const elements = new Set();
|
|
4475
|
+
for (const node of selection.getNodes()) {
|
|
4476
|
+
const topLevel = node.getTopLevelElement();
|
|
4477
|
+
if (topLevel) elements.add(topLevel);
|
|
4478
|
+
}
|
|
4479
|
+
return Array.from(elements)
|
|
4701
4480
|
}
|
|
4702
4481
|
|
|
4703
|
-
#
|
|
4704
|
-
|
|
4482
|
+
#insertUploadNodes(nodes) {
|
|
4483
|
+
if (nodes.every($isActionTextAttachmentNode)) {
|
|
4484
|
+
const uploader = Uploader.for(this.editorElement, []);
|
|
4485
|
+
uploader.nodes = nodes;
|
|
4486
|
+
uploader.$insertUploadNodes();
|
|
4487
|
+
return true
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4705
4490
|
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4491
|
+
#insertLineBelowIfLastNode(node) {
|
|
4492
|
+
this.editor.update(() => {
|
|
4493
|
+
const nextSibling = node.getNextSibling();
|
|
4494
|
+
if (!nextSibling) {
|
|
4495
|
+
const newParagraph = $createParagraphNode();
|
|
4496
|
+
node.insertAfter(newParagraph);
|
|
4497
|
+
newParagraph.selectStart();
|
|
4711
4498
|
}
|
|
4712
4499
|
});
|
|
4713
|
-
|
|
4714
|
-
return sublists
|
|
4715
4500
|
}
|
|
4716
4501
|
|
|
4717
|
-
#
|
|
4718
|
-
|
|
4719
|
-
paragraph.insertAfter(sublist);
|
|
4720
|
-
});
|
|
4721
|
-
}
|
|
4502
|
+
#unwrap(node) {
|
|
4503
|
+
const children = node.getChildren();
|
|
4722
4504
|
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4505
|
+
if (children.length == 0) {
|
|
4506
|
+
node.insertBefore($createParagraphNode());
|
|
4507
|
+
} else {
|
|
4508
|
+
children.forEach((child) => {
|
|
4509
|
+
if ($isTextNode(child) && child.getTextContent().trim() !== "") {
|
|
4510
|
+
const newParagraph = $createParagraphNode();
|
|
4511
|
+
newParagraph.append(child);
|
|
4512
|
+
node.insertBefore(newParagraph);
|
|
4513
|
+
} else if (!$isLineBreakNode(child)) {
|
|
4514
|
+
node.insertBefore(child);
|
|
4515
|
+
}
|
|
4516
|
+
});
|
|
4728
4517
|
}
|
|
4729
|
-
}
|
|
4730
4518
|
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
const firstParagraph = newParagraphs[0];
|
|
4735
|
-
const lastParagraph = newParagraphs[newParagraphs.length - 1];
|
|
4519
|
+
node.remove();
|
|
4520
|
+
}
|
|
4736
4521
|
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4522
|
+
// Anchors with non-meaningful hrefs (e.g. "#", "") appear in content copied
|
|
4523
|
+
// from rendered views where mentions and interactive elements are wrapped in
|
|
4524
|
+
// <a href="#"> tags. Unwrap them so their text content pastes as plain text
|
|
4525
|
+
// and real links are preserved.
|
|
4526
|
+
#unwrapPlaceholderAnchors(doc) {
|
|
4527
|
+
for (const anchor of doc.querySelectorAll("a")) {
|
|
4528
|
+
const href = anchor.getAttribute("href") || "";
|
|
4529
|
+
if (href === "" || href === "#") {
|
|
4530
|
+
anchor.replaceWith(...anchor.childNodes);
|
|
4531
|
+
}
|
|
4741
4532
|
}
|
|
4742
4533
|
}
|
|
4743
4534
|
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4535
|
+
// Table cells copied from a page inherit the source theme's inline color
|
|
4536
|
+
// styles (e.g. dark-mode backgrounds). Strip them so pasted tables adopt
|
|
4537
|
+
// the current theme instead of carrying stale colors.
|
|
4538
|
+
#stripTableCellColorStyles(doc) {
|
|
4539
|
+
for (const cell of doc.querySelectorAll("td, th")) {
|
|
4540
|
+
cell.style.removeProperty("background-color");
|
|
4541
|
+
cell.style.removeProperty("background");
|
|
4542
|
+
cell.style.removeProperty("color");
|
|
4750
4543
|
}
|
|
4751
4544
|
}
|
|
4752
4545
|
|
|
@@ -4773,9 +4566,8 @@ class Contents {
|
|
|
4773
4566
|
const textBeforeString = fullText.slice(0, lastIndex);
|
|
4774
4567
|
const textAfterCursor = fullText.slice(offset);
|
|
4775
4568
|
|
|
4776
|
-
const trailingSpacer = this.#hasInlineDecoratorNode(replacementNodes) ? "\u2060" : " ";
|
|
4777
4569
|
const textNodeBefore = this.#cloneTextNodeFormatting(anchorNode, selection, textBeforeString);
|
|
4778
|
-
const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, selection, textAfterCursor ||
|
|
4570
|
+
const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, selection, textAfterCursor || " ");
|
|
4779
4571
|
|
|
4780
4572
|
anchorNode.replace(textNodeBefore);
|
|
4781
4573
|
|
|
@@ -4787,10 +4579,6 @@ class Contents {
|
|
|
4787
4579
|
textNodeAfter.select(cursorOffset, cursorOffset);
|
|
4788
4580
|
}
|
|
4789
4581
|
|
|
4790
|
-
#hasInlineDecoratorNode(nodes) {
|
|
4791
|
-
return nodes.some(node => node instanceof CustomActionTextAttachmentNode && node.isInline())
|
|
4792
|
-
}
|
|
4793
|
-
|
|
4794
4582
|
#cloneTextNodeFormatting(anchorNode, selection, text) {
|
|
4795
4583
|
const parent = anchorNode.getParent();
|
|
4796
4584
|
const fallbackFormat = parent?.getTextFormat?.() || 0;
|
|
@@ -4887,7 +4675,9 @@ class Clipboard {
|
|
|
4887
4675
|
return true
|
|
4888
4676
|
}
|
|
4889
4677
|
|
|
4890
|
-
|
|
4678
|
+
const handled = this.#handlePastedFiles(clipboardData);
|
|
4679
|
+
if (handled) event.preventDefault();
|
|
4680
|
+
return handled
|
|
4891
4681
|
}
|
|
4892
4682
|
|
|
4893
4683
|
#isPlainTextOrURLPasted(clipboardData) {
|
|
@@ -4985,14 +4775,21 @@ class Clipboard {
|
|
|
4985
4775
|
return true
|
|
4986
4776
|
}
|
|
4987
4777
|
|
|
4988
|
-
if (html) {
|
|
4778
|
+
if (html && !this.#isLexicalClipboardData(clipboardData)) {
|
|
4989
4779
|
this.contents.insertHtml(html, { tag: PASTE_TAG });
|
|
4990
4780
|
return true
|
|
4991
4781
|
}
|
|
4992
4782
|
|
|
4993
|
-
|
|
4783
|
+
if (files.length) {
|
|
4784
|
+
this.#uploadFilesPreservingScroll(files);
|
|
4785
|
+
return true
|
|
4786
|
+
}
|
|
4994
4787
|
|
|
4995
|
-
return
|
|
4788
|
+
return false
|
|
4789
|
+
}
|
|
4790
|
+
|
|
4791
|
+
#isLexicalClipboardData(clipboardData) {
|
|
4792
|
+
return Array.from(clipboardData.types).includes("application/x-lexical-editor")
|
|
4996
4793
|
}
|
|
4997
4794
|
|
|
4998
4795
|
#isCopiedImageHTML(html) {
|
|
@@ -5164,7 +4961,27 @@ class ProvisionalParagraphNode extends ParagraphNode {
|
|
|
5164
4961
|
// https://github.com/facebook/lexical/blob/f1e4f66014377b1f2595aec2b0ee17f5b7ef4dfc/packages/lexical/src/LexicalNode.ts#L646
|
|
5165
4962
|
isSelected(selection = null) {
|
|
5166
4963
|
const targetSelection = selection || $getSelection();
|
|
5167
|
-
|
|
4964
|
+
if (!targetSelection) return false
|
|
4965
|
+
|
|
4966
|
+
if (targetSelection.getNodes().some(node => node.is(this) || this.isParentOf(node))) return true
|
|
4967
|
+
|
|
4968
|
+
// A collapsed range selection on the parent element at an offset adjacent to
|
|
4969
|
+
// this node means the caret is visually at this paragraph's position. Treat it
|
|
4970
|
+
// as selected so the paragraph is visible and the caret renders correctly.
|
|
4971
|
+
//
|
|
4972
|
+
// Both the offset matching our index (cursor just before us) and index + 1
|
|
4973
|
+
// (cursor just after us) count, because the provisional paragraph is an
|
|
4974
|
+
// invisible spacer: the browser resolves both offsets to the same visual spot.
|
|
4975
|
+
if ($isRangeSelection(targetSelection) && targetSelection.isCollapsed()) {
|
|
4976
|
+
const { anchor } = targetSelection;
|
|
4977
|
+
const parent = this.getParent();
|
|
4978
|
+
if (parent && anchor.getNode().is(parent) && anchor.type === "element") {
|
|
4979
|
+
const index = this.getIndexWithinParent();
|
|
4980
|
+
return anchor.offset === index || anchor.offset === index + 1
|
|
4981
|
+
}
|
|
4982
|
+
}
|
|
4983
|
+
|
|
4984
|
+
return false
|
|
5168
4985
|
}
|
|
5169
4986
|
|
|
5170
4987
|
removeUnlessRequired(self = this.getLatest()) {
|
|
@@ -5914,6 +5731,178 @@ function $moveSelectionBeforeGallery(anchor) {
|
|
|
5914
5731
|
return true
|
|
5915
5732
|
}
|
|
5916
5733
|
|
|
5734
|
+
class EarlyEscapeCodeNode extends CodeNode {
|
|
5735
|
+
$config() {
|
|
5736
|
+
return this.config("early_escape_code", { extends: CodeNode })
|
|
5737
|
+
}
|
|
5738
|
+
|
|
5739
|
+
static $fromSelection(selection) {
|
|
5740
|
+
const anchorNode = selection.anchor.getNode();
|
|
5741
|
+
return $getNearestNodeOfType(anchorNode, EarlyEscapeCodeNode)
|
|
5742
|
+
|| (anchorNode instanceof EarlyEscapeCodeNode ? anchorNode : null)
|
|
5743
|
+
}
|
|
5744
|
+
|
|
5745
|
+
insertNewAfter(selection, restoreSelection) {
|
|
5746
|
+
if (!selection.isCollapsed()) return super.insertNewAfter(selection, restoreSelection)
|
|
5747
|
+
|
|
5748
|
+
if (this.#isCursorOnEmptyLastLine(selection)) {
|
|
5749
|
+
$trimTrailingBlankNodes(this);
|
|
5750
|
+
|
|
5751
|
+
const paragraph = $createParagraphNode();
|
|
5752
|
+
this.insertAfter(paragraph);
|
|
5753
|
+
return paragraph
|
|
5754
|
+
}
|
|
5755
|
+
|
|
5756
|
+
return super.insertNewAfter(selection, restoreSelection)
|
|
5757
|
+
}
|
|
5758
|
+
|
|
5759
|
+
#isCursorOnEmptyLastLine(selection) {
|
|
5760
|
+
if (!$isCursorOnLastLine(selection)) return false
|
|
5761
|
+
|
|
5762
|
+
const textContent = this.getTextContent();
|
|
5763
|
+
return textContent === "" || textContent.endsWith("\n")
|
|
5764
|
+
}
|
|
5765
|
+
|
|
5766
|
+
}
|
|
5767
|
+
|
|
5768
|
+
class EarlyEscapeListItemNode extends ListItemNode {
|
|
5769
|
+
$config() {
|
|
5770
|
+
return this.config("early_escape_listitem", { extends: ListItemNode })
|
|
5771
|
+
}
|
|
5772
|
+
|
|
5773
|
+
insertNewAfter(selection, restoreSelection) {
|
|
5774
|
+
if (this.#shouldEscape(selection)) {
|
|
5775
|
+
return this.#escapeFromList()
|
|
5776
|
+
}
|
|
5777
|
+
|
|
5778
|
+
return super.insertNewAfter(selection, restoreSelection)
|
|
5779
|
+
}
|
|
5780
|
+
|
|
5781
|
+
#shouldEscape(selection) {
|
|
5782
|
+
if (!$getNearestNodeOfType(this, QuoteNode)) return false
|
|
5783
|
+
if ($isBlankNode(this)) return true
|
|
5784
|
+
|
|
5785
|
+
const paragraph = $getNearestNodeOfType(selection.anchor.getNode(), ParagraphNode);
|
|
5786
|
+
return paragraph && $isBlankNode(paragraph) && $isListItemNode(paragraph.getParent())
|
|
5787
|
+
}
|
|
5788
|
+
|
|
5789
|
+
#escapeFromList() {
|
|
5790
|
+
const parentList = this.getParent();
|
|
5791
|
+
if (!parentList || !$isListNode(parentList)) return
|
|
5792
|
+
|
|
5793
|
+
const blockquote = parentList.getParent();
|
|
5794
|
+
const isInBlockquote = blockquote && $isQuoteNode(blockquote);
|
|
5795
|
+
|
|
5796
|
+
if (isInBlockquote) {
|
|
5797
|
+
const hasNonEmptyListItems = this.getNextSiblings().some(
|
|
5798
|
+
sibling => $isListItemNode(sibling) && !$isBlankNode(sibling)
|
|
5799
|
+
);
|
|
5800
|
+
|
|
5801
|
+
if (hasNonEmptyListItems) {
|
|
5802
|
+
return this.#splitBlockquoteWithList()
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
|
|
5806
|
+
const paragraph = $createParagraphNode();
|
|
5807
|
+
parentList.insertAfter(paragraph);
|
|
5808
|
+
|
|
5809
|
+
this.remove();
|
|
5810
|
+
return paragraph
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
#splitBlockquoteWithList() {
|
|
5814
|
+
const splitQuotes = $splitNode(this.getParent(), this.getIndexWithinParent());
|
|
5815
|
+
this.remove();
|
|
5816
|
+
|
|
5817
|
+
const middleParagraph = $createParagraphNode();
|
|
5818
|
+
splitQuotes[0].insertAfter(middleParagraph);
|
|
5819
|
+
|
|
5820
|
+
splitQuotes.forEach($trimTrailingBlankNodes);
|
|
5821
|
+
|
|
5822
|
+
return middleParagraph
|
|
5823
|
+
}
|
|
5824
|
+
|
|
5825
|
+
}
|
|
5826
|
+
|
|
5827
|
+
class FormatEscapeExtension extends LexxyExtension {
|
|
5828
|
+
|
|
5829
|
+
get enabled() {
|
|
5830
|
+
return this.editorElement.supportsRichText
|
|
5831
|
+
}
|
|
5832
|
+
|
|
5833
|
+
get lexicalExtension() {
|
|
5834
|
+
return defineExtension({
|
|
5835
|
+
name: "lexxy/format-escape",
|
|
5836
|
+
nodes: [
|
|
5837
|
+
EarlyEscapeCodeNode,
|
|
5838
|
+
{ replace: CodeNode, with: (node) => new EarlyEscapeCodeNode(node.getLanguage()), withKlass: EarlyEscapeCodeNode },
|
|
5839
|
+
EarlyEscapeListItemNode,
|
|
5840
|
+
{ replace: ListItemNode, with: () => new EarlyEscapeListItemNode(), withKlass: EarlyEscapeListItemNode },
|
|
5841
|
+
],
|
|
5842
|
+
register(editor) {
|
|
5843
|
+
return mergeRegister(
|
|
5844
|
+
editor.registerCommand(
|
|
5845
|
+
INSERT_PARAGRAPH_COMMAND,
|
|
5846
|
+
() => $escapeFromBlockquote(),
|
|
5847
|
+
COMMAND_PRIORITY_HIGH
|
|
5848
|
+
),
|
|
5849
|
+
editor.registerCommand(
|
|
5850
|
+
KEY_ARROW_DOWN_COMMAND,
|
|
5851
|
+
(event) => $handleArrowDownInCodeBlock(event),
|
|
5852
|
+
COMMAND_PRIORITY_NORMAL
|
|
5853
|
+
)
|
|
5854
|
+
)
|
|
5855
|
+
}
|
|
5856
|
+
})
|
|
5857
|
+
}
|
|
5858
|
+
}
|
|
5859
|
+
|
|
5860
|
+
function $escapeFromBlockquote() {
|
|
5861
|
+
const anchorNode = $getSelection().anchor.getNode();
|
|
5862
|
+
|
|
5863
|
+
const paragraph = $getNearestNodeOfType(anchorNode, ParagraphNode);
|
|
5864
|
+
if (!paragraph || !$isBlankNode(paragraph)) return false
|
|
5865
|
+
|
|
5866
|
+
const blockquote = paragraph.getParent();
|
|
5867
|
+
if (!blockquote || !$isQuoteNode(blockquote)) return false
|
|
5868
|
+
|
|
5869
|
+
const nonEmptySiblings = paragraph.getNextSiblings().filter(sibling => !$isBlankNode(sibling));
|
|
5870
|
+
|
|
5871
|
+
if (nonEmptySiblings.length > 0) {
|
|
5872
|
+
$splitQuoteNode(blockquote, paragraph);
|
|
5873
|
+
} else {
|
|
5874
|
+
blockquote.insertAfter(paragraph);
|
|
5875
|
+
paragraph.selectStart();
|
|
5876
|
+
}
|
|
5877
|
+
|
|
5878
|
+
return true
|
|
5879
|
+
}
|
|
5880
|
+
|
|
5881
|
+
function $splitQuoteNode(node, paragraph) {
|
|
5882
|
+
const splitQuotes = $splitNode(node, paragraph.getIndexWithinParent());
|
|
5883
|
+
splitQuotes[0].insertAfter(paragraph);
|
|
5884
|
+
splitQuotes.forEach($trimTrailingBlankNodes);
|
|
5885
|
+
paragraph.selectEnd();
|
|
5886
|
+
}
|
|
5887
|
+
|
|
5888
|
+
function $handleArrowDownInCodeBlock(event) {
|
|
5889
|
+
const selection = $getSelection();
|
|
5890
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false
|
|
5891
|
+
|
|
5892
|
+
const codeNode = EarlyEscapeCodeNode.$fromSelection(selection);
|
|
5893
|
+
if (!codeNode) return false
|
|
5894
|
+
|
|
5895
|
+
if ($isCursorOnLastLine(selection) && !codeNode.getNextSibling()) {
|
|
5896
|
+
event?.preventDefault();
|
|
5897
|
+
const paragraph = $createParagraphNode();
|
|
5898
|
+
codeNode.insertAfter(paragraph);
|
|
5899
|
+
paragraph.selectEnd();
|
|
5900
|
+
return true
|
|
5901
|
+
}
|
|
5902
|
+
|
|
5903
|
+
return false
|
|
5904
|
+
}
|
|
5905
|
+
|
|
5917
5906
|
class LexicalEditorElement extends HTMLElement {
|
|
5918
5907
|
static formAssociated = true
|
|
5919
5908
|
static debug = false
|
|
@@ -5974,7 +5963,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
5974
5963
|
}
|
|
5975
5964
|
|
|
5976
5965
|
toString() {
|
|
5977
|
-
if (
|
|
5966
|
+
if (this.cachedStringValue == null) {
|
|
5978
5967
|
this.editor?.getEditorState().read(() => {
|
|
5979
5968
|
this.cachedStringValue = $getReadableTextContent($getRoot());
|
|
5980
5969
|
});
|
|
@@ -6004,7 +5993,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6004
5993
|
HighlightExtension,
|
|
6005
5994
|
TrixContentExtension,
|
|
6006
5995
|
TablesExtension,
|
|
6007
|
-
AttachmentsExtension
|
|
5996
|
+
AttachmentsExtension,
|
|
5997
|
+
FormatEscapeExtension
|
|
6008
5998
|
]
|
|
6009
5999
|
}
|
|
6010
6000
|
|
|
@@ -6354,22 +6344,23 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6354
6344
|
}
|
|
6355
6345
|
|
|
6356
6346
|
#findOrCreateDefaultToolbar() {
|
|
6357
|
-
const
|
|
6358
|
-
if (
|
|
6359
|
-
return document.getElementById(
|
|
6347
|
+
const toolbarConfig = this.config.get("toolbar");
|
|
6348
|
+
if (typeof toolbarConfig === "string") {
|
|
6349
|
+
return document.getElementById(toolbarConfig)
|
|
6360
6350
|
} else {
|
|
6361
6351
|
return this.#createDefaultToolbar()
|
|
6362
6352
|
}
|
|
6363
6353
|
}
|
|
6364
6354
|
|
|
6365
6355
|
get #hasToolbar() {
|
|
6366
|
-
return this.supportsRichText && this.config.get("toolbar")
|
|
6356
|
+
return this.supportsRichText && !!this.config.get("toolbar")
|
|
6367
6357
|
}
|
|
6368
6358
|
|
|
6369
6359
|
#createDefaultToolbar() {
|
|
6370
6360
|
const toolbar = createElement("lexxy-toolbar");
|
|
6371
6361
|
toolbar.innerHTML = LexicalToolbarElement.defaultTemplate;
|
|
6372
6362
|
toolbar.setAttribute("data-attachments", this.supportsAttachments); // Drives toolbar CSS styles
|
|
6363
|
+
toolbar.configure(this.config.get("toolbar"));
|
|
6373
6364
|
this.prepend(toolbar);
|
|
6374
6365
|
return toolbar
|
|
6375
6366
|
}
|
|
@@ -6436,6 +6427,10 @@ function $getReadableTextContent(node) {
|
|
|
6436
6427
|
const children = node.getChildren();
|
|
6437
6428
|
for (let i = 0; i < children.length; i++) {
|
|
6438
6429
|
const child = children[i];
|
|
6430
|
+
const previousChild = children[i - 1];
|
|
6431
|
+
|
|
6432
|
+
if (isAttachmentSpacerTextNode(child, previousChild, i, children.length)) continue
|
|
6433
|
+
|
|
6439
6434
|
text += $getReadableTextContent(child);
|
|
6440
6435
|
if ($isElementNode(child) && i !== children.length - 1 && !child.isInline()) {
|
|
6441
6436
|
text += "\n\n";
|
|
@@ -6820,6 +6815,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
6820
6815
|
constructor() {
|
|
6821
6816
|
super();
|
|
6822
6817
|
this.keyListeners = [];
|
|
6818
|
+
this.showPopoverId = 0;
|
|
6823
6819
|
}
|
|
6824
6820
|
|
|
6825
6821
|
static observedAttributes = [ "connected" ]
|
|
@@ -6965,9 +6961,14 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
6965
6961
|
}
|
|
6966
6962
|
|
|
6967
6963
|
async #showPopover() {
|
|
6964
|
+
const showId = ++this.showPopoverId;
|
|
6968
6965
|
this.popoverElement ??= await this.#buildPopover();
|
|
6966
|
+
if (this.showPopoverId !== showId) return
|
|
6967
|
+
|
|
6969
6968
|
this.#resetPopoverPosition();
|
|
6970
6969
|
await this.#filterOptions();
|
|
6970
|
+
if (this.showPopoverId !== showId) return
|
|
6971
|
+
|
|
6971
6972
|
this.popoverElement.classList.toggle("lexxy-prompt-menu--visible", true);
|
|
6972
6973
|
this.#selectFirstOption();
|
|
6973
6974
|
|
|
@@ -7078,6 +7079,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7078
7079
|
}
|
|
7079
7080
|
|
|
7080
7081
|
async #hidePopover() {
|
|
7082
|
+
this.showPopoverId++;
|
|
7081
7083
|
this.#clearSelection();
|
|
7082
7084
|
this.popoverElement.classList.toggle("lexxy-prompt-menu--visible", false);
|
|
7083
7085
|
this.#editorElement.removeEventListener("lexxy:change", this.#filterOptions);
|
|
@@ -7103,6 +7105,14 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7103
7105
|
|
|
7104
7106
|
if (this.#editorContents.containsTextBackUntil(this.trigger)) {
|
|
7105
7107
|
await this.#showFilteredOptions();
|
|
7108
|
+
|
|
7109
|
+
// Re-check after async operation — the trigger may have been consumed
|
|
7110
|
+
// (e.g. markdown heading shortcut converted "# " to h1 during the fetch)
|
|
7111
|
+
if (!this.#editorContents.containsTextBackUntil(this.trigger)) {
|
|
7112
|
+
this.#hidePopover();
|
|
7113
|
+
return
|
|
7114
|
+
}
|
|
7115
|
+
|
|
7106
7116
|
await nextFrame();
|
|
7107
7117
|
this.#positionPopover();
|
|
7108
7118
|
} else {
|
|
@@ -7111,8 +7121,12 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7111
7121
|
}
|
|
7112
7122
|
|
|
7113
7123
|
async #showFilteredOptions() {
|
|
7124
|
+
const showId = this.showPopoverId;
|
|
7114
7125
|
const filter = this.#editorContents.textBackUntil(this.trigger);
|
|
7115
7126
|
const filteredListItems = await this.source.buildListItems(filter);
|
|
7127
|
+
if (this.showPopoverId !== showId) return
|
|
7128
|
+
if (!this.#editorContents.containsTextBackUntil(this.trigger)) return
|
|
7129
|
+
|
|
7116
7130
|
this.popoverElement.innerHTML = "";
|
|
7117
7131
|
|
|
7118
7132
|
if (filteredListItems.length > 0) {
|