@37signals/lexxy 0.1.20-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 +523 -82
- 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();
|
|
@@ -1133,6 +1235,7 @@ class CommandDispatcher {
|
|
|
1133
1235
|
const input = createElement("input", {
|
|
1134
1236
|
type: "file",
|
|
1135
1237
|
multiple: true,
|
|
1238
|
+
style: "display: none;",
|
|
1136
1239
|
onchange: ({ target }) => {
|
|
1137
1240
|
const files = Array.from(target.files);
|
|
1138
1241
|
if (!files.length) return
|
|
@@ -1143,7 +1246,7 @@ class CommandDispatcher {
|
|
|
1143
1246
|
}
|
|
1144
1247
|
});
|
|
1145
1248
|
|
|
1146
|
-
|
|
1249
|
+
this.editorElement.appendChild(input); // Append and remove just for the sake of making it testable
|
|
1147
1250
|
input.click();
|
|
1148
1251
|
setTimeout(() => input.remove(), 1000);
|
|
1149
1252
|
}
|
|
@@ -1274,8 +1377,10 @@ class Selection {
|
|
|
1274
1377
|
|
|
1275
1378
|
set current(selection) {
|
|
1276
1379
|
if ($isNodeSelection(selection)) {
|
|
1277
|
-
this.
|
|
1278
|
-
|
|
1380
|
+
this.editor.getEditorState().read(() => {
|
|
1381
|
+
this._current = $getSelection();
|
|
1382
|
+
this.#syncSelectedClasses();
|
|
1383
|
+
});
|
|
1279
1384
|
} else {
|
|
1280
1385
|
this.editor.update(() => {
|
|
1281
1386
|
this.#syncSelectedClasses();
|
|
@@ -1459,8 +1564,9 @@ class Selection {
|
|
|
1459
1564
|
|
|
1460
1565
|
this._currentlySelectedKeys = new Set();
|
|
1461
1566
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1567
|
+
const selection = $getSelection();
|
|
1568
|
+
if (selection && $isNodeSelection(selection)) {
|
|
1569
|
+
for (const node of selection.getNodes()) {
|
|
1464
1570
|
this._currentlySelectedKeys.add(node.getKey());
|
|
1465
1571
|
}
|
|
1466
1572
|
}
|
|
@@ -2650,8 +2756,6 @@ class Contents {
|
|
|
2650
2756
|
elements.forEach((element) => {
|
|
2651
2757
|
wrappingNode.append(element);
|
|
2652
2758
|
});
|
|
2653
|
-
|
|
2654
|
-
$setSelection(null);
|
|
2655
2759
|
});
|
|
2656
2760
|
}
|
|
2657
2761
|
|
|
@@ -3127,6 +3231,132 @@ class Clipboard {
|
|
|
3127
3231
|
}
|
|
3128
3232
|
}
|
|
3129
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
|
+
|
|
3130
3360
|
class LexicalEditorElement extends HTMLElement {
|
|
3131
3361
|
static formAssociated = true
|
|
3132
3362
|
static debug = true
|
|
@@ -3149,6 +3379,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3149
3379
|
this.contents = new Contents(this);
|
|
3150
3380
|
this.selection = new Selection(this);
|
|
3151
3381
|
this.clipboard = new Clipboard(this);
|
|
3382
|
+
this.highlighter = new Highlighter(this);
|
|
3152
3383
|
|
|
3153
3384
|
CommandDispatcher.configureFor(this);
|
|
3154
3385
|
this.#initialize();
|
|
@@ -3184,6 +3415,10 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3184
3415
|
return this.internals.form
|
|
3185
3416
|
}
|
|
3186
3417
|
|
|
3418
|
+
get name() {
|
|
3419
|
+
return this.getAttribute("name")
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3187
3422
|
get toolbarElement() {
|
|
3188
3423
|
if (!this.#hasToolbar) return null
|
|
3189
3424
|
|
|
@@ -3259,6 +3494,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3259
3494
|
this.#registerComponents();
|
|
3260
3495
|
this.#listenForInvalidatedNodes();
|
|
3261
3496
|
this.#handleEnter();
|
|
3497
|
+
this.#handleFocus();
|
|
3262
3498
|
this.#attachDebugHooks();
|
|
3263
3499
|
this.#attachToolbar();
|
|
3264
3500
|
this.#loadInitialValue();
|
|
@@ -3284,6 +3520,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3284
3520
|
|
|
3285
3521
|
get #lexicalNodes() {
|
|
3286
3522
|
const nodes = [
|
|
3523
|
+
TrixTextNode,
|
|
3524
|
+
HighlightNode,
|
|
3287
3525
|
QuoteNode,
|
|
3288
3526
|
HeadingNode,
|
|
3289
3527
|
ListNode,
|
|
@@ -3437,6 +3675,14 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3437
3675
|
);
|
|
3438
3676
|
}
|
|
3439
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
|
+
|
|
3440
3686
|
#attachDebugHooks() {
|
|
3441
3687
|
if (!LexicalEditorElement.debug) return
|
|
3442
3688
|
|
|
@@ -3475,7 +3721,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3475
3721
|
}
|
|
3476
3722
|
|
|
3477
3723
|
get #isEmpty() {
|
|
3478
|
-
return
|
|
3724
|
+
return [ "<p><br></p>", "<p></p>", "" ].includes(this.value.trim())
|
|
3479
3725
|
}
|
|
3480
3726
|
|
|
3481
3727
|
#setValidity() {
|
|
@@ -3515,33 +3761,98 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3515
3761
|
|
|
3516
3762
|
customElements.define("lexxy-editor", LexicalEditorElement);
|
|
3517
3763
|
|
|
3518
|
-
class
|
|
3764
|
+
class ToolbarDialog extends HTMLElement {
|
|
3519
3765
|
connectedCallback() {
|
|
3520
3766
|
this.dialog = this.querySelector("dialog");
|
|
3521
|
-
|
|
3767
|
+
if ("closedBy" in this.dialog.constructor.prototype) {
|
|
3768
|
+
this.dialog.closedBy = "any";
|
|
3769
|
+
}
|
|
3770
|
+
this.#registerHandlers();
|
|
3771
|
+
}
|
|
3522
3772
|
|
|
3523
|
-
|
|
3524
|
-
this
|
|
3525
|
-
this.addEventListener("keydown", this.#handleKeyDown.bind(this));
|
|
3773
|
+
disconnectedCallback() {
|
|
3774
|
+
this.#removeClickOutsideHandler();
|
|
3526
3775
|
}
|
|
3527
3776
|
|
|
3528
|
-
|
|
3529
|
-
|
|
3777
|
+
updateStateCallback() { }
|
|
3778
|
+
|
|
3779
|
+
show(triggerButton) {
|
|
3780
|
+
if (this.preventImmediateReopen) { return }
|
|
3781
|
+
|
|
3782
|
+
this.triggerButton = triggerButton;
|
|
3783
|
+
this.#positionDialog();
|
|
3530
3784
|
this.dialog.show();
|
|
3785
|
+
|
|
3786
|
+
this.#setupClickOutsideHandler();
|
|
3531
3787
|
}
|
|
3532
3788
|
|
|
3533
3789
|
close() {
|
|
3534
3790
|
this.dialog.close();
|
|
3535
3791
|
}
|
|
3536
3792
|
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
this.#editor.dispatchCommand(command, this.input.value);
|
|
3793
|
+
get toolbar() {
|
|
3794
|
+
return this.closest("lexxy-toolbar")
|
|
3540
3795
|
}
|
|
3541
3796
|
|
|
3542
|
-
|
|
3543
|
-
this
|
|
3544
|
-
|
|
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"
|
|
3545
3856
|
}
|
|
3546
3857
|
|
|
3547
3858
|
#handleKeyDown(event) {
|
|
@@ -3550,11 +3861,44 @@ class LinkDialog extends HTMLElement {
|
|
|
3550
3861
|
this.close();
|
|
3551
3862
|
}
|
|
3552
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
|
+
}
|
|
3553
3897
|
|
|
3554
3898
|
get #selectedLinkUrl() {
|
|
3555
3899
|
let url = "";
|
|
3556
3900
|
|
|
3557
|
-
this
|
|
3901
|
+
this.editor.getEditorState().read(() => {
|
|
3558
3902
|
const selection = $getSelection();
|
|
3559
3903
|
if (!$isRangeSelection(selection)) return
|
|
3560
3904
|
|
|
@@ -3570,16 +3914,105 @@ class LinkDialog extends HTMLElement {
|
|
|
3570
3914
|
|
|
3571
3915
|
return url
|
|
3572
3916
|
}
|
|
3573
|
-
|
|
3574
|
-
get #editor() {
|
|
3575
|
-
return this.closest("lexxy-toolbar").editor
|
|
3576
|
-
}
|
|
3577
3917
|
}
|
|
3578
3918
|
|
|
3579
3919
|
// We should extend the native dialog and avoid the intermediary <dialog> but not
|
|
3580
3920
|
// supported by Safari yet: customElements.define("lexxy-link-dialog", LinkDialog, { extends: "dialog" })
|
|
3581
3921
|
customElements.define("lexxy-link-dialog", LinkDialog);
|
|
3582
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
|
+
|
|
3583
4016
|
class BaseSource {
|
|
3584
4017
|
// Template method to override
|
|
3585
4018
|
async buildListItems(filter = "") {
|
|
@@ -3781,9 +4214,17 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
3781
4214
|
const fullText = node.getTextContent();
|
|
3782
4215
|
const charBeforeCursor = fullText[offset - 1];
|
|
3783
4216
|
|
|
4217
|
+
// Check if trigger is at the start of the text node (new line case) or preceded by space or newline
|
|
3784
4218
|
if (charBeforeCursor === this.trigger) {
|
|
3785
|
-
|
|
3786
|
-
|
|
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
|
+
}
|
|
3787
4228
|
}
|
|
3788
4229
|
}
|
|
3789
4230
|
});
|
|
@@ -4028,7 +4469,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
4028
4469
|
}
|
|
4029
4470
|
|
|
4030
4471
|
#handleSelectedOption(event) {
|
|
4031
|
-
|
|
4472
|
+
event.preventDefault();
|
|
4032
4473
|
event.stopPropagation();
|
|
4033
4474
|
this.#optionWasSelected();
|
|
4034
4475
|
return true
|