@37signals/lexxy 0.7.4-beta → 0.7.5-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 +653 -693
- package/dist/lexxy_helpers.esm.js +1 -9
- package/dist/stylesheets/lexxy-editor.css +9 -0
- package/package.json +3 -1
package/dist/lexxy.esm.js
CHANGED
|
@@ -10,20 +10,20 @@ import 'prismjs/components/prism-json';
|
|
|
10
10
|
import 'prismjs/components/prism-diff';
|
|
11
11
|
import DOMPurify from 'dompurify';
|
|
12
12
|
import { getStyleObjectFromCSS, getCSSFromStyleObject, $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/selection';
|
|
13
|
-
import {
|
|
14
|
-
import { $isListItemNode, $isListNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, ListNode, $getListDepth, $createListNode, ListItemNode, registerList } from '@lexical/list';
|
|
15
|
-
import { $isQuoteNode, $isHeadingNode, $createQuoteNode, $createHeadingNode, RichTextExtension, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
16
|
-
import { $isCodeNode, CodeNode, normalizeCodeLang, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
|
|
17
|
-
import { $isLinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, LinkNode, AutoLinkNode } from '@lexical/link';
|
|
18
|
-
import { $getTableCellNodeFromLexicalNode, INSERT_TABLE_COMMAND, TableCellNode, TableNode, TableRowNode, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive, TableCellHeaderStates, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $findCellNode, $getElementForTableNode } from '@lexical/table';
|
|
19
|
-
import { createElement, createAttachmentFigure, isPreviewableImage, dispatchCustomEvent, parseHtml, dispatch, generateDomId } from './lexxy_helpers.esm.js';
|
|
20
|
-
export { highlightCode as highlightAll, highlightCode } from './lexxy_helpers.esm.js';
|
|
13
|
+
import { SKIP_DOM_SELECTION_TAG, $getSelection, $isRangeSelection, DecoratorNode, $getEditor, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $createNodeSelection, $isTextNode, TextNode, createCommand, createState, defineExtension, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, $isRootOrShadowRoot, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $isDecoratorNode, $createParagraphNode, $setSelection, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, $isParagraphNode, $insertNodes, $getNodeByKey, $createLineBreakNode, ParagraphNode, RootNode, CLEAR_HISTORY_COMMAND, $addUpdateTag, KEY_SPACE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
|
|
21
14
|
import { buildEditorFromExtensions } from '@lexical/extension';
|
|
15
|
+
import { ListNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, ListItemNode, $getListDepth, $isListItemNode, $isListNode, $createListNode, registerList } from '@lexical/list';
|
|
16
|
+
import { $createAutoLinkNode, $toggleLink, LinkNode, $createLinkNode, AutoLinkNode, $isLinkNode } from '@lexical/link';
|
|
22
17
|
import { registerPlainText } from '@lexical/plain-text';
|
|
18
|
+
import { RichTextExtension, $isQuoteNode, $createQuoteNode, $createHeadingNode, $isHeadingNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
23
19
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
20
|
+
import { $isCodeNode, CodeNode, normalizeCodeLang, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
|
|
24
21
|
import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown';
|
|
25
22
|
import { createEmptyHistoryState, registerHistory } from '@lexical/history';
|
|
26
|
-
import {
|
|
23
|
+
import { createElement, createAttachmentFigure, isPreviewableImage, parseHtml, dispatch, generateDomId } from './lexxy_helpers.esm.js';
|
|
24
|
+
export { highlightCode as highlightAll, highlightCode } from './lexxy_helpers.esm.js';
|
|
25
|
+
import { $getNearestNodeOfType, mergeRegister, $insertFirst, $firstToLastIterator, $descendantsMatching } from '@lexical/utils';
|
|
26
|
+
import { INSERT_TABLE_COMMAND, $getTableCellNodeFromLexicalNode, TableCellNode, TableNode, TableRowNode, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive, TableCellHeaderStates, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $findCellNode, $getElementForTableNode } from '@lexical/table';
|
|
27
27
|
import { marked } from 'marked';
|
|
28
28
|
import { $insertDataTransferForRichText } from '@lexical/clipboard';
|
|
29
29
|
|
|
@@ -156,137 +156,6 @@ function getNonce() {
|
|
|
156
156
|
return element?.content
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
const SILENT_UPDATE_TAGS = [ HISTORY_MERGE_TAG, SKIP_DOM_SELECTION_TAG, SKIP_SCROLL_INTO_VIEW_TAG ];
|
|
160
|
-
|
|
161
|
-
function getNearestListItemNode(node) {
|
|
162
|
-
let current = node;
|
|
163
|
-
while (current !== null) {
|
|
164
|
-
if ($isListItemNode(current)) return current
|
|
165
|
-
current = current.getParent();
|
|
166
|
-
}
|
|
167
|
-
return null
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function getListType(node) {
|
|
171
|
-
let current = node;
|
|
172
|
-
while (current) {
|
|
173
|
-
if ($isListNode(current)) {
|
|
174
|
-
return current.getListType()
|
|
175
|
-
}
|
|
176
|
-
current = current.getParent();
|
|
177
|
-
}
|
|
178
|
-
return null
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function isPrintableCharacter(event) {
|
|
182
|
-
// Ignore if modifier keys are pressed (except Shift for uppercase)
|
|
183
|
-
if (event.ctrlKey || event.metaKey || event.altKey) return false
|
|
184
|
-
|
|
185
|
-
// Ignore special keys
|
|
186
|
-
if (event.key.length > 1 && event.key !== "Enter" && event.key !== "Space") return false
|
|
187
|
-
|
|
188
|
-
// Accept single character keys (letters, numbers, punctuation)
|
|
189
|
-
return event.key.length === 1
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function extendTextNodeConversion(conversionName, ...callbacks) {
|
|
193
|
-
return extendConversion(TextNode, conversionName, (conversionOutput, element) => ({
|
|
194
|
-
...conversionOutput,
|
|
195
|
-
forChild: (lexicalNode, parentNode) => {
|
|
196
|
-
const originalForChild = conversionOutput?.forChild ?? (x => x);
|
|
197
|
-
let childNode = originalForChild(lexicalNode, parentNode);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if ($isTextNode(childNode)) {
|
|
201
|
-
childNode = callbacks.reduce(
|
|
202
|
-
(childNode, callback) => callback(childNode, element) ?? childNode,
|
|
203
|
-
childNode
|
|
204
|
-
);
|
|
205
|
-
return childNode
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}))
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function extendConversion(nodeKlass, conversionName, callback = (output => output)) {
|
|
212
|
-
return (element) => {
|
|
213
|
-
const converter = nodeKlass.importDOM()?.[conversionName]?.(element);
|
|
214
|
-
if (!converter) return null
|
|
215
|
-
|
|
216
|
-
const conversionOutput = converter.conversion(element);
|
|
217
|
-
if (!conversionOutput) return conversionOutput
|
|
218
|
-
|
|
219
|
-
return callback(conversionOutput, element) ?? conversionOutput
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function isSelectionHighlighted(selection) {
|
|
224
|
-
if (!$isRangeSelection(selection)) return false
|
|
225
|
-
|
|
226
|
-
if (selection.isCollapsed()) {
|
|
227
|
-
return hasHighlightStyles(selection.style)
|
|
228
|
-
} else {
|
|
229
|
-
return selection.hasFormat("highlight")
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function hasHighlightStyles(cssOrStyles) {
|
|
234
|
-
const styles = typeof cssOrStyles === "string" ? getStyleObjectFromCSS(cssOrStyles) : cssOrStyles;
|
|
235
|
-
return !!(styles.color || styles["background-color"])
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
class StyleCanonicalizer {
|
|
239
|
-
constructor(property, allowedValues= []) {
|
|
240
|
-
this._property = property;
|
|
241
|
-
this._allowedValues = allowedValues;
|
|
242
|
-
this._canonicalValues = this.#allowedValuesIdentityObject;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
applyCanonicalization(css) {
|
|
246
|
-
const styles = { ...getStyleObjectFromCSS(css) };
|
|
247
|
-
|
|
248
|
-
styles[this._property] = this.getCanonicalAllowedValue(styles[this._property]);
|
|
249
|
-
if (!styles[this._property]) {
|
|
250
|
-
delete styles[this._property];
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return getCSSFromStyleObject(styles)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
getCanonicalAllowedValue(value) {
|
|
257
|
-
return this._canonicalValues[value] ||= this.#resolveCannonicalValue(value)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Private
|
|
261
|
-
|
|
262
|
-
get #allowedValuesIdentityObject() {
|
|
263
|
-
return this._allowedValues.reduce((object, value) => ({ ...object, [value]: value }), {})
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
#resolveCannonicalValue(value) {
|
|
267
|
-
let index = this.#computedAllowedValues.indexOf(value);
|
|
268
|
-
index ||= this.#computedAllowedValues.indexOf(getComputedStyleForProperty(this._property, value));
|
|
269
|
-
return index === -1 ? null : this._allowedValues[index]
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
get #computedAllowedValues() {
|
|
273
|
-
return this._computedAllowedValues ||= this._allowedValues.map(
|
|
274
|
-
value => getComputedStyleForProperty(this._property, value)
|
|
275
|
-
)
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function getComputedStyleForProperty(property, value) {
|
|
280
|
-
const style = `${property}: ${value};`;
|
|
281
|
-
|
|
282
|
-
// the element has to be attached to the DOM have computed styles
|
|
283
|
-
const element = document.body.appendChild(createElement("span", { style: "display: none;" + style }));
|
|
284
|
-
const computedStyle = window.getComputedStyle(element).getPropertyValue(property);
|
|
285
|
-
element.remove();
|
|
286
|
-
|
|
287
|
-
return computedStyle
|
|
288
|
-
}
|
|
289
|
-
|
|
290
159
|
function handleRollingTabIndex(elements, event) {
|
|
291
160
|
const previousActiveElement = document.activeElement;
|
|
292
161
|
|
|
@@ -406,6 +275,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
406
275
|
setEditor(editorElement) {
|
|
407
276
|
this.editorElement = editorElement;
|
|
408
277
|
this.editor = editorElement.editor;
|
|
278
|
+
this.selection = editorElement.selection;
|
|
409
279
|
this.#bindButtons();
|
|
410
280
|
this.#bindHotkeys();
|
|
411
281
|
this.#resetTabIndexValues();
|
|
@@ -565,19 +435,8 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
565
435
|
const anchorNode = selection.anchor.getNode();
|
|
566
436
|
if (!anchorNode.getParent()) { return }
|
|
567
437
|
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
const isBold = selection.hasFormat("bold");
|
|
571
|
-
const isItalic = selection.hasFormat("italic");
|
|
572
|
-
const isStrikethrough = selection.hasFormat("strikethrough");
|
|
573
|
-
const isHighlight = isSelectionHighlighted(selection);
|
|
574
|
-
const isInLink = this.#isInLink(anchorNode);
|
|
575
|
-
const isInQuote = $isQuoteNode(topLevelElement);
|
|
576
|
-
const isInHeading = $isHeadingNode(topLevelElement);
|
|
577
|
-
const isInCode = $isCodeNode(topLevelElement) || selection.hasFormat("code");
|
|
578
|
-
const isInList = this.#isInList(anchorNode);
|
|
579
|
-
const listType = getListType(anchorNode);
|
|
580
|
-
const isInTable = $getTableCellNodeFromLexicalNode(anchorNode) !== null;
|
|
438
|
+
const { isBold, isItalic, isStrikethrough, isHighlight, isInLink, isInQuote, isInHeading,
|
|
439
|
+
isInCode, isInList, listType, isInTable } = this.selection.getFormat();
|
|
581
440
|
|
|
582
441
|
this.#setButtonPressed("bold", isBold);
|
|
583
442
|
this.#setButtonPressed("italic", isItalic);
|
|
@@ -594,24 +453,6 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
594
453
|
this.#updateUndoRedoButtonStates();
|
|
595
454
|
}
|
|
596
455
|
|
|
597
|
-
#isInList(node) {
|
|
598
|
-
let current = node;
|
|
599
|
-
while (current) {
|
|
600
|
-
if ($isListNode(current) || $isListItemNode(current)) return true
|
|
601
|
-
current = current.getParent();
|
|
602
|
-
}
|
|
603
|
-
return false
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
#isInLink(node) {
|
|
607
|
-
let current = node;
|
|
608
|
-
while (current) {
|
|
609
|
-
if ($isLinkNode(current)) return true
|
|
610
|
-
current = current.getParent();
|
|
611
|
-
}
|
|
612
|
-
return false
|
|
613
|
-
}
|
|
614
|
-
|
|
615
456
|
#setButtonPressed(name, isPressed) {
|
|
616
457
|
const button = this.querySelector(`[name="${name}"]`);
|
|
617
458
|
if (button) {
|
|
@@ -975,10 +816,6 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
975
816
|
createDOM() {
|
|
976
817
|
const figure = this.createAttachmentFigure();
|
|
977
818
|
|
|
978
|
-
figure.addEventListener("click", () => {
|
|
979
|
-
this.#select(figure);
|
|
980
|
-
});
|
|
981
|
-
|
|
982
819
|
if (this.isPreviewableAttachment) {
|
|
983
820
|
figure.appendChild(this.#createDOMForImage());
|
|
984
821
|
figure.appendChild(this.#createEditableCaption());
|
|
@@ -1091,10 +928,6 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
1091
928
|
return figcaption
|
|
1092
929
|
}
|
|
1093
930
|
|
|
1094
|
-
#select(figure) {
|
|
1095
|
-
dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() });
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
931
|
#createEditableCaption() {
|
|
1099
932
|
const caption = createElement("figcaption", { className: "attachment__caption" });
|
|
1100
933
|
const input = createElement("textarea", {
|
|
@@ -1125,14 +958,62 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
1125
958
|
|
|
1126
959
|
#handleCaptionInputKeydown(event) {
|
|
1127
960
|
if (event.key === "Enter") {
|
|
1128
|
-
this.#updateCaptionValueFromInput(event.target);
|
|
1129
961
|
event.preventDefault();
|
|
962
|
+
event.stopPropagation();
|
|
963
|
+
event.target.blur();
|
|
1130
964
|
|
|
1131
965
|
this.editor.update(() => {
|
|
1132
|
-
|
|
1133
|
-
|
|
966
|
+
// Place the cursor after the current image
|
|
967
|
+
this.selectNext(0, 0);
|
|
968
|
+
}, {
|
|
969
|
+
tag: HISTORY_MERGE_TAG
|
|
970
|
+
});
|
|
1134
971
|
}
|
|
1135
|
-
|
|
972
|
+
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const SILENT_UPDATE_TAGS = [ HISTORY_MERGE_TAG, SKIP_DOM_SELECTION_TAG, SKIP_SCROLL_INTO_VIEW_TAG ];
|
|
977
|
+
|
|
978
|
+
function $createNodeSelectionWith(...nodes) {
|
|
979
|
+
const selection = $createNodeSelection();
|
|
980
|
+
nodes.forEach(node => selection.add(node.getKey()));
|
|
981
|
+
return selection
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function getListType(node) {
|
|
985
|
+
const list = $getNearestNodeOfType(node, ListNode);
|
|
986
|
+
return list?.getListType() ?? null
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function extendTextNodeConversion(conversionName, ...callbacks) {
|
|
990
|
+
return extendConversion(TextNode, conversionName, (conversionOutput, element) => ({
|
|
991
|
+
...conversionOutput,
|
|
992
|
+
forChild: (lexicalNode, parentNode) => {
|
|
993
|
+
const originalForChild = conversionOutput?.forChild ?? (x => x);
|
|
994
|
+
let childNode = originalForChild(lexicalNode, parentNode);
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
if ($isTextNode(childNode)) {
|
|
998
|
+
childNode = callbacks.reduce(
|
|
999
|
+
(childNode, callback) => callback(childNode, element) ?? childNode,
|
|
1000
|
+
childNode
|
|
1001
|
+
);
|
|
1002
|
+
return childNode
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}))
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function extendConversion(nodeKlass, conversionName, callback = (output => output)) {
|
|
1009
|
+
return (element) => {
|
|
1010
|
+
const converter = nodeKlass.importDOM()?.[conversionName]?.(element);
|
|
1011
|
+
if (!converter) return null
|
|
1012
|
+
|
|
1013
|
+
const conversionOutput = converter.conversion(element);
|
|
1014
|
+
if (!conversionOutput) return conversionOutput
|
|
1015
|
+
|
|
1016
|
+
return callback(conversionOutput, element) ?? conversionOutput
|
|
1136
1017
|
}
|
|
1137
1018
|
}
|
|
1138
1019
|
|
|
@@ -1219,8 +1100,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
1219
1100
|
}
|
|
1220
1101
|
|
|
1221
1102
|
exportDOM() {
|
|
1222
|
-
|
|
1223
|
-
return { element: img }
|
|
1103
|
+
return { element: null }
|
|
1224
1104
|
}
|
|
1225
1105
|
|
|
1226
1106
|
exportJSON() {
|
|
@@ -1429,10 +1309,6 @@ class HorizontalDividerNode extends DecoratorNode {
|
|
|
1429
1309
|
const figure = createElement("figure", { className: "horizontal-divider" });
|
|
1430
1310
|
const hr = createElement("hr");
|
|
1431
1311
|
|
|
1432
|
-
figure.addEventListener("click", (event) => {
|
|
1433
|
-
dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() });
|
|
1434
|
-
});
|
|
1435
|
-
|
|
1436
1312
|
figure.appendChild(hr);
|
|
1437
1313
|
|
|
1438
1314
|
return figure
|
|
@@ -1467,6 +1343,225 @@ class HorizontalDividerNode extends DecoratorNode {
|
|
|
1467
1343
|
}
|
|
1468
1344
|
}
|
|
1469
1345
|
|
|
1346
|
+
function isSelectionHighlighted(selection) {
|
|
1347
|
+
if (!$isRangeSelection(selection)) return false
|
|
1348
|
+
|
|
1349
|
+
if (selection.isCollapsed()) {
|
|
1350
|
+
return hasHighlightStyles(selection.style)
|
|
1351
|
+
} else {
|
|
1352
|
+
return selection.hasFormat("highlight")
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function hasHighlightStyles(cssOrStyles) {
|
|
1357
|
+
const styles = typeof cssOrStyles === "string" ? getStyleObjectFromCSS(cssOrStyles) : cssOrStyles;
|
|
1358
|
+
return !!(styles.color || styles["background-color"])
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function applyCanonicalizers(styles, canonicalizers = []) {
|
|
1362
|
+
return canonicalizers.reduce((css, canonicalizer) => {
|
|
1363
|
+
return canonicalizer.applyCanonicalization(css)
|
|
1364
|
+
}, styles)
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
class StyleCanonicalizer {
|
|
1368
|
+
constructor(property, allowedValues= []) {
|
|
1369
|
+
this._property = property;
|
|
1370
|
+
this._allowedValues = allowedValues;
|
|
1371
|
+
this._canonicalValues = this.#allowedValuesIdentityObject;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
applyCanonicalization(css) {
|
|
1375
|
+
const styles = { ...getStyleObjectFromCSS(css) };
|
|
1376
|
+
|
|
1377
|
+
styles[this._property] = this.getCanonicalAllowedValue(styles[this._property]);
|
|
1378
|
+
if (!styles[this._property]) {
|
|
1379
|
+
delete styles[this._property];
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
return getCSSFromStyleObject(styles)
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
getCanonicalAllowedValue(value) {
|
|
1386
|
+
return this._canonicalValues[value] ||= this.#resolveCannonicalValue(value)
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Private
|
|
1390
|
+
|
|
1391
|
+
get #allowedValuesIdentityObject() {
|
|
1392
|
+
return this._allowedValues.reduce((object, value) => ({ ...object, [value]: value }), {})
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
#resolveCannonicalValue(value) {
|
|
1396
|
+
let index = this.#computedAllowedValues.indexOf(value);
|
|
1397
|
+
index ||= this.#computedAllowedValues.indexOf(getComputedStyleForProperty(this._property, value));
|
|
1398
|
+
return index === -1 ? null : this._allowedValues[index]
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
get #computedAllowedValues() {
|
|
1402
|
+
return this._computedAllowedValues ||= this._allowedValues.map(
|
|
1403
|
+
value => getComputedStyleForProperty(this._property, value)
|
|
1404
|
+
)
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function getComputedStyleForProperty(property, value) {
|
|
1409
|
+
const style = `${property}: ${value};`;
|
|
1410
|
+
|
|
1411
|
+
// the element has to be attached to the DOM have computed styles
|
|
1412
|
+
const element = document.body.appendChild(createElement("span", { style: "display: none;" + style }));
|
|
1413
|
+
const computedStyle = window.getComputedStyle(element).getPropertyValue(property);
|
|
1414
|
+
element.remove();
|
|
1415
|
+
|
|
1416
|
+
return computedStyle
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
class LexxyExtension {
|
|
1420
|
+
#editorElement
|
|
1421
|
+
|
|
1422
|
+
constructor(editorElement) {
|
|
1423
|
+
this.#editorElement = editorElement;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
get editorElement() {
|
|
1427
|
+
return this.#editorElement
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
get editorConfig() {
|
|
1431
|
+
return this.#editorElement.config
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// optional: defaults to true
|
|
1435
|
+
get enabled() {
|
|
1436
|
+
return true
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
get lexicalExtension() {
|
|
1440
|
+
return null
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
initializeToolbar(_lexxyToolbar) {
|
|
1444
|
+
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const TOGGLE_HIGHLIGHT_COMMAND = createCommand();
|
|
1449
|
+
const REMOVE_HIGHLIGHT_COMMAND = createCommand();
|
|
1450
|
+
const BLANK_STYLES = { "color": null, "background-color": null };
|
|
1451
|
+
|
|
1452
|
+
const hasPastedStylesState = createState("hasPastedStyles", {
|
|
1453
|
+
parse: (value) => value || false
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
class HighlightExtension extends LexxyExtension {
|
|
1457
|
+
get enabled() {
|
|
1458
|
+
return this.editorElement.supportsRichText
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
get lexicalExtension() {
|
|
1462
|
+
const extension = defineExtension({
|
|
1463
|
+
dependencies: [ RichTextExtension ],
|
|
1464
|
+
name: "lexxy/highlight",
|
|
1465
|
+
config: {
|
|
1466
|
+
color: { buttons: [], permit: [] },
|
|
1467
|
+
"background-color": { buttons: [], permit: [] }
|
|
1468
|
+
},
|
|
1469
|
+
html: {
|
|
1470
|
+
import: {
|
|
1471
|
+
mark: $markConversion
|
|
1472
|
+
}
|
|
1473
|
+
},
|
|
1474
|
+
register(editor, config) {
|
|
1475
|
+
// keep the ref to the canonicalizers for optimized css conversion
|
|
1476
|
+
const canonicalizers = buildCanonicalizers(config);
|
|
1477
|
+
|
|
1478
|
+
return mergeRegister(
|
|
1479
|
+
editor.registerCommand(TOGGLE_HIGHLIGHT_COMMAND, $toggleSelectionStyles, COMMAND_PRIORITY_NORMAL),
|
|
1480
|
+
editor.registerCommand(REMOVE_HIGHLIGHT_COMMAND, () => $toggleSelectionStyles(BLANK_STYLES), COMMAND_PRIORITY_NORMAL),
|
|
1481
|
+
editor.registerNodeTransform(TextNode, $syncHighlightWithStyle),
|
|
1482
|
+
editor.registerNodeTransform(TextNode, (textNode) => $canonicalizePastedStyles(textNode, canonicalizers))
|
|
1483
|
+
)
|
|
1484
|
+
}
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
return [ extension, this.editorConfig.get("highlight") ]
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function $applyHighlightStyle(textNode, element) {
|
|
1492
|
+
const elementStyles = {
|
|
1493
|
+
color: element.style?.color,
|
|
1494
|
+
"background-color": element.style?.backgroundColor
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
if ($hasUpdateTag(PASTE_TAG)) { $setPastedStyles(textNode); }
|
|
1498
|
+
const highlightStyle = getCSSFromStyleObject(elementStyles);
|
|
1499
|
+
|
|
1500
|
+
if (highlightStyle.length) {
|
|
1501
|
+
return textNode.setStyle(textNode.getStyle() + highlightStyle)
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function $markConversion() {
|
|
1506
|
+
return {
|
|
1507
|
+
conversion: extendTextNodeConversion("mark", $applyHighlightStyle),
|
|
1508
|
+
priority: 1
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function buildCanonicalizers(config) {
|
|
1513
|
+
return [
|
|
1514
|
+
new StyleCanonicalizer("color", [ ...config.buttons.color, ...config.permit.color ]),
|
|
1515
|
+
new StyleCanonicalizer("background-color", [ ...config.buttons["background-color"], ...config.permit["background-color"] ])
|
|
1516
|
+
]
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function $toggleSelectionStyles(styles) {
|
|
1520
|
+
const selection = $getSelection();
|
|
1521
|
+
if (!$isRangeSelection(selection)) return
|
|
1522
|
+
|
|
1523
|
+
const patch = {};
|
|
1524
|
+
for (const property in styles) {
|
|
1525
|
+
const oldValue = $getSelectionStyleValueForProperty(selection, property);
|
|
1526
|
+
patch[property] = toggleOrReplace(oldValue, styles[property]);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
$patchStyleText(selection, patch);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function toggleOrReplace(oldValue, newValue) {
|
|
1533
|
+
return oldValue === newValue ? null : newValue
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function $syncHighlightWithStyle(textNode) {
|
|
1537
|
+
if (hasHighlightStyles(textNode.getStyle()) !== textNode.hasFormat("highlight")) {
|
|
1538
|
+
textNode.toggleFormat("highlight");
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function $canonicalizePastedStyles(textNode, canonicalizers = []) {
|
|
1543
|
+
if ($hasPastedStyles(textNode)) {
|
|
1544
|
+
$setPastedStyles(textNode, false);
|
|
1545
|
+
|
|
1546
|
+
const canonicalizedCSS = applyCanonicalizers(textNode.getStyle(), canonicalizers);
|
|
1547
|
+
textNode.setStyle(canonicalizedCSS);
|
|
1548
|
+
|
|
1549
|
+
const selection = $getSelection();
|
|
1550
|
+
if (textNode.isSelected(selection)) {
|
|
1551
|
+
selection.setStyle(textNode.getStyle());
|
|
1552
|
+
selection.setFormat(textNode.getFormat());
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function $setPastedStyles(textNode, value = true) {
|
|
1558
|
+
$setState(textNode, hasPastedStylesState, value);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
function $hasPastedStyles(textNode) {
|
|
1562
|
+
return $getState(textNode, hasPastedStylesState)
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1470
1565
|
const COMMANDS = [
|
|
1471
1566
|
"bold",
|
|
1472
1567
|
"italic",
|
|
@@ -1500,7 +1595,6 @@ class CommandDispatcher {
|
|
|
1500
1595
|
this.selection = editorElement.selection;
|
|
1501
1596
|
this.contents = editorElement.contents;
|
|
1502
1597
|
this.clipboard = editorElement.clipboard;
|
|
1503
|
-
this.highlighter = editorElement.highlighter;
|
|
1504
1598
|
|
|
1505
1599
|
this.#registerCommands();
|
|
1506
1600
|
this.#registerKeyboardCommands();
|
|
@@ -1524,11 +1618,11 @@ class CommandDispatcher {
|
|
|
1524
1618
|
}
|
|
1525
1619
|
|
|
1526
1620
|
dispatchToggleHighlight(styles) {
|
|
1527
|
-
this.
|
|
1621
|
+
this.editor.dispatchCommand(TOGGLE_HIGHLIGHT_COMMAND, styles);
|
|
1528
1622
|
}
|
|
1529
1623
|
|
|
1530
1624
|
dispatchRemoveHighlight() {
|
|
1531
|
-
this.
|
|
1625
|
+
this.editor.dispatchCommand(REMOVE_HIGHLIGHT_COMMAND);
|
|
1532
1626
|
}
|
|
1533
1627
|
|
|
1534
1628
|
dispatchLink(url) {
|
|
@@ -1593,41 +1687,38 @@ class CommandDispatcher {
|
|
|
1593
1687
|
|
|
1594
1688
|
dispatchInsertHorizontalDivider() {
|
|
1595
1689
|
this.contents.insertAtCursorEnsuringLineBelow(new HorizontalDividerNode());
|
|
1596
|
-
|
|
1597
1690
|
this.editor.focus();
|
|
1598
1691
|
}
|
|
1599
1692
|
|
|
1600
1693
|
dispatchRotateHeadingFormat() {
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
if (!$isRangeSelection(selection)) return
|
|
1604
|
-
|
|
1605
|
-
if ($isRootOrShadowRoot(selection.anchor.getNode())) {
|
|
1606
|
-
selection.insertNodes([ $createHeadingNode("h2") ]);
|
|
1607
|
-
return
|
|
1608
|
-
}
|
|
1694
|
+
const selection = $getSelection();
|
|
1695
|
+
if (!$isRangeSelection(selection)) return
|
|
1609
1696
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
if (currentTag === "h2") {
|
|
1615
|
-
nextTag = "h3";
|
|
1616
|
-
} else if (currentTag === "h3") {
|
|
1617
|
-
nextTag = "h4";
|
|
1618
|
-
} else if (currentTag === "h4") {
|
|
1619
|
-
nextTag = null;
|
|
1620
|
-
} else {
|
|
1621
|
-
nextTag = "h2";
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1697
|
+
if ($isRootOrShadowRoot(selection.anchor.getNode())) {
|
|
1698
|
+
selection.insertNodes([ $createHeadingNode("h2") ]);
|
|
1699
|
+
return
|
|
1700
|
+
}
|
|
1624
1701
|
|
|
1625
|
-
|
|
1626
|
-
|
|
1702
|
+
const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow();
|
|
1703
|
+
let nextTag = "h2";
|
|
1704
|
+
if ($isHeadingNode(topLevelElement)) {
|
|
1705
|
+
const currentTag = topLevelElement.getTag();
|
|
1706
|
+
if (currentTag === "h2") {
|
|
1707
|
+
nextTag = "h3";
|
|
1708
|
+
} else if (currentTag === "h3") {
|
|
1709
|
+
nextTag = "h4";
|
|
1710
|
+
} else if (currentTag === "h4") {
|
|
1711
|
+
nextTag = null;
|
|
1627
1712
|
} else {
|
|
1628
|
-
|
|
1713
|
+
nextTag = "h2";
|
|
1629
1714
|
}
|
|
1630
|
-
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
if (nextTag) {
|
|
1718
|
+
this.contents.insertNodeWrappingEachSelectedLine(() => $createHeadingNode(nextTag));
|
|
1719
|
+
} else {
|
|
1720
|
+
this.contents.removeFormattingFromSelectedLines();
|
|
1721
|
+
}
|
|
1631
1722
|
}
|
|
1632
1723
|
|
|
1633
1724
|
dispatchUploadAttachments() {
|
|
@@ -1796,7 +1887,6 @@ class Selection {
|
|
|
1796
1887
|
|
|
1797
1888
|
this.#listenForNodeSelections();
|
|
1798
1889
|
this.#processSelectionChangeCommands();
|
|
1799
|
-
this.#handleInputWhenDecoratorNodesSelected();
|
|
1800
1890
|
this.#containEditorFocus();
|
|
1801
1891
|
}
|
|
1802
1892
|
|
|
@@ -1807,12 +1897,10 @@ class Selection {
|
|
|
1807
1897
|
}
|
|
1808
1898
|
|
|
1809
1899
|
get hasNodeSelection() {
|
|
1810
|
-
|
|
1811
|
-
this.editor.getEditorState().read(() => {
|
|
1900
|
+
return this.editor.getEditorState().read(() => {
|
|
1812
1901
|
const selection = $getSelection();
|
|
1813
|
-
|
|
1814
|
-
})
|
|
1815
|
-
return result
|
|
1902
|
+
return selection !== null && $isNodeSelection(selection)
|
|
1903
|
+
})
|
|
1816
1904
|
}
|
|
1817
1905
|
|
|
1818
1906
|
get cursorPosition() {
|
|
@@ -1890,6 +1978,36 @@ class Selection {
|
|
|
1890
1978
|
}
|
|
1891
1979
|
}
|
|
1892
1980
|
|
|
1981
|
+
getFormat() {
|
|
1982
|
+
const selection = $getSelection();
|
|
1983
|
+
if (!$isRangeSelection(selection)) return {}
|
|
1984
|
+
|
|
1985
|
+
const anchorNode = selection.anchor.getNode();
|
|
1986
|
+
if (!anchorNode.getParent()) return {}
|
|
1987
|
+
|
|
1988
|
+
const topLevelElement = anchorNode.getTopLevelElementOrThrow();
|
|
1989
|
+
const listType = getListType(anchorNode);
|
|
1990
|
+
|
|
1991
|
+
return {
|
|
1992
|
+
isBold: selection.hasFormat("bold"),
|
|
1993
|
+
isItalic: selection.hasFormat("italic"),
|
|
1994
|
+
isStrikethrough: selection.hasFormat("strikethrough"),
|
|
1995
|
+
isHighlight: isSelectionHighlighted(selection),
|
|
1996
|
+
isInLink: $getNearestNodeOfType(anchorNode, LinkNode) !== null,
|
|
1997
|
+
isInQuote: $isQuoteNode(topLevelElement),
|
|
1998
|
+
isInHeading: $isHeadingNode(topLevelElement),
|
|
1999
|
+
isInCode: selection.hasFormat("code") || $getNearestNodeOfType(anchorNode, CodeNode) !== null,
|
|
2000
|
+
isInList: listType !== null,
|
|
2001
|
+
listType,
|
|
2002
|
+
isInTable: $getTableCellNodeFromLexicalNode(anchorNode) !== null
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
nearestNodeOfType(nodeType) {
|
|
2007
|
+
const anchorNode = $getSelection()?.anchor?.getNode();
|
|
2008
|
+
return $getNearestNodeOfType(anchorNode, nodeType)
|
|
2009
|
+
}
|
|
2010
|
+
|
|
1893
2011
|
get hasSelectedWordsInSingleLine() {
|
|
1894
2012
|
const selection = $getSelection();
|
|
1895
2013
|
if (!$isRangeSelection(selection)) return false
|
|
@@ -1917,42 +2035,20 @@ class Selection {
|
|
|
1917
2035
|
}
|
|
1918
2036
|
|
|
1919
2037
|
get isInsideList() {
|
|
1920
|
-
|
|
1921
|
-
if (!$isRangeSelection(selection)) return false
|
|
1922
|
-
|
|
1923
|
-
const anchorNode = selection.anchor.getNode();
|
|
1924
|
-
return getNearestListItemNode(anchorNode) !== null
|
|
2038
|
+
return this.nearestNodeOfType(ListItemNode)
|
|
1925
2039
|
}
|
|
1926
2040
|
|
|
1927
2041
|
get isIndentedList() {
|
|
1928
|
-
const
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
const nodes = selection.getNodes();
|
|
1932
|
-
for (const node of nodes) {
|
|
1933
|
-
const closestListNode = $getNearestNodeOfType(node, ListNode);
|
|
1934
|
-
if (closestListNode && $getListDepth(closestListNode) > 1) {
|
|
1935
|
-
return true
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
return false
|
|
2042
|
+
const closestListNode = this.nearestNodeOfType(ListNode);
|
|
2043
|
+
return closestListNode && ($getListDepth(closestListNode) > 1)
|
|
1940
2044
|
}
|
|
1941
2045
|
|
|
1942
2046
|
get isInsideCodeBlock() {
|
|
1943
|
-
|
|
1944
|
-
if (!$isRangeSelection(selection)) return false
|
|
1945
|
-
|
|
1946
|
-
const anchorNode = selection.anchor.getNode();
|
|
1947
|
-
return $getNearestNodeOfType(anchorNode, CodeNode) !== null
|
|
2047
|
+
return this.nearestNodeOfType(CodeNode) !== null
|
|
1948
2048
|
}
|
|
1949
2049
|
|
|
1950
2050
|
get isTableCellSelected() {
|
|
1951
|
-
|
|
1952
|
-
if (!$isRangeSelection(selection)) return false
|
|
1953
|
-
|
|
1954
|
-
const anchorNode = selection.anchor.getNode();
|
|
1955
|
-
return $getNearestNodeOfType(anchorNode, TableCellNode) !== null
|
|
2051
|
+
return this.nearestNodeOfType(TableCellNode) !== null
|
|
1956
2052
|
}
|
|
1957
2053
|
|
|
1958
2054
|
get nodeAfterCursor() {
|
|
@@ -2015,10 +2111,6 @@ class Selection {
|
|
|
2015
2111
|
return this.#findPreviousSiblingUp(anchorNode)
|
|
2016
2112
|
}
|
|
2017
2113
|
|
|
2018
|
-
get #contents() {
|
|
2019
|
-
return this.editorElement.contents
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
2114
|
get #currentlySelectedKeys() {
|
|
2023
2115
|
if (this.currentlySelectedKeys) { return this.currentlySelectedKeys }
|
|
2024
2116
|
|
|
@@ -2040,84 +2132,24 @@ class Selection {
|
|
|
2040
2132
|
this.editor.registerCommand(KEY_ARROW_UP_COMMAND, this.#selectPreviousTopLevelNode.bind(this), COMMAND_PRIORITY_LOW);
|
|
2041
2133
|
this.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, this.#selectNextTopLevelNode.bind(this), COMMAND_PRIORITY_LOW);
|
|
2042
2134
|
|
|
2043
|
-
this.editor.registerCommand(
|
|
2044
|
-
this.editor.registerCommand(KEY_BACKSPACE_COMMAND, this.#deletePreviousOrNext.bind(this), COMMAND_PRIORITY_LOW);
|
|
2135
|
+
this.editor.registerCommand(DELETE_CHARACTER_COMMAND, this.#selectDecoratorNodeBeforeDeletion.bind(this), COMMAND_PRIORITY_LOW);
|
|
2045
2136
|
|
|
2046
2137
|
this.editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
|
|
2047
2138
|
this.current = $getSelection();
|
|
2048
|
-
}, COMMAND_PRIORITY_LOW);
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
#listenForNodeSelections() {
|
|
2052
|
-
this.editor.getRootElement().addEventListener("lexxy:internal:select-node", async (event) => {
|
|
2053
|
-
await nextFrame();
|
|
2054
|
-
|
|
2055
|
-
const { key } = event.detail;
|
|
2056
|
-
this.editor.update(() => {
|
|
2057
|
-
const node = $getNodeByKey(key);
|
|
2058
|
-
if (node) {
|
|
2059
|
-
const selection = $createNodeSelection();
|
|
2060
|
-
selection.add(node.getKey());
|
|
2061
|
-
$setSelection(selection);
|
|
2062
|
-
}
|
|
2063
|
-
this.editor.focus();
|
|
2064
|
-
});
|
|
2065
|
-
});
|
|
2066
|
-
|
|
2067
|
-
this.editor.getRootElement().addEventListener("lexxy:internal:move-to-next-line", (event) => {
|
|
2068
|
-
this.#selectOrAppendNextLine();
|
|
2069
|
-
});
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
// In Safari, when the only node in the document is an attachment, it won't let you enter text
|
|
2073
|
-
// before/below it. There is probably a better fix here, but this workaround solves the problem until
|
|
2074
|
-
// we find it.
|
|
2075
|
-
#handleInputWhenDecoratorNodesSelected() {
|
|
2076
|
-
this.editor.getRootElement().addEventListener("keydown", (event) => {
|
|
2077
|
-
if (isPrintableCharacter(event)) {
|
|
2078
|
-
this.editor.update(() => {
|
|
2079
|
-
const selection = $getSelection();
|
|
2080
|
-
|
|
2081
|
-
if ($isRangeSelection(selection) && selection.isCollapsed()) {
|
|
2082
|
-
const anchorNode = selection.anchor.getNode();
|
|
2083
|
-
const offset = selection.anchor.offset;
|
|
2084
|
-
|
|
2085
|
-
const nodeBefore = this.#getNodeBeforePosition(anchorNode, offset);
|
|
2086
|
-
const nodeAfter = this.#getNodeAfterPosition(anchorNode, offset);
|
|
2087
|
-
|
|
2088
|
-
if (nodeBefore instanceof DecoratorNode && !nodeBefore.isInline()) {
|
|
2089
|
-
event.preventDefault();
|
|
2090
|
-
this.#contents.createParagraphAfterNode(nodeBefore, event.key);
|
|
2091
|
-
return
|
|
2092
|
-
} else if (nodeAfter instanceof DecoratorNode && !nodeAfter.isInline()) {
|
|
2093
|
-
event.preventDefault();
|
|
2094
|
-
this.#contents.createParagraphBeforeNode(nodeAfter, event.key);
|
|
2095
|
-
return
|
|
2096
|
-
}
|
|
2097
|
-
}
|
|
2098
|
-
});
|
|
2099
|
-
}
|
|
2100
|
-
}, true);
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
#getNodeBeforePosition(node, offset) {
|
|
2104
|
-
if ($isTextNode(node) && offset === 0) {
|
|
2105
|
-
return node.getPreviousSibling()
|
|
2106
|
-
}
|
|
2107
|
-
if ($isElementNode(node) && offset > 0) {
|
|
2108
|
-
return node.getChildAtIndex(offset - 1)
|
|
2109
|
-
}
|
|
2110
|
-
return null
|
|
2139
|
+
}, COMMAND_PRIORITY_LOW);
|
|
2111
2140
|
}
|
|
2112
2141
|
|
|
2113
|
-
#
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
return
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2142
|
+
#listenForNodeSelections() {
|
|
2143
|
+
this.editor.registerCommand(CLICK_COMMAND, ({ target }) => {
|
|
2144
|
+
if (!isDOMNode(target)) return false
|
|
2145
|
+
|
|
2146
|
+
const targetNode = $getNearestNodeFromDOMNode(target);
|
|
2147
|
+
return $isDecoratorNode(targetNode) && this.#selectInLexical(targetNode)
|
|
2148
|
+
}, COMMAND_PRIORITY_LOW);
|
|
2149
|
+
|
|
2150
|
+
this.editor.getRootElement().addEventListener("lexxy:internal:move-to-next-line", (event) => {
|
|
2151
|
+
this.#selectOrAppendNextLine();
|
|
2152
|
+
});
|
|
2121
2153
|
}
|
|
2122
2154
|
|
|
2123
2155
|
#containEditorFocus() {
|
|
@@ -2184,33 +2216,33 @@ class Selection {
|
|
|
2184
2216
|
|
|
2185
2217
|
async #selectPreviousNode() {
|
|
2186
2218
|
if (this.hasNodeSelection) {
|
|
2187
|
-
await this.#withCurrentNode((currentNode) => currentNode.selectPrevious())
|
|
2219
|
+
return await this.#withCurrentNode((currentNode) => currentNode.selectPrevious())
|
|
2188
2220
|
} else {
|
|
2189
|
-
this.#selectInLexical(this.nodeBeforeCursor)
|
|
2221
|
+
return this.#selectInLexical(this.nodeBeforeCursor)
|
|
2190
2222
|
}
|
|
2191
2223
|
}
|
|
2192
2224
|
|
|
2193
2225
|
async #selectNextNode() {
|
|
2194
2226
|
if (this.hasNodeSelection) {
|
|
2195
|
-
await this.#withCurrentNode((currentNode) => currentNode.selectNext(0, 0))
|
|
2227
|
+
return await this.#withCurrentNode((currentNode) => currentNode.selectNext(0, 0))
|
|
2196
2228
|
} else {
|
|
2197
|
-
this.#selectInLexical(this.nodeAfterCursor)
|
|
2229
|
+
return this.#selectInLexical(this.nodeAfterCursor)
|
|
2198
2230
|
}
|
|
2199
2231
|
}
|
|
2200
2232
|
|
|
2201
2233
|
async #selectPreviousTopLevelNode() {
|
|
2202
2234
|
if (this.hasNodeSelection) {
|
|
2203
|
-
await this.#withCurrentNode((currentNode) => currentNode.selectPrevious())
|
|
2235
|
+
return await this.#withCurrentNode((currentNode) => currentNode.getTopLevelElement().selectPrevious())
|
|
2204
2236
|
} else {
|
|
2205
|
-
this.#selectInLexical(this.topLevelNodeBeforeCursor)
|
|
2237
|
+
return this.#selectInLexical(this.topLevelNodeBeforeCursor)
|
|
2206
2238
|
}
|
|
2207
2239
|
}
|
|
2208
2240
|
|
|
2209
2241
|
async #selectNextTopLevelNode() {
|
|
2210
2242
|
if (this.hasNodeSelection) {
|
|
2211
|
-
await this.#withCurrentNode((currentNode) => currentNode.selectNext(0, 0))
|
|
2243
|
+
return await this.#withCurrentNode((currentNode) => currentNode.getTopLevelElement().selectNext(0, 0))
|
|
2212
2244
|
} else {
|
|
2213
|
-
this.#selectInLexical(this.topLevelNodeAfterCursor)
|
|
2245
|
+
return this.#selectInLexical(this.topLevelNodeAfterCursor)
|
|
2214
2246
|
}
|
|
2215
2247
|
}
|
|
2216
2248
|
|
|
@@ -2276,37 +2308,24 @@ class Selection {
|
|
|
2276
2308
|
}
|
|
2277
2309
|
|
|
2278
2310
|
#selectInLexical(node) {
|
|
2279
|
-
if (
|
|
2280
|
-
|
|
2281
|
-
this.editor.update(() => {
|
|
2282
|
-
const selection = $createNodeSelection();
|
|
2283
|
-
selection.add(node.getKey());
|
|
2311
|
+
if ($isDecoratorNode(node)) {
|
|
2312
|
+
const selection = $createNodeSelectionWith(node);
|
|
2284
2313
|
$setSelection(selection);
|
|
2285
|
-
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
#deleteSelectedOrNext() {
|
|
2289
|
-
const node = this.nodeAfterCursor;
|
|
2290
|
-
if (node instanceof DecoratorNode) {
|
|
2291
|
-
this.#selectInLexical(node);
|
|
2292
|
-
return true
|
|
2314
|
+
return selection
|
|
2293
2315
|
} else {
|
|
2294
|
-
|
|
2316
|
+
return false
|
|
2295
2317
|
}
|
|
2296
|
-
|
|
2297
|
-
return false
|
|
2298
2318
|
}
|
|
2299
2319
|
|
|
2300
|
-
#
|
|
2301
|
-
const node = this.nodeBeforeCursor;
|
|
2320
|
+
#selectDecoratorNodeBeforeDeletion(backwards) {
|
|
2321
|
+
const node = backwards ? this.nodeBeforeCursor : this.nodeAfterCursor;
|
|
2302
2322
|
if (node instanceof DecoratorNode) {
|
|
2303
2323
|
this.#selectInLexical(node);
|
|
2324
|
+
|
|
2304
2325
|
return true
|
|
2305
2326
|
} else {
|
|
2306
|
-
|
|
2327
|
+
return false
|
|
2307
2328
|
}
|
|
2308
|
-
|
|
2309
|
-
return false
|
|
2310
2329
|
}
|
|
2311
2330
|
|
|
2312
2331
|
#getValidSelectionRange() {
|
|
@@ -2600,17 +2619,13 @@ class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
|
2600
2619
|
createDOM() {
|
|
2601
2620
|
const figure = createElement(this.tagName, { "content-type": this.contentType, "data-lexxy-decorator": true });
|
|
2602
2621
|
|
|
2603
|
-
figure.addEventListener("click", (event) => {
|
|
2604
|
-
dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() });
|
|
2605
|
-
});
|
|
2606
|
-
|
|
2607
2622
|
figure.insertAdjacentHTML("beforeend", this.innerHtml);
|
|
2608
2623
|
|
|
2609
2624
|
return figure
|
|
2610
2625
|
}
|
|
2611
2626
|
|
|
2612
2627
|
updateDOM() {
|
|
2613
|
-
return
|
|
2628
|
+
return false
|
|
2614
2629
|
}
|
|
2615
2630
|
|
|
2616
2631
|
getTextContent() {
|
|
@@ -2946,20 +2961,18 @@ class Contents {
|
|
|
2946
2961
|
}
|
|
2947
2962
|
|
|
2948
2963
|
insertAtCursor(node) {
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
const selectedNodes = selection?.getNodes();
|
|
2964
|
+
const selection = $getSelection();
|
|
2965
|
+
const selectedNodes = selection?.getNodes();
|
|
2952
2966
|
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
});
|
|
2967
|
+
if ($isRangeSelection(selection)) {
|
|
2968
|
+
$insertNodes([ node ]);
|
|
2969
|
+
} else if ($isNodeSelection(selection) && selectedNodes && selectedNodes.length > 0) {
|
|
2970
|
+
const lastNode = selectedNodes.at(-1);
|
|
2971
|
+
lastNode.insertAfter(node);
|
|
2972
|
+
} else {
|
|
2973
|
+
const root = $getRoot();
|
|
2974
|
+
root.append(node);
|
|
2975
|
+
}
|
|
2963
2976
|
}
|
|
2964
2977
|
|
|
2965
2978
|
insertAtCursorEnsuringLineBelow(node) {
|
|
@@ -3187,27 +3200,6 @@ class Contents {
|
|
|
3187
3200
|
}, { tag: HISTORY_MERGE_TAG });
|
|
3188
3201
|
}
|
|
3189
3202
|
|
|
3190
|
-
async deleteSelectedNodes() {
|
|
3191
|
-
let focusNode = null;
|
|
3192
|
-
|
|
3193
|
-
this.editor.update(() => {
|
|
3194
|
-
if (this.#selection.hasNodeSelection) {
|
|
3195
|
-
const nodesToRemove = $getSelection().getNodes();
|
|
3196
|
-
if (nodesToRemove.length === 0) return
|
|
3197
|
-
|
|
3198
|
-
focusNode = this.#findAdjacentNodeTo(nodesToRemove);
|
|
3199
|
-
this.#deleteNodes(nodesToRemove);
|
|
3200
|
-
}
|
|
3201
|
-
});
|
|
3202
|
-
|
|
3203
|
-
await nextFrame();
|
|
3204
|
-
|
|
3205
|
-
this.editor.update(() => {
|
|
3206
|
-
this.#selectAfterDeletion(focusNode);
|
|
3207
|
-
this.editor.focus();
|
|
3208
|
-
});
|
|
3209
|
-
}
|
|
3210
|
-
|
|
3211
3203
|
replaceNodeWithHTML(nodeKey, html, options = {}) {
|
|
3212
3204
|
this.editor.update(() => {
|
|
3213
3205
|
const node = $getNodeByKey(nodeKey);
|
|
@@ -3246,10 +3238,6 @@ class Contents {
|
|
|
3246
3238
|
});
|
|
3247
3239
|
}
|
|
3248
3240
|
|
|
3249
|
-
get #selection() {
|
|
3250
|
-
return this.editorElement.selection
|
|
3251
|
-
}
|
|
3252
|
-
|
|
3253
3241
|
#insertLineBelowIfLastNode(node) {
|
|
3254
3242
|
this.editor.update(() => {
|
|
3255
3243
|
const nextSibling = node.getNextSibling();
|
|
@@ -3446,52 +3434,13 @@ class Contents {
|
|
|
3446
3434
|
nodesToDelete.forEach((node) => node.remove());
|
|
3447
3435
|
}
|
|
3448
3436
|
|
|
3449
|
-
#deleteNodes(nodes) {
|
|
3450
|
-
// Use splice() instead of node.remove() for proper removal and
|
|
3451
|
-
// reconciliation. Would have issues with removing unintended decorator nodes
|
|
3452
|
-
// with node.remove()
|
|
3453
|
-
nodes.forEach((node) => {
|
|
3454
|
-
const parent = node.getParent();
|
|
3455
|
-
if (!$isElementNode(parent)) return
|
|
3456
|
-
|
|
3457
|
-
const children = parent.getChildren();
|
|
3458
|
-
const index = children.indexOf(node);
|
|
3459
|
-
|
|
3460
|
-
if (index >= 0) {
|
|
3461
|
-
parent.splice(index, 1, []);
|
|
3462
|
-
}
|
|
3463
|
-
});
|
|
3464
|
-
}
|
|
3465
|
-
|
|
3466
|
-
#findAdjacentNodeTo(nodes) {
|
|
3467
|
-
const firstNode = nodes[0];
|
|
3468
|
-
const lastNode = nodes[nodes.length - 1];
|
|
3469
|
-
|
|
3470
|
-
return firstNode?.getPreviousSibling() || lastNode?.getNextSibling()
|
|
3471
|
-
}
|
|
3472
|
-
|
|
3473
|
-
#selectAfterDeletion(focusNode) {
|
|
3474
|
-
const root = $getRoot();
|
|
3475
|
-
if (root.getChildrenSize() === 0) {
|
|
3476
|
-
const newParagraph = $createParagraphNode();
|
|
3477
|
-
root.append(newParagraph);
|
|
3478
|
-
newParagraph.selectStart();
|
|
3479
|
-
} else if (focusNode) {
|
|
3480
|
-
if ($isTextNode(focusNode) || $isParagraphNode(focusNode)) {
|
|
3481
|
-
focusNode.selectEnd();
|
|
3482
|
-
} else {
|
|
3483
|
-
focusNode.selectNext(0, 0);
|
|
3484
|
-
}
|
|
3485
|
-
}
|
|
3486
|
-
}
|
|
3487
|
-
|
|
3488
3437
|
#collectSelectedListItems(selection) {
|
|
3489
3438
|
const nodes = selection.getNodes();
|
|
3490
3439
|
const listItems = new Set();
|
|
3491
3440
|
const parentLists = new Set();
|
|
3492
3441
|
|
|
3493
3442
|
for (const node of nodes) {
|
|
3494
|
-
const listItem =
|
|
3443
|
+
const listItem = $getNearestNodeOfType(node, ListItemNode);
|
|
3495
3444
|
if (listItem) {
|
|
3496
3445
|
listItems.add(listItem);
|
|
3497
3446
|
const parentList = listItem.getParent();
|
|
@@ -3807,8 +3756,16 @@ class Extensions {
|
|
|
3807
3756
|
return this.lexxyElement.toolbar
|
|
3808
3757
|
}
|
|
3809
3758
|
|
|
3759
|
+
get #baseExtensions() {
|
|
3760
|
+
return this.lexxyElement.baseExtensions
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
get #configuredExtensions() {
|
|
3764
|
+
return Lexxy.global.get("extensions")
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3810
3767
|
#initializeExtensions() {
|
|
3811
|
-
const extensionDefinitions =
|
|
3768
|
+
const extensionDefinitions = this.#baseExtensions.concat(this.#configuredExtensions);
|
|
3812
3769
|
|
|
3813
3770
|
return extensionDefinitions.map(
|
|
3814
3771
|
extension => new extension(this.lexxyElement)
|
|
@@ -3816,156 +3773,175 @@ class Extensions {
|
|
|
3816
3773
|
}
|
|
3817
3774
|
}
|
|
3818
3775
|
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
color: { buttons: [], permit: [] },
|
|
3830
|
-
"background-color": { buttons: [], permit: [] }
|
|
3831
|
-
},
|
|
3832
|
-
html: {
|
|
3833
|
-
import: {
|
|
3834
|
-
mark: $markConversion
|
|
3835
|
-
}
|
|
3836
|
-
},
|
|
3837
|
-
register(editor, config) {
|
|
3838
|
-
const canonicalizers = buildCanonicalizers(config);
|
|
3839
|
-
|
|
3840
|
-
editor.registerCommand(TOGGLE_HIGHLIGHT_COMMAND, $toggleSelectionStyles, COMMAND_PRIORITY_NORMAL);
|
|
3841
|
-
editor.registerNodeTransform(TextNode, $syncHighlightWithStyle);
|
|
3842
|
-
editor.registerNodeTransform(TextNode, (textNode) => $canonicalizePastedStyles(textNode, canonicalizers));
|
|
3776
|
+
class ProvisionalParagraphNode extends ParagraphNode {
|
|
3777
|
+
$config() {
|
|
3778
|
+
return this.config("provisonal_paragraph", {
|
|
3779
|
+
extends: ParagraphNode,
|
|
3780
|
+
importDOM: () => null,
|
|
3781
|
+
$transform: (node) => {
|
|
3782
|
+
node.concretizeIfEdited(node);
|
|
3783
|
+
node.removeUnlessRequired(node);
|
|
3784
|
+
}
|
|
3785
|
+
})
|
|
3843
3786
|
}
|
|
3844
|
-
});
|
|
3845
3787
|
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
};
|
|
3788
|
+
static neededBetween(nodeBefore, nodeAfter) {
|
|
3789
|
+
return !$isSelectableElement(nodeBefore, "next")
|
|
3790
|
+
&& !$isSelectableElement(nodeAfter, "previous")
|
|
3791
|
+
}
|
|
3851
3792
|
|
|
3852
|
-
|
|
3853
|
-
|
|
3793
|
+
createDOM(editor) {
|
|
3794
|
+
const p = super.createDOM(editor);
|
|
3795
|
+
const selected = this.isSelected($getSelection());
|
|
3796
|
+
p.classList.add("provisional-paragraph");
|
|
3797
|
+
p.classList.toggle("hidden", !selected);
|
|
3798
|
+
return p
|
|
3799
|
+
}
|
|
3854
3800
|
|
|
3855
|
-
|
|
3856
|
-
|
|
3801
|
+
updateDOM(_prevNode, dom) {
|
|
3802
|
+
const selected = this.isSelected($getSelection());
|
|
3803
|
+
dom.classList.toggle("hidden", !selected);
|
|
3804
|
+
return false
|
|
3857
3805
|
}
|
|
3858
|
-
}
|
|
3859
3806
|
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
conversion: extendTextNodeConversion("mark", $applyHighlightStyle),
|
|
3863
|
-
priority: 1
|
|
3807
|
+
getTextContent() {
|
|
3808
|
+
return ""
|
|
3864
3809
|
}
|
|
3865
|
-
}
|
|
3866
3810
|
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
}
|
|
3811
|
+
exportDOM() {
|
|
3812
|
+
return {
|
|
3813
|
+
element: null
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3873
3816
|
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3817
|
+
// override as Lexical has an interesting view of collapsed selection in ElementNodes
|
|
3818
|
+
// https://github.com/facebook/lexical/blob/f1e4f66014377b1f2595aec2b0ee17f5b7ef4dfc/packages/lexical/src/LexicalNode.ts#L646
|
|
3819
|
+
isSelected(selection = null) {
|
|
3820
|
+
const targetSelection = selection || $getSelection();
|
|
3821
|
+
return targetSelection?.getNodes().some(node => node.is(this) || this.isParentOf(node))
|
|
3822
|
+
}
|
|
3877
3823
|
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
const oldValue = $getSelectionStyleValueForProperty(selection, property);
|
|
3881
|
-
patch[property] = toggleOrReplace(oldValue, styles[property]);
|
|
3824
|
+
removeUnlessRequired(self = this.getLatest()) {
|
|
3825
|
+
if (!self.required) self.remove();
|
|
3882
3826
|
}
|
|
3883
3827
|
|
|
3884
|
-
|
|
3885
|
-
|
|
3828
|
+
concretizeIfEdited(self = this.getLatest()) {
|
|
3829
|
+
if (self.getTextContentSize() > 0) {
|
|
3830
|
+
self.replace($createParagraphNode(), true);
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3886
3833
|
|
|
3887
|
-
function toggleOrReplace(oldValue, newValue) {
|
|
3888
|
-
return oldValue === newValue ? null : newValue
|
|
3889
|
-
}
|
|
3890
3834
|
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
textNode.toggleFormat("highlight");
|
|
3835
|
+
get required() {
|
|
3836
|
+
return this.isDirectRootChild && ProvisionalParagraphNode.neededBetween(...this.immediateSiblings)
|
|
3894
3837
|
}
|
|
3895
|
-
}
|
|
3896
|
-
|
|
3897
|
-
function $canonicalizePastedStyles(textNode, canonicalizers = []) {
|
|
3898
|
-
if ($hasPastedStyles(textNode)) {
|
|
3899
|
-
$setPastedStyles(textNode, false);
|
|
3900
3838
|
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3839
|
+
get isDirectRootChild() {
|
|
3840
|
+
const parent = this.getParent();
|
|
3841
|
+
return $isRootOrShadowRoot(parent)
|
|
3842
|
+
}
|
|
3904
3843
|
|
|
3905
|
-
|
|
3844
|
+
get immediateSiblings() {
|
|
3845
|
+
return [ this.getPreviousSibling(), this.getNextSibling() ]
|
|
3906
3846
|
}
|
|
3907
3847
|
}
|
|
3908
3848
|
|
|
3909
|
-
function $
|
|
3910
|
-
|
|
3849
|
+
function $isProvisionalParagraphNode(node) {
|
|
3850
|
+
return node instanceof ProvisionalParagraphNode
|
|
3911
3851
|
}
|
|
3912
3852
|
|
|
3913
|
-
function $
|
|
3914
|
-
return $
|
|
3853
|
+
function $isSelectableElement(node, direction) {
|
|
3854
|
+
return $isElementNode(node) && (direction === "next" ? node.canInsertTextBefore() : node.canInsertTextAfter())
|
|
3915
3855
|
}
|
|
3916
3856
|
|
|
3917
|
-
class
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3857
|
+
class ProvisionalParagraphExtension extends LexxyExtension {
|
|
3858
|
+
get lexicalExtension() {
|
|
3859
|
+
return defineExtension({
|
|
3860
|
+
name: "lexxy/provisional-paragraph",
|
|
3861
|
+
nodes: [
|
|
3862
|
+
ProvisionalParagraphNode
|
|
3863
|
+
],
|
|
3864
|
+
register(editor) {
|
|
3865
|
+
return mergeRegister(
|
|
3866
|
+
// Process Provisional Paragraph Nodes on RootNode changes as sibling status influences whether
|
|
3867
|
+
// they are required and their visible/hidden status
|
|
3868
|
+
editor.registerNodeTransform(RootNode, $insertRequiredProvisionalParagraphs),
|
|
3869
|
+
editor.registerNodeTransform(RootNode, $removeUnneededProvisionalParagraphs),
|
|
3870
|
+
editor.registerCommand(SELECTION_CHANGE_COMMAND, $markAllProvisionalParagraphsDirty, COMMAND_PRIORITY_HIGH)
|
|
3871
|
+
)
|
|
3872
|
+
}
|
|
3873
|
+
})
|
|
3921
3874
|
}
|
|
3875
|
+
}
|
|
3922
3876
|
|
|
3923
|
-
|
|
3924
|
-
|
|
3877
|
+
function $insertRequiredProvisionalParagraphs(rootNode) {
|
|
3878
|
+
const firstNode = rootNode.getFirstChild();
|
|
3879
|
+
if (ProvisionalParagraphNode.neededBetween(null, firstNode)) {
|
|
3880
|
+
$insertFirst(rootNode, new ProvisionalParagraphNode);
|
|
3925
3881
|
}
|
|
3926
3882
|
|
|
3927
|
-
|
|
3928
|
-
|
|
3883
|
+
for (const node of $firstToLastIterator(rootNode)) {
|
|
3884
|
+
const nextNode = node.getNextSibling();
|
|
3885
|
+
if (ProvisionalParagraphNode.neededBetween(node, nextNode)) {
|
|
3886
|
+
node.insertAfter(new ProvisionalParagraphNode);
|
|
3887
|
+
}
|
|
3929
3888
|
}
|
|
3889
|
+
}
|
|
3930
3890
|
|
|
3931
|
-
|
|
3932
|
-
|
|
3891
|
+
function $removeUnneededProvisionalParagraphs(rootNode) {
|
|
3892
|
+
for (const provisionalParagraph of $getAllProvisionalParagraphs(rootNode)) {
|
|
3893
|
+
provisionalParagraph.removeUnlessRequired();
|
|
3933
3894
|
}
|
|
3895
|
+
}
|
|
3934
3896
|
|
|
3935
|
-
|
|
3936
|
-
|
|
3897
|
+
function $markAllProvisionalParagraphsDirty() {
|
|
3898
|
+
for (const provisionalParagraph of $getAllProvisionalParagraphs()) {
|
|
3899
|
+
provisionalParagraph.markDirty();
|
|
3937
3900
|
}
|
|
3938
3901
|
}
|
|
3939
3902
|
|
|
3903
|
+
function $getAllProvisionalParagraphs(rootNode = $getRoot()) {
|
|
3904
|
+
return $descendantsMatching(rootNode.getChildren(), $isProvisionalParagraphNode)
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3940
3907
|
const TRIX_LANGUAGE_ATTR = "language";
|
|
3941
3908
|
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
em: (element) => onlyStyledElements(element, {
|
|
3947
|
-
conversion: extendTextNodeConversion("i", $applyHighlightStyle),
|
|
3948
|
-
priority: 1
|
|
3949
|
-
}),
|
|
3950
|
-
span: (element) => onlyStyledElements(element, {
|
|
3951
|
-
conversion: extendTextNodeConversion("mark", $applyHighlightStyle),
|
|
3952
|
-
priority: 1
|
|
3953
|
-
}),
|
|
3954
|
-
strong: (element) => onlyStyledElements(element, {
|
|
3955
|
-
conversion: extendTextNodeConversion("b", $applyHighlightStyle),
|
|
3956
|
-
priority: 1
|
|
3957
|
-
}),
|
|
3958
|
-
del: () => ({
|
|
3959
|
-
conversion: extendTextNodeConversion("s", $applyStrikethrough, $applyHighlightStyle),
|
|
3960
|
-
priority: 1
|
|
3961
|
-
}),
|
|
3962
|
-
pre: (element) => onlyPreLanguageElements(element, {
|
|
3963
|
-
conversion: extendConversion(CodeNode, "pre", $applyLanguage),
|
|
3964
|
-
priority: 1
|
|
3965
|
-
})
|
|
3966
|
-
}
|
|
3909
|
+
class TrixContentExtension extends LexxyExtension {
|
|
3910
|
+
|
|
3911
|
+
get enabled() {
|
|
3912
|
+
return this.editorElement.supportsRichText
|
|
3967
3913
|
}
|
|
3968
|
-
|
|
3914
|
+
|
|
3915
|
+
get lexicalExtension() {
|
|
3916
|
+
return defineExtension({
|
|
3917
|
+
name: "lexxy/trix-content",
|
|
3918
|
+
html: {
|
|
3919
|
+
import: {
|
|
3920
|
+
em: (element) => onlyStyledElements(element, {
|
|
3921
|
+
conversion: extendTextNodeConversion("i", $applyHighlightStyle),
|
|
3922
|
+
priority: 1
|
|
3923
|
+
}),
|
|
3924
|
+
span: (element) => onlyStyledElements(element, {
|
|
3925
|
+
conversion: extendTextNodeConversion("mark", $applyHighlightStyle),
|
|
3926
|
+
priority: 1
|
|
3927
|
+
}),
|
|
3928
|
+
strong: (element) => onlyStyledElements(element, {
|
|
3929
|
+
conversion: extendTextNodeConversion("b", $applyHighlightStyle),
|
|
3930
|
+
priority: 1
|
|
3931
|
+
}),
|
|
3932
|
+
del: () => ({
|
|
3933
|
+
conversion: extendTextNodeConversion("s", $applyStrikethrough, $applyHighlightStyle),
|
|
3934
|
+
priority: 1
|
|
3935
|
+
}),
|
|
3936
|
+
pre: (element) => onlyPreLanguageElements(element, {
|
|
3937
|
+
conversion: extendConversion(CodeNode, "pre", $applyLanguage),
|
|
3938
|
+
priority: 1
|
|
3939
|
+
})
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
})
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3969
3945
|
|
|
3970
3946
|
function onlyStyledElements(element, conversion) {
|
|
3971
3947
|
const elementHighlighted = element.style.color !== "" || element.style.backgroundColor !== "";
|
|
@@ -3987,8 +3963,12 @@ function $applyLanguage(conversionOutput, element) {
|
|
|
3987
3963
|
}
|
|
3988
3964
|
|
|
3989
3965
|
class WrappedTableNode extends TableNode {
|
|
3990
|
-
|
|
3991
|
-
return
|
|
3966
|
+
$config() {
|
|
3967
|
+
return this.config("wrapped_table_node", { extends: TableNode })
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
static importDOM() {
|
|
3971
|
+
return super.importDOM()
|
|
3992
3972
|
}
|
|
3993
3973
|
|
|
3994
3974
|
exportDOM(editor) {
|
|
@@ -4010,99 +3990,106 @@ class WrappedTableNode extends TableNode {
|
|
|
4010
3990
|
}
|
|
4011
3991
|
}
|
|
4012
3992
|
|
|
4013
|
-
|
|
4014
|
-
name: "lexxy/tables",
|
|
4015
|
-
nodes: [
|
|
4016
|
-
WrappedTableNode,
|
|
4017
|
-
{
|
|
4018
|
-
replace: TableNode,
|
|
4019
|
-
with: () => new WrappedTableNode()
|
|
4020
|
-
},
|
|
4021
|
-
TableCellNode,
|
|
4022
|
-
TableRowNode
|
|
4023
|
-
],
|
|
4024
|
-
register(editor) {
|
|
4025
|
-
// Register Lexical table plugins
|
|
4026
|
-
registerTablePlugin(editor);
|
|
4027
|
-
registerTableSelectionObserver(editor, true);
|
|
4028
|
-
setScrollableTablesActive(editor, true);
|
|
4029
|
-
|
|
4030
|
-
// Bug fix: Prevent hardcoded background color (Lexical #8089)
|
|
4031
|
-
editor.registerNodeTransform(TableCellNode, (node) => {
|
|
4032
|
-
if (node.getBackgroundColor() === null) {
|
|
4033
|
-
node.setBackgroundColor("");
|
|
4034
|
-
}
|
|
4035
|
-
});
|
|
3993
|
+
class TablesExtension extends LexxyExtension {
|
|
4036
3994
|
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
3995
|
+
get enabled() {
|
|
3996
|
+
return this.editorElement.supportsRichText
|
|
3997
|
+
}
|
|
4040
3998
|
|
|
4041
|
-
|
|
3999
|
+
get lexicalExtension() {
|
|
4000
|
+
return defineExtension({
|
|
4001
|
+
name: "lexxy/tables",
|
|
4002
|
+
nodes: [
|
|
4003
|
+
WrappedTableNode,
|
|
4004
|
+
{
|
|
4005
|
+
replace: TableNode,
|
|
4006
|
+
with: () => new WrappedTableNode(),
|
|
4007
|
+
withKlass: WrappedTableNode
|
|
4008
|
+
},
|
|
4009
|
+
TableCellNode,
|
|
4010
|
+
TableRowNode
|
|
4011
|
+
],
|
|
4012
|
+
register(editor) {
|
|
4013
|
+
return mergeRegister(
|
|
4014
|
+
// Register Lexical table plugins
|
|
4015
|
+
registerTablePlugin(editor),
|
|
4016
|
+
registerTableSelectionObserver(editor, true),
|
|
4017
|
+
setScrollableTablesActive(editor, true),
|
|
4018
|
+
|
|
4019
|
+
// Bug fix: Prevent hardcoded background color (Lexical #8089)
|
|
4020
|
+
editor.registerNodeTransform(TableCellNode, (node) => {
|
|
4021
|
+
if (node.getBackgroundColor() === null) {
|
|
4022
|
+
node.setBackgroundColor("");
|
|
4023
|
+
}
|
|
4024
|
+
}),
|
|
4042
4025
|
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4026
|
+
// Bug fix: Fix column header states (Lexical #8090)
|
|
4027
|
+
editor.registerNodeTransform(TableCellNode, (node) => {
|
|
4028
|
+
const headerState = node.getHeaderStyles();
|
|
4046
4029
|
|
|
4047
|
-
|
|
4048
|
-
const cellIndex = rowParent.getChildren().indexOf(node);
|
|
4030
|
+
if (headerState !== TableCellHeaderStates.ROW) return
|
|
4049
4031
|
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
);
|
|
4032
|
+
const rowParent = node.getParent();
|
|
4033
|
+
const tableNode = rowParent?.getParent();
|
|
4034
|
+
if (!tableNode) return
|
|
4054
4035
|
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
return cell && cell.getHeaderStyles() !== TableCellHeaderStates.NO_STATUS
|
|
4058
|
-
});
|
|
4036
|
+
const rows = tableNode.getChildren();
|
|
4037
|
+
const cellIndex = rowParent.getChildren().indexOf(node);
|
|
4059
4038
|
|
|
4060
|
-
|
|
4039
|
+
const cellsInRow = rowParent.getChildren();
|
|
4040
|
+
const isHeaderRow = cellsInRow.every(cell =>
|
|
4041
|
+
cell.getHeaderStyles() !== TableCellHeaderStates.NO_STATUS
|
|
4042
|
+
);
|
|
4061
4043
|
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4044
|
+
const isHeaderColumn = rows.every(row => {
|
|
4045
|
+
const cell = row.getChildren()[cellIndex];
|
|
4046
|
+
return cell && cell.getHeaderStyles() !== TableCellHeaderStates.NO_STATUS
|
|
4047
|
+
});
|
|
4065
4048
|
|
|
4066
|
-
|
|
4067
|
-
newHeaderState |= TableCellHeaderStates.COLUMN;
|
|
4068
|
-
}
|
|
4049
|
+
let newHeaderState = TableCellHeaderStates.NO_STATUS;
|
|
4069
4050
|
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
}
|
|
4073
|
-
});
|
|
4051
|
+
if (isHeaderRow) newHeaderState |= TableCellHeaderStates.ROW;
|
|
4052
|
+
if (isHeaderColumn) newHeaderState |= TableCellHeaderStates.COLUMN;
|
|
4074
4053
|
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4054
|
+
if (newHeaderState !== headerState) {
|
|
4055
|
+
node.setHeaderStyles(newHeaderState, TableCellHeaderStates.BOTH);
|
|
4056
|
+
}
|
|
4057
|
+
}),
|
|
4078
4058
|
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4059
|
+
editor.registerCommand("insertTableRowAfter", () => {
|
|
4060
|
+
$insertTableRowAtSelection(true);
|
|
4061
|
+
}, COMMAND_PRIORITY_NORMAL),
|
|
4082
4062
|
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4063
|
+
editor.registerCommand("insertTableRowBefore", () => {
|
|
4064
|
+
$insertTableRowAtSelection(false);
|
|
4065
|
+
}, COMMAND_PRIORITY_NORMAL),
|
|
4086
4066
|
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4067
|
+
editor.registerCommand("insertTableColumnAfter", () => {
|
|
4068
|
+
$insertTableColumnAtSelection(true);
|
|
4069
|
+
}, COMMAND_PRIORITY_NORMAL),
|
|
4090
4070
|
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4071
|
+
editor.registerCommand("insertTableColumnBefore", () => {
|
|
4072
|
+
$insertTableColumnAtSelection(false);
|
|
4073
|
+
}, COMMAND_PRIORITY_NORMAL),
|
|
4094
4074
|
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4075
|
+
editor.registerCommand("deleteTableRow", () => {
|
|
4076
|
+
$deleteTableRowAtSelection();
|
|
4077
|
+
}, COMMAND_PRIORITY_NORMAL),
|
|
4098
4078
|
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4079
|
+
editor.registerCommand("deleteTableColumn", () => {
|
|
4080
|
+
$deleteTableColumnAtSelection();
|
|
4081
|
+
}, COMMAND_PRIORITY_NORMAL),
|
|
4082
|
+
|
|
4083
|
+
editor.registerCommand("deleteTable", () => {
|
|
4084
|
+
const selection = $getSelection();
|
|
4085
|
+
if (!$isRangeSelection(selection)) return false
|
|
4086
|
+
$findTableNode(selection.anchor.getNode())?.remove();
|
|
4087
|
+
}, COMMAND_PRIORITY_NORMAL)
|
|
4088
|
+
)
|
|
4089
|
+
}
|
|
4090
|
+
})
|
|
4104
4091
|
}
|
|
4105
|
-
}
|
|
4092
|
+
}
|
|
4106
4093
|
|
|
4107
4094
|
class LexicalEditorElement extends HTMLElement {
|
|
4108
4095
|
static formAssociated = true
|
|
@@ -4124,7 +4111,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
4124
4111
|
this.id ??= generateDomId("lexxy-editor");
|
|
4125
4112
|
this.config = new EditorConfiguration(this);
|
|
4126
4113
|
this.extensions = new Extensions(this);
|
|
4127
|
-
this.highlighter = new Highlighter(this);
|
|
4128
4114
|
|
|
4129
4115
|
this.editor = this.#createEditor();
|
|
4130
4116
|
|
|
@@ -4189,6 +4175,15 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
4189
4175
|
return this.toolbar
|
|
4190
4176
|
}
|
|
4191
4177
|
|
|
4178
|
+
get baseExtensions() {
|
|
4179
|
+
return [
|
|
4180
|
+
ProvisionalParagraphExtension,
|
|
4181
|
+
HighlightExtension,
|
|
4182
|
+
TrixContentExtension,
|
|
4183
|
+
TablesExtension
|
|
4184
|
+
]
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4192
4187
|
get directUploadUrl() {
|
|
4193
4188
|
return this.dataset.directUploadUrl
|
|
4194
4189
|
}
|
|
@@ -4271,23 +4266,33 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
4271
4266
|
|
|
4272
4267
|
#parseHtmlIntoLexicalNodes(html) {
|
|
4273
4268
|
if (!html) html = "<p></p>";
|
|
4274
|
-
const nodes = $generateNodesFromDOM(this.editor, parseHtml(
|
|
4269
|
+
const nodes = $generateNodesFromDOM(this.editor, parseHtml(`${html}`));
|
|
4275
4270
|
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4271
|
+
return nodes
|
|
4272
|
+
.map(this.#wrapTextNode)
|
|
4273
|
+
.map(this.#unwrapDecoratorNode)
|
|
4274
|
+
}
|
|
4279
4275
|
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4276
|
+
// Raw string values produce TextNodes which cannot be appended directly to the RootNode.
|
|
4277
|
+
// We wrap those in <p>
|
|
4278
|
+
#wrapTextNode(node) {
|
|
4279
|
+
if (!$isTextNode(node)) return node
|
|
4280
|
+
|
|
4281
|
+
const paragraph = $createParagraphNode();
|
|
4282
|
+
paragraph.append(node);
|
|
4283
|
+
return paragraph
|
|
4284
|
+
}
|
|
4285
|
+
|
|
4286
|
+
// Custom decorator block elements such as action-text-attachments get wrapped into <p> automatically by Lexical.
|
|
4287
|
+
// We unwrap those.
|
|
4288
|
+
#unwrapDecoratorNode(node) {
|
|
4289
|
+
if ($isParagraphNode(node) && node.getChildrenSize() === 1) {
|
|
4290
|
+
const child = node.getFirstChild();
|
|
4291
|
+
if ($isDecoratorNode(child) && !child.isInline()) {
|
|
4292
|
+
return child
|
|
4288
4293
|
}
|
|
4289
|
-
|
|
4290
|
-
|
|
4294
|
+
}
|
|
4295
|
+
return node
|
|
4291
4296
|
}
|
|
4292
4297
|
|
|
4293
4298
|
#initialize() {
|
|
@@ -4310,7 +4315,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
4310
4315
|
theme: theme,
|
|
4311
4316
|
nodes: this.#lexicalNodes
|
|
4312
4317
|
},
|
|
4313
|
-
...this
|
|
4318
|
+
...this.extensions.lexicalExtensions
|
|
4314
4319
|
);
|
|
4315
4320
|
|
|
4316
4321
|
editor.setRootElement(this.editorContentElement);
|
|
@@ -4318,23 +4323,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
4318
4323
|
return editor
|
|
4319
4324
|
}
|
|
4320
4325
|
|
|
4321
|
-
get #lexicalExtensions() {
|
|
4322
|
-
const extensions = [];
|
|
4323
|
-
const richTextExtensions = [
|
|
4324
|
-
this.highlighter.lexicalExtension,
|
|
4325
|
-
TrixContentExtension,
|
|
4326
|
-
TablesLexicalExtension
|
|
4327
|
-
];
|
|
4328
|
-
|
|
4329
|
-
if (this.supportsRichText) {
|
|
4330
|
-
extensions.push(...richTextExtensions);
|
|
4331
|
-
}
|
|
4332
|
-
|
|
4333
|
-
extensions.push(...this.extensions.lexicalExtensions);
|
|
4334
|
-
|
|
4335
|
-
return extensions
|
|
4336
|
-
}
|
|
4337
|
-
|
|
4338
4326
|
get #lexicalNodes() {
|
|
4339
4327
|
const nodes = [ CustomActionTextAttachmentNode ];
|
|
4340
4328
|
|
|
@@ -4540,6 +4528,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
4540
4528
|
#attachToolbar() {
|
|
4541
4529
|
if (this.#hasToolbar) {
|
|
4542
4530
|
this.toolbarElement.setEditor(this);
|
|
4531
|
+
this.extensions.initializeToolbars();
|
|
4543
4532
|
}
|
|
4544
4533
|
}
|
|
4545
4534
|
|
|
@@ -6274,35 +6263,6 @@ function defineElements() {
|
|
|
6274
6263
|
});
|
|
6275
6264
|
}
|
|
6276
6265
|
|
|
6277
|
-
class LexxyExtension {
|
|
6278
|
-
#editorElement
|
|
6279
|
-
|
|
6280
|
-
constructor(editorElement) {
|
|
6281
|
-
this.#editorElement = editorElement;
|
|
6282
|
-
}
|
|
6283
|
-
|
|
6284
|
-
get editorElement() {
|
|
6285
|
-
return this.#editorElement
|
|
6286
|
-
}
|
|
6287
|
-
|
|
6288
|
-
get editorConfig() {
|
|
6289
|
-
return this.#editorElement.config
|
|
6290
|
-
}
|
|
6291
|
-
|
|
6292
|
-
// optional: defaults to true
|
|
6293
|
-
get enabled() {
|
|
6294
|
-
return true
|
|
6295
|
-
}
|
|
6296
|
-
|
|
6297
|
-
get lexicalExtension() {
|
|
6298
|
-
return null
|
|
6299
|
-
}
|
|
6300
|
-
|
|
6301
|
-
initializeToolbar(_lexxyToolbar) {
|
|
6302
|
-
|
|
6303
|
-
}
|
|
6304
|
-
}
|
|
6305
|
-
|
|
6306
6266
|
const configure = Lexxy.configure;
|
|
6307
6267
|
|
|
6308
6268
|
// Pushing elements definition to after the current call stack to allow global configuration to take place first
|