@37signals/lexxy 0.1.19-beta → 0.1.21-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/README.md +6 -1
- package/dist/lexxy.esm.js +662 -104
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ A modern rich text editor for Rails.
|
|
|
23
23
|
Add this line to your application's Gemfile:
|
|
24
24
|
|
|
25
25
|
```ruby
|
|
26
|
-
gem 'lexxy', '~> 0.1.
|
|
26
|
+
gem 'lexxy', '~> 0.1.20.beta' # Need to specify the version since it's a pre-release
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
And then execute:
|
|
@@ -355,6 +355,11 @@ Each event is dispatched on the `<lexxy-editor>` element.
|
|
|
355
355
|
Fired when the `<lexxy-editor>` element is attached to the DOM and ready for use.
|
|
356
356
|
This is useful for one-time setup.
|
|
357
357
|
|
|
358
|
+
### lexxy:focus and lexxy:blur
|
|
359
|
+
|
|
360
|
+
Fired whenever the editor element gains or loses focus.
|
|
361
|
+
Useful to show or hide accessory UI state.
|
|
362
|
+
|
|
358
363
|
### `lexxy:change`
|
|
359
364
|
|
|
360
365
|
Fired whenever the editor content changes.
|
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import DOMPurify from 'dompurify';
|
|
2
|
-
import {
|
|
2
|
+
import { getStyleObjectFromCSS, getCSSFromStyleObject, $forEachSelectedTextNode, $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/selection';
|
|
3
|
+
import { $isTextNode, TextNode, $getSelection, $isRangeSelection, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, SELECTION_CHANGE_COMMAND, $createNodeSelection, $setSelection, $createParagraphNode, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, $isParagraphNode, $insertNodes, $createLineBreakNode, CLEAR_HISTORY_COMMAND, $addUpdateTag, SKIP_DOM_SELECTION_TAG, createEditor, COMMAND_PRIORITY_NORMAL, BLUR_COMMAND, FOCUS_COMMAND, KEY_TAB_COMMAND, KEY_SPACE_COMMAND } from 'lexical';
|
|
3
4
|
import { $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $createListNode, ListNode, ListItemNode, registerList } from '@lexical/list';
|
|
4
5
|
import { $isQuoteNode, $isHeadingNode, $createQuoteNode, $createHeadingNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
5
|
-
import { $isCodeNode, CodeNode, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP
|
|
6
|
+
import { $isCodeNode, CodeNode, normalizeCodeLang, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
|
|
6
7
|
import { $isLinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, LinkNode, AutoLinkNode } from '@lexical/link';
|
|
7
8
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
8
9
|
import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown';
|
|
@@ -11,12 +12,48 @@ import { DirectUpload } from '@rails/activestorage';
|
|
|
11
12
|
import { marked } from 'marked';
|
|
12
13
|
import 'prismjs/components/prism-ruby';
|
|
13
14
|
|
|
15
|
+
const ALLOWED_HTML_TAGS = [ "a", "action-text-attachment", "b", "blockquote", "br", "code", "em",
|
|
16
|
+
"figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "li", "mark", "ol", "p", "pre", "q", "s", "strong", "ul" ];
|
|
17
|
+
|
|
18
|
+
const ALLOWED_HTML_ATTRIBUTES = [ "alt", "caption", "class", "content", "content-type", "contenteditable",
|
|
19
|
+
"data-direct-upload-id", "data-sgid", "filename", "filesize", "height", "href", "presentation",
|
|
20
|
+
"previewable", "sgid", "src", "style", "title", "url", "width" ];
|
|
21
|
+
|
|
22
|
+
const ALLOWED_STYLE_PROPERTIES = [ "color", "background-color" ];
|
|
23
|
+
|
|
24
|
+
function styleFilterHook(_currentNode, hookEvent) {
|
|
25
|
+
if (hookEvent.attrName === "style" && hookEvent.attrValue) {
|
|
26
|
+
const styles = { ...getStyleObjectFromCSS(hookEvent.attrValue) };
|
|
27
|
+
const sanitizedStyles = { };
|
|
28
|
+
|
|
29
|
+
for (const property in styles) {
|
|
30
|
+
if (ALLOWED_STYLE_PROPERTIES.includes(property)) {
|
|
31
|
+
sanitizedStyles[property] = styles[property];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (Object.keys(sanitizedStyles).length) {
|
|
36
|
+
hookEvent.attrValue = getCSSFromStyleObject(sanitizedStyles);
|
|
37
|
+
} else {
|
|
38
|
+
hookEvent.keepAttr = false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
DOMPurify.addHook("uponSanitizeAttribute", styleFilterHook);
|
|
44
|
+
|
|
14
45
|
DOMPurify.addHook("uponSanitizeElement", (node, data) => {
|
|
15
46
|
if (data.tagName === "strong" || data.tagName === "em") {
|
|
16
47
|
node.removeAttribute("class");
|
|
17
48
|
}
|
|
18
49
|
});
|
|
19
50
|
|
|
51
|
+
DOMPurify.setConfig({
|
|
52
|
+
ALLOWED_TAGS: ALLOWED_HTML_TAGS,
|
|
53
|
+
ALLOWED_ATTR: ALLOWED_HTML_ATTRIBUTES,
|
|
54
|
+
SAFE_FOR_XML: false // So that it does not strip attributes that contains serialized HTML (like content)
|
|
55
|
+
});
|
|
56
|
+
|
|
20
57
|
function getNonce() {
|
|
21
58
|
const element = document.head.querySelector("meta[name=csp-nonce]");
|
|
22
59
|
return element?.content
|
|
@@ -53,6 +90,31 @@ function isPrintableCharacter(event) {
|
|
|
53
90
|
return event.key.length === 1
|
|
54
91
|
}
|
|
55
92
|
|
|
93
|
+
function extendTextNodeConversion(conversionName, callback = (textNode => textNode)) {
|
|
94
|
+
return extendConversion(TextNode, conversionName, (conversionOutput, element) => ({
|
|
95
|
+
...conversionOutput,
|
|
96
|
+
forChild: (lexicalNode, parentNode) => {
|
|
97
|
+
const originalForChild = conversionOutput?.forChild ?? (x => x);
|
|
98
|
+
let childNode = originalForChild(lexicalNode, parentNode);
|
|
99
|
+
|
|
100
|
+
if ($isTextNode(childNode)) childNode = callback(childNode, element) ?? childNode;
|
|
101
|
+
return childNode
|
|
102
|
+
}
|
|
103
|
+
}))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function extendConversion(nodeKlass, conversionName, callback = (output => output)) {
|
|
107
|
+
return (element) => {
|
|
108
|
+
const converter = nodeKlass.importDOM()?.[conversionName]?.(element);
|
|
109
|
+
if (!converter) return null
|
|
110
|
+
|
|
111
|
+
const conversionOutput = converter.conversion(element);
|
|
112
|
+
if (!conversionOutput) return conversionOutput
|
|
113
|
+
|
|
114
|
+
return callback(conversionOutput, element) ?? conversionOutput
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
56
118
|
class LexicalToolbarElement extends HTMLElement {
|
|
57
119
|
constructor() {
|
|
58
120
|
super();
|
|
@@ -83,6 +145,14 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
83
145
|
this.#monitorSelectionChanges();
|
|
84
146
|
this.#monitorHistoryChanges();
|
|
85
147
|
this.#refreshToolbarOverflow();
|
|
148
|
+
|
|
149
|
+
this.toggleAttribute("connected", true);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get #dialogs() {
|
|
153
|
+
const dialogButtons = this.querySelectorAll("[data-dialog-target]");
|
|
154
|
+
const dialogTags = Array.from(dialogButtons).map(button => `lexxy-${button.dataset.dialogTarget}`);
|
|
155
|
+
return Array.from(this.querySelectorAll(dialogTags))
|
|
86
156
|
}
|
|
87
157
|
|
|
88
158
|
#bindButtons() {
|
|
@@ -108,15 +178,25 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
108
178
|
|
|
109
179
|
// Not using popover because of CSS anchoring still not widely available.
|
|
110
180
|
#toggleDialog(button) {
|
|
111
|
-
const
|
|
181
|
+
const dialogTarget = button.dataset.dialogTarget;
|
|
182
|
+
const dialog = this.querySelector("lexxy-" + dialogTarget);
|
|
183
|
+
if (!dialog) return
|
|
112
184
|
|
|
113
185
|
if (dialog.open) {
|
|
114
186
|
dialog.close();
|
|
115
187
|
} else {
|
|
116
|
-
|
|
188
|
+
this.#closeOpenDialogs();
|
|
189
|
+
dialog.show(button);
|
|
117
190
|
}
|
|
118
191
|
}
|
|
119
192
|
|
|
193
|
+
#closeOpenDialogs() {
|
|
194
|
+
const openDialogs = this.querySelectorAll("dialog[open]");
|
|
195
|
+
openDialogs.forEach(openDialog => {
|
|
196
|
+
openDialog.closest(".lexxy-dialog").close();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
120
200
|
#bindHotkeys() {
|
|
121
201
|
this.editorElement.addEventListener("keydown", (event) => {
|
|
122
202
|
const buttons = this.querySelectorAll("[data-hotkey]");
|
|
@@ -154,6 +234,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
154
234
|
this.editor.registerUpdateListener(() => {
|
|
155
235
|
this.editor.getEditorState().read(() => {
|
|
156
236
|
this.#updateButtonStates();
|
|
237
|
+
this.#updateDialogStates();
|
|
157
238
|
});
|
|
158
239
|
});
|
|
159
240
|
}
|
|
@@ -174,14 +255,6 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
174
255
|
});
|
|
175
256
|
}
|
|
176
257
|
|
|
177
|
-
#setButtonDisabled(name, isDisabled) {
|
|
178
|
-
const button = this.querySelector(`[name="${name}"]`);
|
|
179
|
-
if (button) {
|
|
180
|
-
button.disabled = isDisabled;
|
|
181
|
-
button.setAttribute("aria-disabled", isDisabled.toString());
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
258
|
#updateButtonStates() {
|
|
186
259
|
const selection = $getSelection();
|
|
187
260
|
if (!$isRangeSelection(selection)) return
|
|
@@ -194,26 +267,32 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
194
267
|
const isBold = selection.hasFormat("bold");
|
|
195
268
|
const isItalic = selection.hasFormat("italic");
|
|
196
269
|
const isStrikethrough = selection.hasFormat("strikethrough");
|
|
270
|
+
const isHighlight = selection.hasFormat("highlight");
|
|
271
|
+
const isInLink = this.#isInLink(anchorNode);
|
|
272
|
+
const isInQuote = $isQuoteNode(topLevelElement);
|
|
273
|
+
const isInHeading = $isHeadingNode(topLevelElement);
|
|
197
274
|
const isInCode = $isCodeNode(topLevelElement) || selection.hasFormat("code");
|
|
198
275
|
const isInList = this.#isInList(anchorNode);
|
|
199
276
|
const listType = getListType(anchorNode);
|
|
200
|
-
const isInQuote = $isQuoteNode(topLevelElement);
|
|
201
|
-
const isInHeading = $isHeadingNode(topLevelElement);
|
|
202
|
-
const isInLink = this.#isInLink(anchorNode);
|
|
203
277
|
|
|
204
278
|
this.#setButtonPressed("bold", isBold);
|
|
205
279
|
this.#setButtonPressed("italic", isItalic);
|
|
206
280
|
this.#setButtonPressed("strikethrough", isStrikethrough);
|
|
281
|
+
this.#setButtonPressed("highlight", isHighlight);
|
|
282
|
+
this.#setButtonPressed("link", isInLink);
|
|
283
|
+
this.#setButtonPressed("quote", isInQuote);
|
|
284
|
+
this.#setButtonPressed("heading", isInHeading);
|
|
207
285
|
this.#setButtonPressed("code", isInCode);
|
|
208
286
|
this.#setButtonPressed("unordered-list", isInList && listType === "bullet");
|
|
209
287
|
this.#setButtonPressed("ordered-list", isInList && listType === "number");
|
|
210
|
-
this.#setButtonPressed("quote", isInQuote);
|
|
211
|
-
this.#setButtonPressed("heading", isInHeading);
|
|
212
|
-
this.#setButtonPressed("link", isInLink);
|
|
213
288
|
|
|
214
289
|
this.#updateUndoRedoButtonStates();
|
|
215
290
|
}
|
|
216
291
|
|
|
292
|
+
#updateDialogStates() {
|
|
293
|
+
this.#dialogs.forEach(dialog => dialog.updateStateCallback());
|
|
294
|
+
}
|
|
295
|
+
|
|
217
296
|
#isInList(node) {
|
|
218
297
|
let current = node;
|
|
219
298
|
while (current) {
|
|
@@ -239,6 +318,14 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
239
318
|
}
|
|
240
319
|
}
|
|
241
320
|
|
|
321
|
+
#setButtonDisabled(name, isDisabled) {
|
|
322
|
+
const button = this.querySelector(`[name="${name}"]`);
|
|
323
|
+
if (button) {
|
|
324
|
+
button.disabled = isDisabled;
|
|
325
|
+
button.setAttribute("aria-disabled", isDisabled.toString());
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
242
329
|
#toolbarIsOverflowing() {
|
|
243
330
|
return this.scrollWidth > this.clientWidth
|
|
244
331
|
}
|
|
@@ -249,6 +336,9 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
249
336
|
|
|
250
337
|
this.#overflow.style.display = this.#overflowMenu.children.length ? "block" : "none";
|
|
251
338
|
this.#overflow.setAttribute("nonce", getNonce());
|
|
339
|
+
|
|
340
|
+
const isOverflowing = this.#overflowMenu.children.length > 0;
|
|
341
|
+
this.toggleAttribute("overflowing", isOverflowing);
|
|
252
342
|
}
|
|
253
343
|
|
|
254
344
|
get #overflow() {
|
|
@@ -266,7 +356,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
266
356
|
}
|
|
267
357
|
|
|
268
358
|
#compactMenu() {
|
|
269
|
-
const buttons = this.#
|
|
359
|
+
const buttons = this.#buttonsWithSeparator.reverse();
|
|
270
360
|
let movedToOverflow = false;
|
|
271
361
|
|
|
272
362
|
for (const button of buttons) {
|
|
@@ -281,6 +371,10 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
281
371
|
}
|
|
282
372
|
|
|
283
373
|
get #buttons() {
|
|
374
|
+
return Array.from(this.querySelectorAll(":scope > button"))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
get #buttonsWithSeparator() {
|
|
284
378
|
return Array.from(this.querySelectorAll(":scope > button, :scope > [role=separator]"))
|
|
285
379
|
}
|
|
286
380
|
|
|
@@ -300,12 +394,22 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
300
394
|
</svg>
|
|
301
395
|
</button>
|
|
302
396
|
|
|
303
|
-
<
|
|
304
|
-
<
|
|
397
|
+
<lexxy-highlight-dialog class="lexxy-dialog lexxy-highlight-dialog">
|
|
398
|
+
<dialog class="highlight-dialog">
|
|
399
|
+
<div class="lexxy-highlight-dialog-content">
|
|
400
|
+
<div data-button-group="color" data-values="var(--highlight-1); var(--highlight-2); var(--highlight-3); var(--highlight-4); var(--highlight-5); var(--highlight-6); var(--highlight-7); var(--highlight-8); var(--highlight-9)"></div>
|
|
401
|
+
<div data-button-group="background-color" data-values="var(--highlight-bg-1); var(--highlight-bg-2); var(--highlight-bg-3); var(--highlight-bg-4); var(--highlight-bg-5); var(--highlight-bg-6); var(--highlight-bg-7); var(--highlight-bg-8); var(--highlight-bg-9)"></div>
|
|
402
|
+
<button data-command="removeHighlight" class="lexxy-highlight-dialog-reset">Remove all coloring</button>
|
|
403
|
+
</div>
|
|
404
|
+
</dialog>
|
|
405
|
+
</lexxy-highlight-dialog>
|
|
406
|
+
|
|
407
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="highlight" title="Color highlight" data-dialog-target="highlight-dialog">
|
|
408
|
+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M7.65422 0.711575C7.1856 0.242951 6.42579 0.242951 5.95717 0.711575C5.48853 1.18021 5.48853 1.94 5.95717 2.40864L8.70864 5.16011L2.85422 11.0145C1.44834 12.4204 1.44833 14.6998 2.85422 16.1057L7.86011 21.1115C9.26599 22.5174 11.5454 22.5174 12.9513 21.1115L19.6542 14.4087C20.1228 13.94 20.1228 13.1802 19.6542 12.7115L11.8544 4.91171L11.2542 4.31158L7.65422 0.711575ZM4.55127 12.7115L10.4057 6.85716L17.1087 13.56H4.19981C4.19981 13.253 4.31696 12.9459 4.55127 12.7115ZM23.6057 20.76C23.6057 22.0856 22.5311 23.16 21.2057 23.16C19.8802 23.16 18.8057 22.0856 18.8057 20.76C18.8057 19.5408 19.8212 18.5339 20.918 17.4462C21.0135 17.3516 21.1096 17.2563 21.2057 17.16C21.3018 17.2563 21.398 17.3516 21.4935 17.4462C22.5903 18.5339 23.6057 19.5408 23.6057 20.76Z"/></svg>
|
|
305
409
|
</button>
|
|
306
410
|
|
|
307
|
-
<lexxy-link-dialog class="lexxy-link-dialog">
|
|
308
|
-
<dialog class="link-dialog"
|
|
411
|
+
<lexxy-link-dialog class="lexxy-dialog lexxy-link-dialog">
|
|
412
|
+
<dialog class="link-dialog">
|
|
309
413
|
<form method="dialog">
|
|
310
414
|
<input type="url" placeholder="Enter a URL…" class="input" required>
|
|
311
415
|
<div class="lexxy-dialog-actions">
|
|
@@ -316,6 +420,10 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
316
420
|
</dialog>
|
|
317
421
|
</lexxy-link-dialog>
|
|
318
422
|
|
|
423
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="link" title="Link" data-dialog-target="link-dialog" data-hotkey="cmd+k ctrl+k">
|
|
424
|
+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12.111 9.546a1.5 1.5 0 012.121 0 5.5 5.5 0 010 7.778l-2.828 2.828a5.5 5.5 0 01-7.778 0 5.498 5.498 0 010-7.777l2.828-2.83a1.5 1.5 0 01.355-.262 6.52 6.52 0 00.351 3.799l-1.413 1.414a2.499 2.499 0 000 3.535 2.499 2.499 0 003.535 0l2.83-2.828a2.5 2.5 0 000-3.536 1.5 1.5 0 010-2.121z"/><path d="M12.111 3.89a5.5 5.5 0 117.778 7.777l-2.828 2.829a1.496 1.496 0 01-.355.262 6.522 6.522 0 00-.351-3.8l1.413-1.412a2.5 2.5 0 10-3.536-3.535l-2.828 2.828a2.5 2.5 0 000 3.536 1.5 1.5 0 01-2.122 2.12 5.5 5.5 0 010-7.777l2.83-2.829z"/></svg>
|
|
425
|
+
</button>
|
|
426
|
+
|
|
319
427
|
<button class="lexxy-editor__toolbar-button" type="button" name="quote" data-command="insertQuoteBlock" title="Quote">
|
|
320
428
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.5 5C8.985 5 11 7.09 11 9.667c0 2.694-.962 5.005-2.187 6.644-.613.82-1.3 1.481-1.978 1.943-.668.454-1.375.746-2.022.746a.563.563 0 01-.52-.36.602.602 0 01.067-.57l.055-.066.009-.009.041-.048a4.25 4.25 0 00.168-.21c.143-.188.336-.47.53-.84a6.743 6.743 0 00.75-2.605C3.705 13.994 2 12.038 2 9.667 2 7.089 4.015 5 6.5 5zM17.5 5C19.985 5 22 7.09 22 9.667c0 2.694-.962 5.005-2.187 6.644-.613.82-1.3 1.481-1.978 1.943-.668.454-1.375.746-2.023.746a.563.563 0 01-.52-.36.602.602 0 01.068-.57l.055-.066.009-.009.041-.048c.039-.045.097-.115.168-.21a6.16 6.16 0 00.53-.84 6.745 6.745 0 00.75-2.605C14.705 13.994 13 12.038 13 9.667 13 7.089 15.015 5 17.5 5z"/></svg>
|
|
321
429
|
</button>
|
|
@@ -343,9 +451,9 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
343
451
|
<button class="lexxy-editor__toolbar-button" type="button" name="divider" data-command="insertHorizontalDivider" title="Insert a divider">
|
|
344
452
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 12C0 11.4477 0.447715 11 1 11H23C23.5523 11 24 11.4477 24 12C24 12.5523 23.5523 13 23 13H1C0.447716 13 0 12.5523 0 12Z"/><path d="M4 5C4 3.89543 4.89543 3 6 3H18C19.1046 3 20 3.89543 20 5C20 6.10457 19.1046 7 18 7H6C4.89543 7 4 6.10457 4 5Z"/><path d="M4 19C4 17.8954 4.89543 17 6 17H18C19.1046 17 20 17.8954 20 19C20 20.1046 19.1046 21 18 21H6C4.89543 21 4 20.1046 4 19Z"/></svg>
|
|
345
453
|
</button>
|
|
346
|
-
|
|
454
|
+
|
|
347
455
|
<div class="lexxy-editor__toolbar-spacer" role="separator"></div>
|
|
348
|
-
|
|
456
|
+
|
|
349
457
|
<button class="lexxy-editor__toolbar-button" type="button" name="undo" data-command="undo" title="Undo" data-hotkey="cmd+z ctrl+z">
|
|
350
458
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M5.64648 8.26531C7.93911 6.56386 10.7827 5.77629 13.624 6.05535C16.4655 6.33452 19.1018 7.66079 21.0195 9.77605C22.5839 11.5016 23.5799 13.6516 23.8936 15.9352C24.0115 16.7939 23.2974 17.4997 22.4307 17.4997C21.5641 17.4997 20.8766 16.7915 20.7148 15.9401C20.4295 14.4379 19.7348 13.0321 18.6943 11.8844C17.3 10.3464 15.3835 9.38139 13.3174 9.17839C11.2514 8.97546 9.18359 9.54856 7.5166 10.7858C6.38259 11.6275 5.48981 12.7361 4.90723 13.9997H8.5C9.3283 13.9997 9.99979 14.6714 10 15.4997C10 16.3281 9.32843 16.9997 8.5 16.9997H1.5C0.671573 16.9997 0 16.3281 0 15.4997V8.49968C0.000213656 7.67144 0.671705 6.99968 1.5 6.99968C2.3283 6.99968 2.99979 7.67144 3 8.49968V11.0212C3.7166 9.9704 4.60793 9.03613 5.64648 8.26531Z"/></svg>
|
|
351
459
|
</button>
|
|
@@ -370,6 +478,12 @@ var theme = {
|
|
|
370
478
|
italic: "lexxy-content__italic",
|
|
371
479
|
strikethrough: "lexxy-content__strikethrough",
|
|
372
480
|
underline: "lexxy-content__underline",
|
|
481
|
+
highlight: "lexxy-content__highlight"
|
|
482
|
+
},
|
|
483
|
+
list: {
|
|
484
|
+
nested: {
|
|
485
|
+
listitem: "lexxy-nested-listitem",
|
|
486
|
+
}
|
|
373
487
|
},
|
|
374
488
|
codeHighlight: {
|
|
375
489
|
atrule: "code-token__attr",
|
|
@@ -417,24 +531,12 @@ var theme = {
|
|
|
417
531
|
}
|
|
418
532
|
};
|
|
419
533
|
|
|
420
|
-
const VISUALLY_RELEVANT_ELEMENTS_SELECTOR = [
|
|
421
|
-
"img", "video", "audio", "iframe", "embed", "object", "picture", "source", "canvas", "svg", "math",
|
|
422
|
-
"form", "input", "textarea", "select", "button", "code", "blockquote", "hr"
|
|
423
|
-
].join(",");
|
|
424
|
-
|
|
425
|
-
const ALLOWED_HTML_TAGS = [ "a", "action-text-attachment", "b", "blockquote", "br", "code", "em",
|
|
426
|
-
"figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "li", "ol", "p", "pre", "q", "s", "strong", "ul" ];
|
|
427
|
-
|
|
428
|
-
const ALLOWED_HTML_ATTRIBUTES = [ "alt", "caption", "class", "content", "content-type", "contenteditable",
|
|
429
|
-
"data-direct-upload-id", "data-sgid", "filename", "filesize", "height", "href", "presentation",
|
|
430
|
-
"previewable", "sgid", "src", "title", "url", "width" ];
|
|
431
|
-
|
|
432
534
|
function createElement(name, properties) {
|
|
433
535
|
const element = document.createElement(name);
|
|
434
536
|
for (const [ key, value ] of Object.entries(properties || {})) {
|
|
435
537
|
if (key in element) {
|
|
436
538
|
element[key] = value;
|
|
437
|
-
} else if (value !== null && value !== undefined
|
|
539
|
+
} else if (value !== null && value !== undefined) {
|
|
438
540
|
element.setAttribute(key, value);
|
|
439
541
|
}
|
|
440
542
|
}
|
|
@@ -466,18 +568,8 @@ function dispatchCustomEvent(element, name, detail) {
|
|
|
466
568
|
element.dispatchEvent(event);
|
|
467
569
|
}
|
|
468
570
|
|
|
469
|
-
function containsVisuallyRelevantChildren(element) {
|
|
470
|
-
return element.querySelector(VISUALLY_RELEVANT_ELEMENTS_SELECTOR)
|
|
471
|
-
}
|
|
472
|
-
|
|
473
571
|
function sanitize(html) {
|
|
474
|
-
|
|
475
|
-
ALLOWED_TAGS: ALLOWED_HTML_TAGS,
|
|
476
|
-
ALLOWED_ATTR: ALLOWED_HTML_ATTRIBUTES,
|
|
477
|
-
SAFE_FOR_XML: false // So that it does not stripe attributes that contains serialized HTML (like content)
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
return sanitizedHtml
|
|
572
|
+
return DOMPurify.sanitize(html)
|
|
481
573
|
}
|
|
482
574
|
|
|
483
575
|
function dispatch(element, eventName, detail = null, cancelable = false) {
|
|
@@ -694,11 +786,10 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
694
786
|
|
|
695
787
|
#createEditableCaption() {
|
|
696
788
|
const caption = createElement("figcaption", { className: "attachment__caption" });
|
|
697
|
-
const input = createElement("
|
|
698
|
-
type: "text",
|
|
699
|
-
class: "input",
|
|
789
|
+
const input = createElement("textarea", {
|
|
700
790
|
value: this.caption,
|
|
701
|
-
placeholder: this.fileName
|
|
791
|
+
placeholder: this.fileName,
|
|
792
|
+
rows: "1"
|
|
702
793
|
});
|
|
703
794
|
|
|
704
795
|
input.addEventListener("focusin", () => input.placeholder = "Add caption...");
|
|
@@ -990,6 +1081,8 @@ const COMMANDS = [
|
|
|
990
1081
|
"strikethrough",
|
|
991
1082
|
"link",
|
|
992
1083
|
"unlink",
|
|
1084
|
+
"toggleHighlight",
|
|
1085
|
+
"removeHighlight",
|
|
993
1086
|
"rotateHeadingFormat",
|
|
994
1087
|
"insertUnorderedList",
|
|
995
1088
|
"insertOrderedList",
|
|
@@ -1012,6 +1105,7 @@ class CommandDispatcher {
|
|
|
1012
1105
|
this.selection = editorElement.selection;
|
|
1013
1106
|
this.contents = editorElement.contents;
|
|
1014
1107
|
this.clipboard = editorElement.clipboard;
|
|
1108
|
+
this.highlighter = editorElement.highlighter;
|
|
1015
1109
|
|
|
1016
1110
|
this.#registerCommands();
|
|
1017
1111
|
this.#registerDragAndDropHandlers();
|
|
@@ -1033,6 +1127,14 @@ class CommandDispatcher {
|
|
|
1033
1127
|
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough");
|
|
1034
1128
|
}
|
|
1035
1129
|
|
|
1130
|
+
dispatchToggleHighlight(styles) {
|
|
1131
|
+
this.highlighter.toggle(styles);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
dispatchRemoveHighlight() {
|
|
1135
|
+
this.highlighter.remove();
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1036
1138
|
dispatchLink(url) {
|
|
1037
1139
|
this.editor.update(() => {
|
|
1038
1140
|
const selection = $getSelection();
|
|
@@ -1095,8 +1197,10 @@ class CommandDispatcher {
|
|
|
1095
1197
|
|
|
1096
1198
|
dispatchInsertHorizontalDivider() {
|
|
1097
1199
|
this.editor.update(() => {
|
|
1098
|
-
this.contents.
|
|
1200
|
+
this.contents.insertAtCursorEnsuringLineBelow(new HorizontalDividerNode());
|
|
1099
1201
|
});
|
|
1202
|
+
|
|
1203
|
+
this.editor.focus();
|
|
1100
1204
|
}
|
|
1101
1205
|
|
|
1102
1206
|
dispatchRotateHeadingFormat() {
|
|
@@ -1131,6 +1235,7 @@ class CommandDispatcher {
|
|
|
1131
1235
|
const input = createElement("input", {
|
|
1132
1236
|
type: "file",
|
|
1133
1237
|
multiple: true,
|
|
1238
|
+
style: "display: none;",
|
|
1134
1239
|
onchange: ({ target }) => {
|
|
1135
1240
|
const files = Array.from(target.files);
|
|
1136
1241
|
if (!files.length) return
|
|
@@ -1141,7 +1246,7 @@ class CommandDispatcher {
|
|
|
1141
1246
|
}
|
|
1142
1247
|
});
|
|
1143
1248
|
|
|
1144
|
-
|
|
1249
|
+
this.editorElement.appendChild(input); // Append and remove just for the sake of making it testable
|
|
1145
1250
|
input.click();
|
|
1146
1251
|
setTimeout(() => input.remove(), 1000);
|
|
1147
1252
|
}
|
|
@@ -1272,8 +1377,10 @@ class Selection {
|
|
|
1272
1377
|
|
|
1273
1378
|
set current(selection) {
|
|
1274
1379
|
if ($isNodeSelection(selection)) {
|
|
1275
|
-
this.
|
|
1276
|
-
|
|
1380
|
+
this.editor.getEditorState().read(() => {
|
|
1381
|
+
this._current = $getSelection();
|
|
1382
|
+
this.#syncSelectedClasses();
|
|
1383
|
+
});
|
|
1277
1384
|
} else {
|
|
1278
1385
|
this.editor.update(() => {
|
|
1279
1386
|
this.#syncSelectedClasses();
|
|
@@ -1308,6 +1415,52 @@ class Selection {
|
|
|
1308
1415
|
});
|
|
1309
1416
|
}
|
|
1310
1417
|
|
|
1418
|
+
selectedNodeWithOffset() {
|
|
1419
|
+
const selection = $getSelection();
|
|
1420
|
+
if (!selection) return { node: null, offset: 0 }
|
|
1421
|
+
|
|
1422
|
+
if ($isRangeSelection(selection)) {
|
|
1423
|
+
return {
|
|
1424
|
+
node: selection.anchor.getNode(),
|
|
1425
|
+
offset: selection.anchor.offset
|
|
1426
|
+
}
|
|
1427
|
+
} else if ($isNodeSelection(selection)) {
|
|
1428
|
+
const [ node ] = selection.getNodes();
|
|
1429
|
+
return {
|
|
1430
|
+
node,
|
|
1431
|
+
offset: 0
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
return { node: null, offset: 0 }
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
preservingSelection(fn) {
|
|
1439
|
+
let selectionState = null;
|
|
1440
|
+
|
|
1441
|
+
this.editor.getEditorState().read(() => {
|
|
1442
|
+
const selection = $getSelection();
|
|
1443
|
+
if (selection && $isRangeSelection(selection)) {
|
|
1444
|
+
selectionState = {
|
|
1445
|
+
anchor: { key: selection.anchor.key, offset: selection.anchor.offset },
|
|
1446
|
+
focus: { key: selection.focus.key, offset: selection.focus.offset }
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
fn();
|
|
1452
|
+
|
|
1453
|
+
if (selectionState) {
|
|
1454
|
+
this.editor.update(() => {
|
|
1455
|
+
const selection = $getSelection();
|
|
1456
|
+
if (selection && $isRangeSelection(selection)) {
|
|
1457
|
+
selection.anchor.set(selectionState.anchor.key, selectionState.anchor.offset, "text");
|
|
1458
|
+
selection.focus.set(selectionState.focus.key, selectionState.focus.offset, "text");
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1311
1464
|
get hasSelectedWordsInSingleLine() {
|
|
1312
1465
|
const selection = $getSelection();
|
|
1313
1466
|
if (!$isRangeSelection(selection)) return false
|
|
@@ -1411,8 +1564,9 @@ class Selection {
|
|
|
1411
1564
|
|
|
1412
1565
|
this._currentlySelectedKeys = new Set();
|
|
1413
1566
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1567
|
+
const selection = $getSelection();
|
|
1568
|
+
if (selection && $isNodeSelection(selection)) {
|
|
1569
|
+
for (const node of selection.getNodes()) {
|
|
1416
1570
|
this._currentlySelectedKeys.add(node.getKey());
|
|
1417
1571
|
}
|
|
1418
1572
|
}
|
|
@@ -2265,6 +2419,11 @@ class Contents {
|
|
|
2265
2419
|
});
|
|
2266
2420
|
}
|
|
2267
2421
|
|
|
2422
|
+
insertAtCursorEnsuringLineBelow(node) {
|
|
2423
|
+
this.insertAtCursor(node);
|
|
2424
|
+
this.#insertLineBelowIfLastNode(node);
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2268
2427
|
insertNodeWrappingEachSelectedLine(newNodeFn) {
|
|
2269
2428
|
this.editor.update(() => {
|
|
2270
2429
|
const selection = $getSelection();
|
|
@@ -2549,6 +2708,17 @@ class Contents {
|
|
|
2549
2708
|
return this.editorElement.selection
|
|
2550
2709
|
}
|
|
2551
2710
|
|
|
2711
|
+
#insertLineBelowIfLastNode(node) {
|
|
2712
|
+
this.editor.update(() => {
|
|
2713
|
+
const nextSibling = node.getNextSibling();
|
|
2714
|
+
if (!nextSibling) {
|
|
2715
|
+
const newParagraph = $createParagraphNode();
|
|
2716
|
+
node.insertAfter(newParagraph);
|
|
2717
|
+
newParagraph.selectStart();
|
|
2718
|
+
}
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2552
2722
|
#unwrap(node) {
|
|
2553
2723
|
const children = node.getChildren();
|
|
2554
2724
|
|
|
@@ -2586,8 +2756,6 @@ class Contents {
|
|
|
2586
2756
|
elements.forEach((element) => {
|
|
2587
2757
|
wrappingNode.append(element);
|
|
2588
2758
|
});
|
|
2589
|
-
|
|
2590
|
-
$setSelection(null);
|
|
2591
2759
|
});
|
|
2592
2760
|
}
|
|
2593
2761
|
|
|
@@ -2891,7 +3059,8 @@ class Contents {
|
|
|
2891
3059
|
lastInsertedNode.insertAfter(textNodeAfter);
|
|
2892
3060
|
|
|
2893
3061
|
this.#appendLineBreakIfNeeded(textNodeAfter.getParentOrThrow());
|
|
2894
|
-
|
|
3062
|
+
const cursorOffset = textAfterCursor ? 0 : 1;
|
|
3063
|
+
textNodeAfter.select(cursorOffset, cursorOffset);
|
|
2895
3064
|
}
|
|
2896
3065
|
|
|
2897
3066
|
#insertReplacementNodes(startNode, replacementNodes) {
|
|
@@ -3062,6 +3231,132 @@ class Clipboard {
|
|
|
3062
3231
|
}
|
|
3063
3232
|
}
|
|
3064
3233
|
|
|
3234
|
+
class Highlighter {
|
|
3235
|
+
constructor(editorElement) {
|
|
3236
|
+
this.editor = editorElement.editor;
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
toggle(styles) {
|
|
3240
|
+
this.editor.update(() => {
|
|
3241
|
+
this.#toggleSelectionStyles(styles);
|
|
3242
|
+
$forEachSelectedTextNode(node => this.#syncHighlightWithStyle(node));
|
|
3243
|
+
});
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
remove() {
|
|
3247
|
+
this.toggle({ "color": undefined, "background-color": undefined });
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
#toggleSelectionStyles(styles) {
|
|
3251
|
+
const selection = $getSelection();
|
|
3252
|
+
|
|
3253
|
+
for (const property in styles) {
|
|
3254
|
+
const oldValue = $getSelectionStyleValueForProperty(selection, property);
|
|
3255
|
+
const patch = { [property]: this.#toggleOrReplace(oldValue, styles[property]) };
|
|
3256
|
+
$patchStyleText(selection, patch);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
#toggleOrReplace(oldValue, newValue) {
|
|
3261
|
+
return oldValue === newValue ? null : newValue
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
#syncHighlightWithStyle(node) {
|
|
3265
|
+
if (this.#hasHighlightStyles(node) !== node.hasFormat("highlight")) {
|
|
3266
|
+
node.toggleFormat("highlight");
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
#hasHighlightStyles(node) {
|
|
3271
|
+
const styles = getStyleObjectFromCSS(node.getStyle());
|
|
3272
|
+
return !!(styles.color || styles["background-color"])
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
class HighlightNode extends TextNode {
|
|
3277
|
+
$config() {
|
|
3278
|
+
return this.config("highlight", { extends: TextNode })
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
static importDOM() {
|
|
3282
|
+
return {
|
|
3283
|
+
mark: () => ({
|
|
3284
|
+
conversion: extendTextNodeConversion("mark", applyHighlightStyle),
|
|
3285
|
+
priority: 1
|
|
3286
|
+
})
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
function applyHighlightStyle(textNode, element) {
|
|
3292
|
+
const textColor = element.style?.color;
|
|
3293
|
+
const backgroundColor = element.style?.backgroundColor;
|
|
3294
|
+
|
|
3295
|
+
let highlightStyle = "";
|
|
3296
|
+
if (textColor && textColor !== "") highlightStyle += `color: ${textColor};`;
|
|
3297
|
+
if (backgroundColor && backgroundColor !== "") highlightStyle += `background-color: ${backgroundColor};`;
|
|
3298
|
+
|
|
3299
|
+
if (highlightStyle.length) {
|
|
3300
|
+
if (!textNode.hasFormat("highlight")) textNode.toggleFormat("highlight");
|
|
3301
|
+
return textNode.setStyle(textNode.getStyle() + highlightStyle)
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
const TRIX_LANGUAGE_ATTR = "language";
|
|
3306
|
+
|
|
3307
|
+
class TrixTextNode extends TextNode {
|
|
3308
|
+
$config() {
|
|
3309
|
+
return this.config("trix-text", { extends: TextNode })
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
static importDOM() {
|
|
3313
|
+
return {
|
|
3314
|
+
// em, span, and strong elements are directly styled in trix
|
|
3315
|
+
em: (element) => onlyStyledElements(element, {
|
|
3316
|
+
conversion: extendTextNodeConversion("i", applyHighlightStyle),
|
|
3317
|
+
priority: 1
|
|
3318
|
+
}),
|
|
3319
|
+
span: (element) => onlyStyledElements(element, {
|
|
3320
|
+
conversion: extendTextNodeConversion("mark", applyHighlightStyle),
|
|
3321
|
+
priority: 1
|
|
3322
|
+
}),
|
|
3323
|
+
strong: (element) => onlyStyledElements(element, {
|
|
3324
|
+
conversion: extendTextNodeConversion("b", applyHighlightStyle),
|
|
3325
|
+
priority: 1
|
|
3326
|
+
}),
|
|
3327
|
+
// del => s
|
|
3328
|
+
del: () => ({
|
|
3329
|
+
conversion: extendTextNodeConversion("s", applyStrikethrough),
|
|
3330
|
+
priority: 1
|
|
3331
|
+
}),
|
|
3332
|
+
// read "language" attribute and normalize
|
|
3333
|
+
pre: (element) => onlyPreLanguageElements(element, {
|
|
3334
|
+
conversion: extendConversion(CodeNode, "pre", applyLanguage),
|
|
3335
|
+
priority: 1
|
|
3336
|
+
})
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
function onlyStyledElements(element, conversion) {
|
|
3342
|
+
const elementHighlighted = element.style.color !== "" || element.style.backgroundColor !== "";
|
|
3343
|
+
return elementHighlighted ? conversion : null
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
function applyStrikethrough(textNode, element) {
|
|
3347
|
+
if (!textNode.hasFormat("strikethrough")) textNode.toggleFormat("strikethrough");
|
|
3348
|
+
return applyHighlightStyle(textNode, element)
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
function onlyPreLanguageElements(element, conversion) {
|
|
3352
|
+
return element.hasAttribute(TRIX_LANGUAGE_ATTR) ? conversion : null
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
function applyLanguage(conversionOutput, element) {
|
|
3356
|
+
const language = normalizeCodeLang(element.getAttribute(TRIX_LANGUAGE_ATTR));
|
|
3357
|
+
conversionOutput.node.setLanguage(language);
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3065
3360
|
class LexicalEditorElement extends HTMLElement {
|
|
3066
3361
|
static formAssociated = true
|
|
3067
3362
|
static debug = true
|
|
@@ -3084,6 +3379,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3084
3379
|
this.contents = new Contents(this);
|
|
3085
3380
|
this.selection = new Selection(this);
|
|
3086
3381
|
this.clipboard = new Clipboard(this);
|
|
3382
|
+
this.highlighter = new Highlighter(this);
|
|
3087
3383
|
|
|
3088
3384
|
CommandDispatcher.configureFor(this);
|
|
3089
3385
|
this.#initialize();
|
|
@@ -3119,6 +3415,10 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3119
3415
|
return this.internals.form
|
|
3120
3416
|
}
|
|
3121
3417
|
|
|
3418
|
+
get name() {
|
|
3419
|
+
return this.getAttribute("name")
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3122
3422
|
get toolbarElement() {
|
|
3123
3423
|
if (!this.#hasToolbar) return null
|
|
3124
3424
|
|
|
@@ -3194,6 +3494,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3194
3494
|
this.#registerComponents();
|
|
3195
3495
|
this.#listenForInvalidatedNodes();
|
|
3196
3496
|
this.#handleEnter();
|
|
3497
|
+
this.#handleFocus();
|
|
3197
3498
|
this.#attachDebugHooks();
|
|
3198
3499
|
this.#attachToolbar();
|
|
3199
3500
|
this.#loadInitialValue();
|
|
@@ -3219,6 +3520,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3219
3520
|
|
|
3220
3521
|
get #lexicalNodes() {
|
|
3221
3522
|
const nodes = [
|
|
3523
|
+
TrixTextNode,
|
|
3524
|
+
HighlightNode,
|
|
3222
3525
|
QuoteNode,
|
|
3223
3526
|
HeadingNode,
|
|
3224
3527
|
ListNode,
|
|
@@ -3372,6 +3675,14 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3372
3675
|
);
|
|
3373
3676
|
}
|
|
3374
3677
|
|
|
3678
|
+
#handleFocus() {
|
|
3679
|
+
// Lexxy handles focus and blur as commands
|
|
3680
|
+
// see https://github.com/facebook/lexical/blob/d1a8e84fe9063a4f817655b346b6ff373aa107f0/packages/lexical/src/LexicalEvents.ts#L35
|
|
3681
|
+
// and https://stackoverflow.com/a/72212077
|
|
3682
|
+
this.editor.registerCommand(BLUR_COMMAND, () => { dispatch(this, "lexxy:blur"); }, COMMAND_PRIORITY_NORMAL);
|
|
3683
|
+
this.editor.registerCommand(FOCUS_COMMAND, () => { dispatch(this, "lexxy:focus"); }, COMMAND_PRIORITY_NORMAL);
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3375
3686
|
#attachDebugHooks() {
|
|
3376
3687
|
if (!LexicalEditorElement.debug) return
|
|
3377
3688
|
|
|
@@ -3410,7 +3721,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3410
3721
|
}
|
|
3411
3722
|
|
|
3412
3723
|
get #isEmpty() {
|
|
3413
|
-
return
|
|
3724
|
+
return [ "<p><br></p>", "<p></p>", "" ].includes(this.value.trim())
|
|
3414
3725
|
}
|
|
3415
3726
|
|
|
3416
3727
|
#setValidity() {
|
|
@@ -3450,33 +3761,98 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3450
3761
|
|
|
3451
3762
|
customElements.define("lexxy-editor", LexicalEditorElement);
|
|
3452
3763
|
|
|
3453
|
-
class
|
|
3764
|
+
class ToolbarDialog extends HTMLElement {
|
|
3454
3765
|
connectedCallback() {
|
|
3455
3766
|
this.dialog = this.querySelector("dialog");
|
|
3456
|
-
|
|
3767
|
+
if ("closedBy" in this.dialog.constructor.prototype) {
|
|
3768
|
+
this.dialog.closedBy = "any";
|
|
3769
|
+
}
|
|
3770
|
+
this.#registerHandlers();
|
|
3771
|
+
}
|
|
3457
3772
|
|
|
3458
|
-
|
|
3459
|
-
this
|
|
3460
|
-
this.addEventListener("keydown", this.#handleKeyDown.bind(this));
|
|
3773
|
+
disconnectedCallback() {
|
|
3774
|
+
this.#removeClickOutsideHandler();
|
|
3461
3775
|
}
|
|
3462
3776
|
|
|
3463
|
-
|
|
3464
|
-
|
|
3777
|
+
updateStateCallback() { }
|
|
3778
|
+
|
|
3779
|
+
show(triggerButton) {
|
|
3780
|
+
if (this.preventImmediateReopen) { return }
|
|
3781
|
+
|
|
3782
|
+
this.triggerButton = triggerButton;
|
|
3783
|
+
this.#positionDialog();
|
|
3465
3784
|
this.dialog.show();
|
|
3785
|
+
|
|
3786
|
+
this.#setupClickOutsideHandler();
|
|
3466
3787
|
}
|
|
3467
3788
|
|
|
3468
3789
|
close() {
|
|
3469
3790
|
this.dialog.close();
|
|
3470
3791
|
}
|
|
3471
3792
|
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
this.#editor.dispatchCommand(command, this.input.value);
|
|
3793
|
+
get toolbar() {
|
|
3794
|
+
return this.closest("lexxy-toolbar")
|
|
3475
3795
|
}
|
|
3476
3796
|
|
|
3477
|
-
|
|
3478
|
-
this
|
|
3479
|
-
|
|
3797
|
+
get editor() {
|
|
3798
|
+
return this.toolbar.editor
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
get open() { return this.dialog.open }
|
|
3802
|
+
|
|
3803
|
+
#registerHandlers() {
|
|
3804
|
+
this.#setupKeydownHandler();
|
|
3805
|
+
this.dialog.addEventListener("cancel", this.#handleCancel.bind(this));
|
|
3806
|
+
this.dialog.addEventListener("close", this.#handleClose.bind(this));
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
#handleClose() {
|
|
3810
|
+
this.#removeClickOutsideHandler();
|
|
3811
|
+
this.triggerButton = null;
|
|
3812
|
+
this.editor.focus();
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
#handleCancel() {
|
|
3816
|
+
this.preventImmediateReopen = true;
|
|
3817
|
+
requestAnimationFrame(() => this.preventImmediateReopen = undefined);
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
#positionDialog() {
|
|
3821
|
+
const left = this.triggerButton.offsetLeft;
|
|
3822
|
+
this.dialog.style.insetInlineStart = `${left}px`;
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
#setupClickOutsideHandler() {
|
|
3826
|
+
if (this.#browserHandlesClose || this.clickOutsideHandler) return
|
|
3827
|
+
|
|
3828
|
+
this.clickOutsideHandler = this.#handleClickOutside.bind(this);
|
|
3829
|
+
document.addEventListener("click", this.clickOutsideHandler, true);
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3832
|
+
#removeClickOutsideHandler() {
|
|
3833
|
+
if (!this.clickOutsideHandler) return
|
|
3834
|
+
|
|
3835
|
+
document.removeEventListener("click", this.clickOutsideHandler, true);
|
|
3836
|
+
this.clickOutsideHandler = null;
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
#handleClickOutside({ target }) {
|
|
3840
|
+
if (!this.dialog.open) return
|
|
3841
|
+
|
|
3842
|
+
const isClickInsideDialog = this.dialog.contains(target);
|
|
3843
|
+
const isClickOnTrigger = this.triggerButton.contains(target);
|
|
3844
|
+
|
|
3845
|
+
if (!isClickInsideDialog && !isClickOnTrigger) {
|
|
3846
|
+
this.close();
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
#setupKeydownHandler() {
|
|
3851
|
+
if (!this.#browserHandlesClose) { this.addEventListener("keydown", this.#handleKeyDown.bind(this)); }
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
get #browserHandlesClose() {
|
|
3855
|
+
return this.dialog.closedBy === "any"
|
|
3480
3856
|
}
|
|
3481
3857
|
|
|
3482
3858
|
#handleKeyDown(event) {
|
|
@@ -3485,11 +3861,44 @@ class LinkDialog extends HTMLElement {
|
|
|
3485
3861
|
this.close();
|
|
3486
3862
|
}
|
|
3487
3863
|
}
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
class LinkDialog extends ToolbarDialog {
|
|
3867
|
+
connectedCallback() {
|
|
3868
|
+
super.connectedCallback();
|
|
3869
|
+
this.input = this.querySelector("input");
|
|
3870
|
+
|
|
3871
|
+
this.#registerHandlers();
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3874
|
+
updateStateCallback() {
|
|
3875
|
+
this.input.value = this.#selectedLinkUrl;
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
#registerHandlers() {
|
|
3879
|
+
this.dialog.addEventListener("beforetoggle", this.#handleBeforeToggle.bind(this));
|
|
3880
|
+
this.dialog.addEventListener("submit", this.#handleSubmit.bind(this));
|
|
3881
|
+
this.querySelector("[value='unlink']").addEventListener("click", this.#handleUnlink.bind(this));
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
#handleBeforeToggle({ newState }) {
|
|
3885
|
+
this.input.required = newState === "open";
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
#handleSubmit(event) {
|
|
3889
|
+
const command = event.submitter?.value;
|
|
3890
|
+
this.editor.dispatchCommand(command, this.input.value);
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3893
|
+
#handleUnlink() {
|
|
3894
|
+
this.editor.dispatchCommand("unlink");
|
|
3895
|
+
this.close();
|
|
3896
|
+
}
|
|
3488
3897
|
|
|
3489
3898
|
get #selectedLinkUrl() {
|
|
3490
3899
|
let url = "";
|
|
3491
3900
|
|
|
3492
|
-
this
|
|
3901
|
+
this.editor.getEditorState().read(() => {
|
|
3493
3902
|
const selection = $getSelection();
|
|
3494
3903
|
if (!$isRangeSelection(selection)) return
|
|
3495
3904
|
|
|
@@ -3505,16 +3914,105 @@ class LinkDialog extends HTMLElement {
|
|
|
3505
3914
|
|
|
3506
3915
|
return url
|
|
3507
3916
|
}
|
|
3508
|
-
|
|
3509
|
-
get #editor() {
|
|
3510
|
-
return this.closest("lexxy-toolbar").editor
|
|
3511
|
-
}
|
|
3512
3917
|
}
|
|
3513
3918
|
|
|
3514
3919
|
// We should extend the native dialog and avoid the intermediary <dialog> but not
|
|
3515
3920
|
// supported by Safari yet: customElements.define("lexxy-link-dialog", LinkDialog, { extends: "dialog" })
|
|
3516
3921
|
customElements.define("lexxy-link-dialog", LinkDialog);
|
|
3517
3922
|
|
|
3923
|
+
const APPLY_HIGHLIGHT_SELECTOR = "button.lexxy-highlight-button";
|
|
3924
|
+
const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
|
|
3925
|
+
|
|
3926
|
+
class HighlightDialog extends ToolbarDialog {
|
|
3927
|
+
connectedCallback() {
|
|
3928
|
+
super.connectedCallback();
|
|
3929
|
+
|
|
3930
|
+
this.#setUpButtons();
|
|
3931
|
+
this.#registerHandlers();
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
updateStateCallback() {
|
|
3935
|
+
this.#updateColorButtonStates($getSelection());
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
#registerHandlers() {
|
|
3939
|
+
this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).addEventListener("click", this.#handleRemoveHighlightClick.bind(this));
|
|
3940
|
+
this.#colorButtons.forEach(button => button.addEventListener("click", this.#handleColorButtonClick.bind(this)));
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
#setUpButtons() {
|
|
3944
|
+
this.#buttonGroups.forEach(buttonGroup => {
|
|
3945
|
+
this.#populateButtonGroup(buttonGroup);
|
|
3946
|
+
});
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
#populateButtonGroup(buttonGroup) {
|
|
3950
|
+
const values = buttonGroup.dataset.values?.split("; ") || [];
|
|
3951
|
+
const attribute = buttonGroup.dataset.buttonGroup;
|
|
3952
|
+
values.forEach((value, index) => {
|
|
3953
|
+
buttonGroup.appendChild(this.#createButton(attribute, value, index));
|
|
3954
|
+
});
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
#createButton(attribute, value, index) {
|
|
3958
|
+
const button = document.createElement("button");
|
|
3959
|
+
button.dataset.style = attribute;
|
|
3960
|
+
button.style.setProperty(attribute, value);
|
|
3961
|
+
button.dataset.value = value;
|
|
3962
|
+
button.classList.add("lexxy-highlight-button");
|
|
3963
|
+
button.name = attribute + "-" + index;
|
|
3964
|
+
return button
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
#handleColorButtonClick(event) {
|
|
3968
|
+
event.preventDefault();
|
|
3969
|
+
|
|
3970
|
+
const button = event.target.closest(APPLY_HIGHLIGHT_SELECTOR);
|
|
3971
|
+
if (!button) return
|
|
3972
|
+
|
|
3973
|
+
const attribute = button.dataset.style;
|
|
3974
|
+
const value = button.dataset.value;
|
|
3975
|
+
|
|
3976
|
+
this.editor.dispatchCommand("toggleHighlight", { [attribute]: value });
|
|
3977
|
+
this.close();
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
#handleRemoveHighlightClick(event) {
|
|
3981
|
+
event.preventDefault();
|
|
3982
|
+
|
|
3983
|
+
this.editor.dispatchCommand("removeHighlight");
|
|
3984
|
+
this.close();
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
#updateColorButtonStates(selection) {
|
|
3988
|
+
if (!$isRangeSelection(selection)) { return }
|
|
3989
|
+
|
|
3990
|
+
// Use null default, so "" indicates mixed highlighting
|
|
3991
|
+
const textColor = $getSelectionStyleValueForProperty(selection, "color", null);
|
|
3992
|
+
const backgroundColor = $getSelectionStyleValueForProperty(selection, "background-color", null);
|
|
3993
|
+
|
|
3994
|
+
this.#colorButtons.forEach(button => {
|
|
3995
|
+
const matchesSelection = button.dataset.value === textColor || button.dataset.value === backgroundColor;
|
|
3996
|
+
button.setAttribute("aria-pressed", matchesSelection);
|
|
3997
|
+
});
|
|
3998
|
+
|
|
3999
|
+
const hasHighlight = textColor !== null || backgroundColor !== null;
|
|
4000
|
+
this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).disabled = !hasHighlight;
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
get #buttonGroups() {
|
|
4004
|
+
return this.querySelectorAll("[data-button-group]")
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
get #colorButtons() {
|
|
4008
|
+
return Array.from(this.querySelectorAll(APPLY_HIGHLIGHT_SELECTOR))
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
// We should extend the native dialog and avoid the intermediary <dialog> but not
|
|
4013
|
+
// supported by Safari yet: customElements.define("lexxy-hightlight-dialog", HighlightDialog, { extends: "dialog" })
|
|
4014
|
+
customElements.define("lexxy-highlight-dialog", HighlightDialog);
|
|
4015
|
+
|
|
3518
4016
|
class BaseSource {
|
|
3519
4017
|
// Template method to override
|
|
3520
4018
|
async buildListItems(filter = "") {
|
|
@@ -3681,6 +4179,14 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
3681
4179
|
return this.hasAttribute("supports-space-in-searches")
|
|
3682
4180
|
}
|
|
3683
4181
|
|
|
4182
|
+
get open() {
|
|
4183
|
+
return this.popoverElement?.classList?.contains("lexxy-prompt-menu--visible")
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
get closed() {
|
|
4187
|
+
return !this.open
|
|
4188
|
+
}
|
|
4189
|
+
|
|
3684
4190
|
get #doesSpaceSelect() {
|
|
3685
4191
|
return !this.supportsSpaceInSearches
|
|
3686
4192
|
}
|
|
@@ -3701,28 +4207,62 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
3701
4207
|
#addTriggerListener() {
|
|
3702
4208
|
const unregister = this.#editor.registerUpdateListener(() => {
|
|
3703
4209
|
this.#editor.read(() => {
|
|
3704
|
-
const
|
|
3705
|
-
if (!
|
|
3706
|
-
|
|
3707
|
-
if ($
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
4210
|
+
const { node, offset } = this.#selection.selectedNodeWithOffset();
|
|
4211
|
+
if (!node) return
|
|
4212
|
+
|
|
4213
|
+
if ($isTextNode(node) && offset > 0) {
|
|
4214
|
+
const fullText = node.getTextContent();
|
|
4215
|
+
const charBeforeCursor = fullText[offset - 1];
|
|
4216
|
+
|
|
4217
|
+
// Check if trigger is at the start of the text node (new line case) or preceded by space or newline
|
|
4218
|
+
if (charBeforeCursor === this.trigger) {
|
|
4219
|
+
const isAtStart = offset === 1;
|
|
4220
|
+
|
|
4221
|
+
const charBeforeTrigger = offset > 1 ? fullText[offset - 2] : null;
|
|
4222
|
+
const isPrecededBySpaceOrNewline = charBeforeTrigger === " " || charBeforeTrigger === "\n";
|
|
4223
|
+
|
|
4224
|
+
if (isAtStart || isPrecededBySpaceOrNewline) {
|
|
4225
|
+
unregister();
|
|
4226
|
+
this.#showPopover();
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
3711
4229
|
}
|
|
4230
|
+
});
|
|
4231
|
+
});
|
|
4232
|
+
}
|
|
4233
|
+
|
|
4234
|
+
#addCursorPositionListener() {
|
|
4235
|
+
this.cursorPositionListener = this.#editor.registerUpdateListener(() => {
|
|
4236
|
+
if (this.closed) return
|
|
3712
4237
|
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
4238
|
+
this.#editor.read(() => {
|
|
4239
|
+
const { node, offset } = this.#selection.selectedNodeWithOffset();
|
|
4240
|
+
if (!node) return
|
|
4241
|
+
|
|
4242
|
+
if ($isTextNode(node) && offset > 0) {
|
|
4243
|
+
const fullText = node.getTextContent();
|
|
4244
|
+
const textBeforeCursor = fullText.slice(0, offset);
|
|
4245
|
+
const lastTriggerIndex = textBeforeCursor.lastIndexOf(this.trigger);
|
|
3716
4246
|
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
this.#
|
|
4247
|
+
// If trigger is not found, or cursor is at or before the trigger position, hide popover
|
|
4248
|
+
if (lastTriggerIndex === -1 || offset <= lastTriggerIndex) {
|
|
4249
|
+
this.#hidePopover();
|
|
3720
4250
|
}
|
|
4251
|
+
} else {
|
|
4252
|
+
// Cursor is not in a text node or at offset 0, hide popover
|
|
4253
|
+
this.#hidePopover();
|
|
3721
4254
|
}
|
|
3722
4255
|
});
|
|
3723
4256
|
});
|
|
3724
4257
|
}
|
|
3725
4258
|
|
|
4259
|
+
#removeCursorPositionListener() {
|
|
4260
|
+
if (this.cursorPositionListener) {
|
|
4261
|
+
this.cursorPositionListener();
|
|
4262
|
+
this.cursorPositionListener = null;
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
|
|
3726
4266
|
get #editor() {
|
|
3727
4267
|
return this.#editorElement.editor
|
|
3728
4268
|
}
|
|
@@ -3746,6 +4286,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
3746
4286
|
this.#editorElement.addEventListener("lexxy:change", this.#filterOptions);
|
|
3747
4287
|
|
|
3748
4288
|
this.#registerKeyListeners();
|
|
4289
|
+
this.#addCursorPositionListener();
|
|
3749
4290
|
}
|
|
3750
4291
|
|
|
3751
4292
|
#registerKeyListeners() {
|
|
@@ -3756,6 +4297,22 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
3756
4297
|
if (this.#doesSpaceSelect) {
|
|
3757
4298
|
this.keyListeners.push(this.#editor.registerCommand(KEY_SPACE_COMMAND, this.#handleSelectedOption.bind(this), COMMAND_PRIORITY_HIGH));
|
|
3758
4299
|
}
|
|
4300
|
+
|
|
4301
|
+
// Register arrow keys with HIGH priority to prevent Lexical's selection handlers from running
|
|
4302
|
+
this.keyListeners.push(this.#editor.registerCommand(KEY_ARROW_UP_COMMAND, this.#handleArrowUp.bind(this), COMMAND_PRIORITY_HIGH));
|
|
4303
|
+
this.keyListeners.push(this.#editor.registerCommand(KEY_ARROW_DOWN_COMMAND, this.#handleArrowDown.bind(this), COMMAND_PRIORITY_HIGH));
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
#handleArrowUp(event) {
|
|
4307
|
+
this.#moveSelectionUp();
|
|
4308
|
+
event.preventDefault();
|
|
4309
|
+
return true
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4312
|
+
#handleArrowDown(event) {
|
|
4313
|
+
this.#moveSelectionDown();
|
|
4314
|
+
event.preventDefault();
|
|
4315
|
+
return true
|
|
3759
4316
|
}
|
|
3760
4317
|
|
|
3761
4318
|
#selectFirstOption() {
|
|
@@ -3773,8 +4330,14 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
3773
4330
|
#selectOption(listItem) {
|
|
3774
4331
|
this.#clearSelection();
|
|
3775
4332
|
listItem.toggleAttribute("aria-selected", true);
|
|
4333
|
+
listItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
3776
4334
|
listItem.focus();
|
|
3777
|
-
|
|
4335
|
+
|
|
4336
|
+
// Preserve selection to prevent cursor jump
|
|
4337
|
+
this.#selection.preservingSelection(() => {
|
|
4338
|
+
this.#editorElement.focus();
|
|
4339
|
+
});
|
|
4340
|
+
|
|
3778
4341
|
this.#editorContentElement.setAttribute("aria-controls", this.popoverElement.id);
|
|
3779
4342
|
this.#editorContentElement.setAttribute("aria-activedescendant", listItem.id);
|
|
3780
4343
|
this.#editorContentElement.setAttribute("aria-haspopup", "listbox");
|
|
@@ -3823,6 +4386,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
3823
4386
|
this.#editorElement.removeEventListener("keydown", this.#handleKeydownOnPopover);
|
|
3824
4387
|
|
|
3825
4388
|
this.#unregisterKeyListeners();
|
|
4389
|
+
this.#removeCursorPositionListener();
|
|
3826
4390
|
|
|
3827
4391
|
await nextFrame();
|
|
3828
4392
|
this.#addTriggerListener();
|
|
@@ -3841,6 +4405,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
3841
4405
|
|
|
3842
4406
|
if (this.#editorContents.containsTextBackUntil(this.trigger)) {
|
|
3843
4407
|
await this.#showFilteredOptions();
|
|
4408
|
+
await nextFrame();
|
|
3844
4409
|
this.#positionPopover();
|
|
3845
4410
|
} else {
|
|
3846
4411
|
this.#hidePopover();
|
|
@@ -3881,15 +4446,8 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
3881
4446
|
this.#hidePopover();
|
|
3882
4447
|
this.#editorElement.focus();
|
|
3883
4448
|
event.stopPropagation();
|
|
3884
|
-
} else if (event.key === "ArrowDown") {
|
|
3885
|
-
this.#moveSelectionDown();
|
|
3886
|
-
event.preventDefault();
|
|
3887
|
-
event.stopPropagation();
|
|
3888
|
-
} else if (event.key === "ArrowUp") {
|
|
3889
|
-
this.#moveSelectionUp();
|
|
3890
|
-
event.preventDefault();
|
|
3891
|
-
event.stopPropagation();
|
|
3892
4449
|
}
|
|
4450
|
+
// Arrow keys are now handled via Lexical commands with HIGH priority
|
|
3893
4451
|
}
|
|
3894
4452
|
|
|
3895
4453
|
#moveSelectionDown() {
|
|
@@ -3911,7 +4469,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
3911
4469
|
}
|
|
3912
4470
|
|
|
3913
4471
|
#handleSelectedOption(event) {
|
|
3914
|
-
|
|
4472
|
+
event.preventDefault();
|
|
3915
4473
|
event.stopPropagation();
|
|
3916
4474
|
this.#optionWasSelected();
|
|
3917
4475
|
return true
|