@37signals/lexxy 0.1.20-beta → 0.1.22-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 +570 -86
- 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.21.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,15 +1,51 @@
|
|
|
1
1
|
import DOMPurify from 'dompurify';
|
|
2
|
-
import {
|
|
2
|
+
import { getStyleObjectFromCSS, getCSSFromStyleObject, $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/selection';
|
|
3
|
+
import { $isTextNode, TextNode, $isRangeSelection, $getSelection, 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';
|
|
8
|
+
import 'prismjs/components/prism-ruby';
|
|
9
|
+
import 'prismjs/components/prism-php';
|
|
10
|
+
import 'prismjs/components/prism-go';
|
|
11
|
+
import 'prismjs/components/prism-bash';
|
|
12
|
+
import 'prismjs/components/prism-json';
|
|
13
|
+
import 'prismjs/components/prism-diff';
|
|
7
14
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
8
15
|
import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown';
|
|
9
16
|
import { createEmptyHistoryState, registerHistory } from '@lexical/history';
|
|
10
17
|
import { DirectUpload } from '@rails/activestorage';
|
|
11
18
|
import { marked } from 'marked';
|
|
12
|
-
|
|
19
|
+
|
|
20
|
+
const ALLOWED_HTML_TAGS = [ "a", "action-text-attachment", "b", "blockquote", "br", "code", "em",
|
|
21
|
+
"figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "li", "mark", "ol", "p", "pre", "q", "s", "strong", "ul" ];
|
|
22
|
+
|
|
23
|
+
const ALLOWED_HTML_ATTRIBUTES = [ "alt", "caption", "class", "content", "content-type", "contenteditable",
|
|
24
|
+
"data-direct-upload-id", "data-sgid", "filename", "filesize", "height", "href", "presentation",
|
|
25
|
+
"previewable", "sgid", "src", "style", "title", "url", "width" ];
|
|
26
|
+
|
|
27
|
+
const ALLOWED_STYLE_PROPERTIES = [ "color", "background-color" ];
|
|
28
|
+
|
|
29
|
+
function styleFilterHook(_currentNode, hookEvent) {
|
|
30
|
+
if (hookEvent.attrName === "style" && hookEvent.attrValue) {
|
|
31
|
+
const styles = { ...getStyleObjectFromCSS(hookEvent.attrValue) };
|
|
32
|
+
const sanitizedStyles = { };
|
|
33
|
+
|
|
34
|
+
for (const property in styles) {
|
|
35
|
+
if (ALLOWED_STYLE_PROPERTIES.includes(property)) {
|
|
36
|
+
sanitizedStyles[property] = styles[property];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (Object.keys(sanitizedStyles).length) {
|
|
41
|
+
hookEvent.attrValue = getCSSFromStyleObject(sanitizedStyles);
|
|
42
|
+
} else {
|
|
43
|
+
hookEvent.keepAttr = false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
DOMPurify.addHook("uponSanitizeAttribute", styleFilterHook);
|
|
13
49
|
|
|
14
50
|
DOMPurify.addHook("uponSanitizeElement", (node, data) => {
|
|
15
51
|
if (data.tagName === "strong" || data.tagName === "em") {
|
|
@@ -17,6 +53,12 @@ DOMPurify.addHook("uponSanitizeElement", (node, data) => {
|
|
|
17
53
|
}
|
|
18
54
|
});
|
|
19
55
|
|
|
56
|
+
DOMPurify.setConfig({
|
|
57
|
+
ALLOWED_TAGS: ALLOWED_HTML_TAGS,
|
|
58
|
+
ALLOWED_ATTR: ALLOWED_HTML_ATTRIBUTES,
|
|
59
|
+
SAFE_FOR_XML: false // So that it does not strip attributes that contains serialized HTML (like content)
|
|
60
|
+
});
|
|
61
|
+
|
|
20
62
|
function getNonce() {
|
|
21
63
|
const element = document.head.querySelector("meta[name=csp-nonce]");
|
|
22
64
|
return element?.content
|
|
@@ -53,6 +95,46 @@ function isPrintableCharacter(event) {
|
|
|
53
95
|
return event.key.length === 1
|
|
54
96
|
}
|
|
55
97
|
|
|
98
|
+
function extendTextNodeConversion(conversionName, callback = (textNode => textNode)) {
|
|
99
|
+
return extendConversion(TextNode, conversionName, (conversionOutput, element) => ({
|
|
100
|
+
...conversionOutput,
|
|
101
|
+
forChild: (lexicalNode, parentNode) => {
|
|
102
|
+
const originalForChild = conversionOutput?.forChild ?? (x => x);
|
|
103
|
+
let childNode = originalForChild(lexicalNode, parentNode);
|
|
104
|
+
|
|
105
|
+
if ($isTextNode(childNode)) childNode = callback(childNode, element) ?? childNode;
|
|
106
|
+
return childNode
|
|
107
|
+
}
|
|
108
|
+
}))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function extendConversion(nodeKlass, conversionName, callback = (output => output)) {
|
|
112
|
+
return (element) => {
|
|
113
|
+
const converter = nodeKlass.importDOM()?.[conversionName]?.(element);
|
|
114
|
+
if (!converter) return null
|
|
115
|
+
|
|
116
|
+
const conversionOutput = converter.conversion(element);
|
|
117
|
+
if (!conversionOutput) return conversionOutput
|
|
118
|
+
|
|
119
|
+
return callback(conversionOutput, element) ?? conversionOutput
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isSelectionHighlighted(selection) {
|
|
124
|
+
if (!$isRangeSelection(selection)) return false
|
|
125
|
+
|
|
126
|
+
if (selection.isCollapsed()) {
|
|
127
|
+
return hasHighlightStyles(selection.style)
|
|
128
|
+
} else {
|
|
129
|
+
return selection.hasFormat("highlight")
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function hasHighlightStyles(cssOrStyles) {
|
|
134
|
+
const styles = typeof cssOrStyles === "string" ? getStyleObjectFromCSS(cssOrStyles) : cssOrStyles;
|
|
135
|
+
return !!(styles.color || styles["background-color"])
|
|
136
|
+
}
|
|
137
|
+
|
|
56
138
|
class LexicalToolbarElement extends HTMLElement {
|
|
57
139
|
constructor() {
|
|
58
140
|
super();
|
|
@@ -83,6 +165,14 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
83
165
|
this.#monitorSelectionChanges();
|
|
84
166
|
this.#monitorHistoryChanges();
|
|
85
167
|
this.#refreshToolbarOverflow();
|
|
168
|
+
|
|
169
|
+
this.toggleAttribute("connected", true);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
get #dialogs() {
|
|
173
|
+
const dialogButtons = this.querySelectorAll("[data-dialog-target]");
|
|
174
|
+
const dialogTags = Array.from(dialogButtons).map(button => `lexxy-${button.dataset.dialogTarget}`);
|
|
175
|
+
return Array.from(this.querySelectorAll(dialogTags))
|
|
86
176
|
}
|
|
87
177
|
|
|
88
178
|
#bindButtons() {
|
|
@@ -108,15 +198,25 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
108
198
|
|
|
109
199
|
// Not using popover because of CSS anchoring still not widely available.
|
|
110
200
|
#toggleDialog(button) {
|
|
111
|
-
const
|
|
201
|
+
const dialogTarget = button.dataset.dialogTarget;
|
|
202
|
+
const dialog = this.querySelector("lexxy-" + dialogTarget);
|
|
203
|
+
if (!dialog) return
|
|
112
204
|
|
|
113
205
|
if (dialog.open) {
|
|
114
206
|
dialog.close();
|
|
115
207
|
} else {
|
|
116
|
-
|
|
208
|
+
this.#closeOpenDialogs();
|
|
209
|
+
dialog.show(button);
|
|
117
210
|
}
|
|
118
211
|
}
|
|
119
212
|
|
|
213
|
+
#closeOpenDialogs() {
|
|
214
|
+
const openDialogs = this.querySelectorAll("dialog[open]");
|
|
215
|
+
openDialogs.forEach(openDialog => {
|
|
216
|
+
openDialog.closest(".lexxy-dialog").close();
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
120
220
|
#bindHotkeys() {
|
|
121
221
|
this.editorElement.addEventListener("keydown", (event) => {
|
|
122
222
|
const buttons = this.querySelectorAll("[data-hotkey]");
|
|
@@ -154,6 +254,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
154
254
|
this.editor.registerUpdateListener(() => {
|
|
155
255
|
this.editor.getEditorState().read(() => {
|
|
156
256
|
this.#updateButtonStates();
|
|
257
|
+
this.#updateDialogStates();
|
|
157
258
|
});
|
|
158
259
|
});
|
|
159
260
|
}
|
|
@@ -174,14 +275,6 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
174
275
|
});
|
|
175
276
|
}
|
|
176
277
|
|
|
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
278
|
#updateButtonStates() {
|
|
186
279
|
const selection = $getSelection();
|
|
187
280
|
if (!$isRangeSelection(selection)) return
|
|
@@ -194,26 +287,32 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
194
287
|
const isBold = selection.hasFormat("bold");
|
|
195
288
|
const isItalic = selection.hasFormat("italic");
|
|
196
289
|
const isStrikethrough = selection.hasFormat("strikethrough");
|
|
290
|
+
const isHighlight = isSelectionHighlighted(selection);
|
|
291
|
+
const isInLink = this.#isInLink(anchorNode);
|
|
292
|
+
const isInQuote = $isQuoteNode(topLevelElement);
|
|
293
|
+
const isInHeading = $isHeadingNode(topLevelElement);
|
|
197
294
|
const isInCode = $isCodeNode(topLevelElement) || selection.hasFormat("code");
|
|
198
295
|
const isInList = this.#isInList(anchorNode);
|
|
199
296
|
const listType = getListType(anchorNode);
|
|
200
|
-
const isInQuote = $isQuoteNode(topLevelElement);
|
|
201
|
-
const isInHeading = $isHeadingNode(topLevelElement);
|
|
202
|
-
const isInLink = this.#isInLink(anchorNode);
|
|
203
297
|
|
|
204
298
|
this.#setButtonPressed("bold", isBold);
|
|
205
299
|
this.#setButtonPressed("italic", isItalic);
|
|
206
300
|
this.#setButtonPressed("strikethrough", isStrikethrough);
|
|
301
|
+
this.#setButtonPressed("highlight", isHighlight);
|
|
302
|
+
this.#setButtonPressed("link", isInLink);
|
|
303
|
+
this.#setButtonPressed("quote", isInQuote);
|
|
304
|
+
this.#setButtonPressed("heading", isInHeading);
|
|
207
305
|
this.#setButtonPressed("code", isInCode);
|
|
208
306
|
this.#setButtonPressed("unordered-list", isInList && listType === "bullet");
|
|
209
307
|
this.#setButtonPressed("ordered-list", isInList && listType === "number");
|
|
210
|
-
this.#setButtonPressed("quote", isInQuote);
|
|
211
|
-
this.#setButtonPressed("heading", isInHeading);
|
|
212
|
-
this.#setButtonPressed("link", isInLink);
|
|
213
308
|
|
|
214
309
|
this.#updateUndoRedoButtonStates();
|
|
215
310
|
}
|
|
216
311
|
|
|
312
|
+
#updateDialogStates() {
|
|
313
|
+
this.#dialogs.forEach(dialog => dialog.updateStateCallback());
|
|
314
|
+
}
|
|
315
|
+
|
|
217
316
|
#isInList(node) {
|
|
218
317
|
let current = node;
|
|
219
318
|
while (current) {
|
|
@@ -239,6 +338,14 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
239
338
|
}
|
|
240
339
|
}
|
|
241
340
|
|
|
341
|
+
#setButtonDisabled(name, isDisabled) {
|
|
342
|
+
const button = this.querySelector(`[name="${name}"]`);
|
|
343
|
+
if (button) {
|
|
344
|
+
button.disabled = isDisabled;
|
|
345
|
+
button.setAttribute("aria-disabled", isDisabled.toString());
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
242
349
|
#toolbarIsOverflowing() {
|
|
243
350
|
return this.scrollWidth > this.clientWidth
|
|
244
351
|
}
|
|
@@ -249,6 +356,9 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
249
356
|
|
|
250
357
|
this.#overflow.style.display = this.#overflowMenu.children.length ? "block" : "none";
|
|
251
358
|
this.#overflow.setAttribute("nonce", getNonce());
|
|
359
|
+
|
|
360
|
+
const isOverflowing = this.#overflowMenu.children.length > 0;
|
|
361
|
+
this.toggleAttribute("overflowing", isOverflowing);
|
|
252
362
|
}
|
|
253
363
|
|
|
254
364
|
get #overflow() {
|
|
@@ -266,7 +376,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
266
376
|
}
|
|
267
377
|
|
|
268
378
|
#compactMenu() {
|
|
269
|
-
const buttons = this.#
|
|
379
|
+
const buttons = this.#buttonsWithSeparator.reverse();
|
|
270
380
|
let movedToOverflow = false;
|
|
271
381
|
|
|
272
382
|
for (const button of buttons) {
|
|
@@ -281,6 +391,10 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
281
391
|
}
|
|
282
392
|
|
|
283
393
|
get #buttons() {
|
|
394
|
+
return Array.from(this.querySelectorAll(":scope > button"))
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
get #buttonsWithSeparator() {
|
|
284
398
|
return Array.from(this.querySelectorAll(":scope > button, :scope > [role=separator]"))
|
|
285
399
|
}
|
|
286
400
|
|
|
@@ -300,12 +414,22 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
300
414
|
</svg>
|
|
301
415
|
</button>
|
|
302
416
|
|
|
303
|
-
<
|
|
304
|
-
<
|
|
417
|
+
<lexxy-highlight-dialog class="lexxy-dialog lexxy-highlight-dialog">
|
|
418
|
+
<dialog class="highlight-dialog">
|
|
419
|
+
<div class="lexxy-highlight-dialog-content">
|
|
420
|
+
<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>
|
|
421
|
+
<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>
|
|
422
|
+
<button data-command="removeHighlight" class="lexxy-highlight-dialog-reset">Remove all coloring</button>
|
|
423
|
+
</div>
|
|
424
|
+
</dialog>
|
|
425
|
+
</lexxy-highlight-dialog>
|
|
426
|
+
|
|
427
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="highlight" title="Color highlight" data-dialog-target="highlight-dialog">
|
|
428
|
+
<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
429
|
</button>
|
|
306
430
|
|
|
307
|
-
<lexxy-link-dialog class="lexxy-link-dialog">
|
|
308
|
-
<dialog class="link-dialog"
|
|
431
|
+
<lexxy-link-dialog class="lexxy-dialog lexxy-link-dialog">
|
|
432
|
+
<dialog class="link-dialog">
|
|
309
433
|
<form method="dialog">
|
|
310
434
|
<input type="url" placeholder="Enter a URL…" class="input" required>
|
|
311
435
|
<div class="lexxy-dialog-actions">
|
|
@@ -316,6 +440,10 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
316
440
|
</dialog>
|
|
317
441
|
</lexxy-link-dialog>
|
|
318
442
|
|
|
443
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="link" title="Link" data-dialog-target="link-dialog" data-hotkey="cmd+k ctrl+k">
|
|
444
|
+
<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>
|
|
445
|
+
</button>
|
|
446
|
+
|
|
319
447
|
<button class="lexxy-editor__toolbar-button" type="button" name="quote" data-command="insertQuoteBlock" title="Quote">
|
|
320
448
|
<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
449
|
</button>
|
|
@@ -343,9 +471,9 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
343
471
|
<button class="lexxy-editor__toolbar-button" type="button" name="divider" data-command="insertHorizontalDivider" title="Insert a divider">
|
|
344
472
|
<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
473
|
</button>
|
|
346
|
-
|
|
474
|
+
|
|
347
475
|
<div class="lexxy-editor__toolbar-spacer" role="separator"></div>
|
|
348
|
-
|
|
476
|
+
|
|
349
477
|
<button class="lexxy-editor__toolbar-button" type="button" name="undo" data-command="undo" title="Undo" data-hotkey="cmd+z ctrl+z">
|
|
350
478
|
<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
479
|
</button>
|
|
@@ -370,8 +498,15 @@ var theme = {
|
|
|
370
498
|
italic: "lexxy-content__italic",
|
|
371
499
|
strikethrough: "lexxy-content__strikethrough",
|
|
372
500
|
underline: "lexxy-content__underline",
|
|
501
|
+
highlight: "lexxy-content__highlight"
|
|
502
|
+
},
|
|
503
|
+
list: {
|
|
504
|
+
nested: {
|
|
505
|
+
listitem: "lexxy-nested-listitem",
|
|
506
|
+
}
|
|
373
507
|
},
|
|
374
508
|
codeHighlight: {
|
|
509
|
+
addition: "code-token__selector",
|
|
375
510
|
atrule: "code-token__attr",
|
|
376
511
|
attr: "code-token__attr",
|
|
377
512
|
"attr-name": "code-token__attr",
|
|
@@ -386,24 +521,30 @@ var theme = {
|
|
|
386
521
|
color: "code-token__property",
|
|
387
522
|
comment: "code-token__comment",
|
|
388
523
|
constant: "code-token__property",
|
|
389
|
-
coord: "code-
|
|
524
|
+
coord: "code-token__comment",
|
|
390
525
|
decorator: "code-token__function",
|
|
391
|
-
deleted: "code-
|
|
526
|
+
deleted: "code-token__operator",
|
|
527
|
+
deletion: "code-token__operator",
|
|
528
|
+
directive: "code-token__attr",
|
|
529
|
+
"directive-hash": "code-token__property",
|
|
392
530
|
doctype: "code-token__comment",
|
|
393
531
|
entity: "code-token__operator",
|
|
394
532
|
function: "code-token__function",
|
|
395
533
|
hexcode: "code-token__property",
|
|
396
|
-
important: "code-
|
|
534
|
+
important: "code-token__function",
|
|
397
535
|
inserted: "code-token__selector",
|
|
398
536
|
italic: "code-token__comment",
|
|
399
537
|
keyword: "code-token__attr",
|
|
538
|
+
line: "code-token__selector",
|
|
400
539
|
namespace: "code-token__variable",
|
|
401
540
|
number: "code-token__property",
|
|
541
|
+
macro: "code-token__function",
|
|
402
542
|
operator: "code-token__operator",
|
|
403
543
|
parameter: "code-token__variable",
|
|
404
544
|
prolog: "code-token__comment",
|
|
405
545
|
property: "code-token__property",
|
|
406
546
|
punctuation: "code-token__punctuation",
|
|
547
|
+
"raw-string": "code-token__operator",
|
|
407
548
|
regex: "code-token__variable",
|
|
408
549
|
script: "code-token__function",
|
|
409
550
|
selector: "code-token__selector",
|
|
@@ -412,29 +553,18 @@ var theme = {
|
|
|
412
553
|
symbol: "code-token__property",
|
|
413
554
|
tag: "code-token__property",
|
|
414
555
|
title: "code-token__function",
|
|
556
|
+
"type-definition": "code-token__function",
|
|
415
557
|
url: "code-token__operator",
|
|
416
558
|
variable: "code-token__variable",
|
|
417
559
|
}
|
|
418
560
|
};
|
|
419
561
|
|
|
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
562
|
function createElement(name, properties) {
|
|
433
563
|
const element = document.createElement(name);
|
|
434
564
|
for (const [ key, value ] of Object.entries(properties || {})) {
|
|
435
565
|
if (key in element) {
|
|
436
566
|
element[key] = value;
|
|
437
|
-
} else if (value !== null && value !== undefined
|
|
567
|
+
} else if (value !== null && value !== undefined) {
|
|
438
568
|
element.setAttribute(key, value);
|
|
439
569
|
}
|
|
440
570
|
}
|
|
@@ -466,18 +596,8 @@ function dispatchCustomEvent(element, name, detail) {
|
|
|
466
596
|
element.dispatchEvent(event);
|
|
467
597
|
}
|
|
468
598
|
|
|
469
|
-
function containsVisuallyRelevantChildren(element) {
|
|
470
|
-
return element.querySelector(VISUALLY_RELEVANT_ELEMENTS_SELECTOR)
|
|
471
|
-
}
|
|
472
|
-
|
|
473
599
|
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
|
|
600
|
+
return DOMPurify.sanitize(html)
|
|
481
601
|
}
|
|
482
602
|
|
|
483
603
|
function dispatch(element, eventName, detail = null, cancelable = false) {
|
|
@@ -694,11 +814,10 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
694
814
|
|
|
695
815
|
#createEditableCaption() {
|
|
696
816
|
const caption = createElement("figcaption", { className: "attachment__caption" });
|
|
697
|
-
const input = createElement("
|
|
698
|
-
type: "text",
|
|
699
|
-
class: "input",
|
|
817
|
+
const input = createElement("textarea", {
|
|
700
818
|
value: this.caption,
|
|
701
|
-
placeholder: this.fileName
|
|
819
|
+
placeholder: this.fileName,
|
|
820
|
+
rows: "1"
|
|
702
821
|
});
|
|
703
822
|
|
|
704
823
|
input.addEventListener("focusin", () => input.placeholder = "Add caption...");
|
|
@@ -990,6 +1109,8 @@ const COMMANDS = [
|
|
|
990
1109
|
"strikethrough",
|
|
991
1110
|
"link",
|
|
992
1111
|
"unlink",
|
|
1112
|
+
"toggleHighlight",
|
|
1113
|
+
"removeHighlight",
|
|
993
1114
|
"rotateHeadingFormat",
|
|
994
1115
|
"insertUnorderedList",
|
|
995
1116
|
"insertOrderedList",
|
|
@@ -1012,6 +1133,7 @@ class CommandDispatcher {
|
|
|
1012
1133
|
this.selection = editorElement.selection;
|
|
1013
1134
|
this.contents = editorElement.contents;
|
|
1014
1135
|
this.clipboard = editorElement.clipboard;
|
|
1136
|
+
this.highlighter = editorElement.highlighter;
|
|
1015
1137
|
|
|
1016
1138
|
this.#registerCommands();
|
|
1017
1139
|
this.#registerDragAndDropHandlers();
|
|
@@ -1033,6 +1155,14 @@ class CommandDispatcher {
|
|
|
1033
1155
|
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough");
|
|
1034
1156
|
}
|
|
1035
1157
|
|
|
1158
|
+
dispatchToggleHighlight(styles) {
|
|
1159
|
+
this.highlighter.toggle(styles);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
dispatchRemoveHighlight() {
|
|
1163
|
+
this.highlighter.remove();
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1036
1166
|
dispatchLink(url) {
|
|
1037
1167
|
this.editor.update(() => {
|
|
1038
1168
|
const selection = $getSelection();
|
|
@@ -1133,6 +1263,7 @@ class CommandDispatcher {
|
|
|
1133
1263
|
const input = createElement("input", {
|
|
1134
1264
|
type: "file",
|
|
1135
1265
|
multiple: true,
|
|
1266
|
+
style: "display: none;",
|
|
1136
1267
|
onchange: ({ target }) => {
|
|
1137
1268
|
const files = Array.from(target.files);
|
|
1138
1269
|
if (!files.length) return
|
|
@@ -1143,7 +1274,7 @@ class CommandDispatcher {
|
|
|
1143
1274
|
}
|
|
1144
1275
|
});
|
|
1145
1276
|
|
|
1146
|
-
|
|
1277
|
+
this.editorElement.appendChild(input); // Append and remove just for the sake of making it testable
|
|
1147
1278
|
input.click();
|
|
1148
1279
|
setTimeout(() => input.remove(), 1000);
|
|
1149
1280
|
}
|
|
@@ -1274,8 +1405,10 @@ class Selection {
|
|
|
1274
1405
|
|
|
1275
1406
|
set current(selection) {
|
|
1276
1407
|
if ($isNodeSelection(selection)) {
|
|
1277
|
-
this.
|
|
1278
|
-
|
|
1408
|
+
this.editor.getEditorState().read(() => {
|
|
1409
|
+
this._current = $getSelection();
|
|
1410
|
+
this.#syncSelectedClasses();
|
|
1411
|
+
});
|
|
1279
1412
|
} else {
|
|
1280
1413
|
this.editor.update(() => {
|
|
1281
1414
|
this.#syncSelectedClasses();
|
|
@@ -1459,8 +1592,9 @@ class Selection {
|
|
|
1459
1592
|
|
|
1460
1593
|
this._currentlySelectedKeys = new Set();
|
|
1461
1594
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1595
|
+
const selection = $getSelection();
|
|
1596
|
+
if (selection && $isNodeSelection(selection)) {
|
|
1597
|
+
for (const node of selection.getNodes()) {
|
|
1464
1598
|
this._currentlySelectedKeys.add(node.getKey());
|
|
1465
1599
|
}
|
|
1466
1600
|
}
|
|
@@ -2650,8 +2784,6 @@ class Contents {
|
|
|
2650
2784
|
elements.forEach((element) => {
|
|
2651
2785
|
wrappingNode.append(element);
|
|
2652
2786
|
});
|
|
2653
|
-
|
|
2654
|
-
$setSelection(null);
|
|
2655
2787
|
});
|
|
2656
2788
|
}
|
|
2657
2789
|
|
|
@@ -3127,6 +3259,137 @@ class Clipboard {
|
|
|
3127
3259
|
}
|
|
3128
3260
|
}
|
|
3129
3261
|
|
|
3262
|
+
class Highlighter {
|
|
3263
|
+
constructor(editorElement) {
|
|
3264
|
+
this.editor = editorElement.editor;
|
|
3265
|
+
|
|
3266
|
+
this.#registerHighlightTransform();
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
toggle(styles) {
|
|
3270
|
+
this.editor.update(() => {
|
|
3271
|
+
this.#toggleSelectionStyles(styles);
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
remove() {
|
|
3276
|
+
this.toggle({ "color": null, "background-color": null });
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
#registerHighlightTransform() {
|
|
3280
|
+
return this.editor.registerNodeTransform(TextNode, (textNode) => {
|
|
3281
|
+
this.#syncHighlightWithStyle(textNode);
|
|
3282
|
+
})
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
#toggleSelectionStyles(styles) {
|
|
3286
|
+
const selection = $getSelection();
|
|
3287
|
+
if (!$isRangeSelection(selection)) return
|
|
3288
|
+
|
|
3289
|
+
const patch = {};
|
|
3290
|
+
for (const property in styles) {
|
|
3291
|
+
const oldValue = $getSelectionStyleValueForProperty(selection, property);
|
|
3292
|
+
patch[property] = this.#toggleOrReplace(oldValue, styles[property]);
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
$patchStyleText(selection, patch);
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
#toggleOrReplace(oldValue, newValue) {
|
|
3299
|
+
return oldValue === newValue ? null : newValue
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
#syncHighlightWithStyle(node) {
|
|
3303
|
+
if (hasHighlightStyles(node.getStyle()) !== node.hasFormat("highlight")) {
|
|
3304
|
+
node.toggleFormat("highlight");
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
class HighlightNode extends TextNode {
|
|
3310
|
+
$config() {
|
|
3311
|
+
return this.config("highlight", { extends: TextNode })
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
static importDOM() {
|
|
3315
|
+
return {
|
|
3316
|
+
mark: () => ({
|
|
3317
|
+
conversion: extendTextNodeConversion("mark", applyHighlightStyle),
|
|
3318
|
+
priority: 1
|
|
3319
|
+
})
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
function applyHighlightStyle(textNode, element) {
|
|
3325
|
+
const textColor = element.style?.color;
|
|
3326
|
+
const backgroundColor = element.style?.backgroundColor;
|
|
3327
|
+
|
|
3328
|
+
let highlightStyle = "";
|
|
3329
|
+
if (textColor && textColor !== "") highlightStyle += `color: ${textColor};`;
|
|
3330
|
+
if (backgroundColor && backgroundColor !== "") highlightStyle += `background-color: ${backgroundColor};`;
|
|
3331
|
+
|
|
3332
|
+
if (highlightStyle.length) {
|
|
3333
|
+
if (!textNode.hasFormat("highlight")) textNode.toggleFormat("highlight");
|
|
3334
|
+
return textNode.setStyle(textNode.getStyle() + highlightStyle)
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
const TRIX_LANGUAGE_ATTR = "language";
|
|
3339
|
+
|
|
3340
|
+
class TrixTextNode extends TextNode {
|
|
3341
|
+
$config() {
|
|
3342
|
+
return this.config("trix-text", { extends: TextNode })
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
static importDOM() {
|
|
3346
|
+
return {
|
|
3347
|
+
// em, span, and strong elements are directly styled in trix
|
|
3348
|
+
em: (element) => onlyStyledElements(element, {
|
|
3349
|
+
conversion: extendTextNodeConversion("i", applyHighlightStyle),
|
|
3350
|
+
priority: 1
|
|
3351
|
+
}),
|
|
3352
|
+
span: (element) => onlyStyledElements(element, {
|
|
3353
|
+
conversion: extendTextNodeConversion("mark", applyHighlightStyle),
|
|
3354
|
+
priority: 1
|
|
3355
|
+
}),
|
|
3356
|
+
strong: (element) => onlyStyledElements(element, {
|
|
3357
|
+
conversion: extendTextNodeConversion("b", applyHighlightStyle),
|
|
3358
|
+
priority: 1
|
|
3359
|
+
}),
|
|
3360
|
+
// del => s
|
|
3361
|
+
del: () => ({
|
|
3362
|
+
conversion: extendTextNodeConversion("s", applyStrikethrough),
|
|
3363
|
+
priority: 1
|
|
3364
|
+
}),
|
|
3365
|
+
// read "language" attribute and normalize
|
|
3366
|
+
pre: (element) => onlyPreLanguageElements(element, {
|
|
3367
|
+
conversion: extendConversion(CodeNode, "pre", applyLanguage),
|
|
3368
|
+
priority: 1
|
|
3369
|
+
})
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
function onlyStyledElements(element, conversion) {
|
|
3375
|
+
const elementHighlighted = element.style.color !== "" || element.style.backgroundColor !== "";
|
|
3376
|
+
return elementHighlighted ? conversion : null
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
function applyStrikethrough(textNode, element) {
|
|
3380
|
+
if (!textNode.hasFormat("strikethrough")) textNode.toggleFormat("strikethrough");
|
|
3381
|
+
return applyHighlightStyle(textNode, element)
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
function onlyPreLanguageElements(element, conversion) {
|
|
3385
|
+
return element.hasAttribute(TRIX_LANGUAGE_ATTR) ? conversion : null
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
function applyLanguage(conversionOutput, element) {
|
|
3389
|
+
const language = normalizeCodeLang(element.getAttribute(TRIX_LANGUAGE_ATTR));
|
|
3390
|
+
conversionOutput.node.setLanguage(language);
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3130
3393
|
class LexicalEditorElement extends HTMLElement {
|
|
3131
3394
|
static formAssociated = true
|
|
3132
3395
|
static debug = true
|
|
@@ -3149,6 +3412,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3149
3412
|
this.contents = new Contents(this);
|
|
3150
3413
|
this.selection = new Selection(this);
|
|
3151
3414
|
this.clipboard = new Clipboard(this);
|
|
3415
|
+
this.highlighter = new Highlighter(this);
|
|
3152
3416
|
|
|
3153
3417
|
CommandDispatcher.configureFor(this);
|
|
3154
3418
|
this.#initialize();
|
|
@@ -3184,6 +3448,10 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3184
3448
|
return this.internals.form
|
|
3185
3449
|
}
|
|
3186
3450
|
|
|
3451
|
+
get name() {
|
|
3452
|
+
return this.getAttribute("name")
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3187
3455
|
get toolbarElement() {
|
|
3188
3456
|
if (!this.#hasToolbar) return null
|
|
3189
3457
|
|
|
@@ -3259,6 +3527,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3259
3527
|
this.#registerComponents();
|
|
3260
3528
|
this.#listenForInvalidatedNodes();
|
|
3261
3529
|
this.#handleEnter();
|
|
3530
|
+
this.#handleFocus();
|
|
3262
3531
|
this.#attachDebugHooks();
|
|
3263
3532
|
this.#attachToolbar();
|
|
3264
3533
|
this.#loadInitialValue();
|
|
@@ -3284,6 +3553,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3284
3553
|
|
|
3285
3554
|
get #lexicalNodes() {
|
|
3286
3555
|
const nodes = [
|
|
3556
|
+
TrixTextNode,
|
|
3557
|
+
HighlightNode,
|
|
3287
3558
|
QuoteNode,
|
|
3288
3559
|
HeadingNode,
|
|
3289
3560
|
ListNode,
|
|
@@ -3437,6 +3708,14 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3437
3708
|
);
|
|
3438
3709
|
}
|
|
3439
3710
|
|
|
3711
|
+
#handleFocus() {
|
|
3712
|
+
// Lexxy handles focus and blur as commands
|
|
3713
|
+
// see https://github.com/facebook/lexical/blob/d1a8e84fe9063a4f817655b346b6ff373aa107f0/packages/lexical/src/LexicalEvents.ts#L35
|
|
3714
|
+
// and https://stackoverflow.com/a/72212077
|
|
3715
|
+
this.editor.registerCommand(BLUR_COMMAND, () => { dispatch(this, "lexxy:blur"); }, COMMAND_PRIORITY_NORMAL);
|
|
3716
|
+
this.editor.registerCommand(FOCUS_COMMAND, () => { dispatch(this, "lexxy:focus"); }, COMMAND_PRIORITY_NORMAL);
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3440
3719
|
#attachDebugHooks() {
|
|
3441
3720
|
if (!LexicalEditorElement.debug) return
|
|
3442
3721
|
|
|
@@ -3475,7 +3754,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3475
3754
|
}
|
|
3476
3755
|
|
|
3477
3756
|
get #isEmpty() {
|
|
3478
|
-
return
|
|
3757
|
+
return [ "<p><br></p>", "<p></p>", "" ].includes(this.value.trim())
|
|
3479
3758
|
}
|
|
3480
3759
|
|
|
3481
3760
|
#setValidity() {
|
|
@@ -3515,33 +3794,98 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3515
3794
|
|
|
3516
3795
|
customElements.define("lexxy-editor", LexicalEditorElement);
|
|
3517
3796
|
|
|
3518
|
-
class
|
|
3797
|
+
class ToolbarDialog extends HTMLElement {
|
|
3519
3798
|
connectedCallback() {
|
|
3520
3799
|
this.dialog = this.querySelector("dialog");
|
|
3521
|
-
|
|
3800
|
+
if ("closedBy" in this.dialog.constructor.prototype) {
|
|
3801
|
+
this.dialog.closedBy = "any";
|
|
3802
|
+
}
|
|
3803
|
+
this.#registerHandlers();
|
|
3804
|
+
}
|
|
3522
3805
|
|
|
3523
|
-
|
|
3524
|
-
this
|
|
3525
|
-
this.addEventListener("keydown", this.#handleKeyDown.bind(this));
|
|
3806
|
+
disconnectedCallback() {
|
|
3807
|
+
this.#removeClickOutsideHandler();
|
|
3526
3808
|
}
|
|
3527
3809
|
|
|
3528
|
-
|
|
3529
|
-
|
|
3810
|
+
updateStateCallback() { }
|
|
3811
|
+
|
|
3812
|
+
show(triggerButton) {
|
|
3813
|
+
if (this.preventImmediateReopen) { return }
|
|
3814
|
+
|
|
3815
|
+
this.triggerButton = triggerButton;
|
|
3816
|
+
this.#positionDialog();
|
|
3530
3817
|
this.dialog.show();
|
|
3818
|
+
|
|
3819
|
+
this.#setupClickOutsideHandler();
|
|
3531
3820
|
}
|
|
3532
3821
|
|
|
3533
3822
|
close() {
|
|
3534
3823
|
this.dialog.close();
|
|
3535
3824
|
}
|
|
3536
3825
|
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
this.#editor.dispatchCommand(command, this.input.value);
|
|
3826
|
+
get toolbar() {
|
|
3827
|
+
return this.closest("lexxy-toolbar")
|
|
3540
3828
|
}
|
|
3541
3829
|
|
|
3542
|
-
|
|
3543
|
-
this
|
|
3544
|
-
|
|
3830
|
+
get editor() {
|
|
3831
|
+
return this.toolbar.editor
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
get open() { return this.dialog.open }
|
|
3835
|
+
|
|
3836
|
+
#registerHandlers() {
|
|
3837
|
+
this.#setupKeydownHandler();
|
|
3838
|
+
this.dialog.addEventListener("cancel", this.#handleCancel.bind(this));
|
|
3839
|
+
this.dialog.addEventListener("close", this.#handleClose.bind(this));
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
#handleClose() {
|
|
3843
|
+
this.#removeClickOutsideHandler();
|
|
3844
|
+
this.triggerButton = null;
|
|
3845
|
+
this.editor.focus();
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
#handleCancel() {
|
|
3849
|
+
this.preventImmediateReopen = true;
|
|
3850
|
+
requestAnimationFrame(() => this.preventImmediateReopen = undefined);
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
#positionDialog() {
|
|
3854
|
+
const left = this.triggerButton.offsetLeft;
|
|
3855
|
+
this.dialog.style.insetInlineStart = `${left}px`;
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
#setupClickOutsideHandler() {
|
|
3859
|
+
if (this.#browserHandlesClose || this.clickOutsideHandler) return
|
|
3860
|
+
|
|
3861
|
+
this.clickOutsideHandler = this.#handleClickOutside.bind(this);
|
|
3862
|
+
document.addEventListener("click", this.clickOutsideHandler, true);
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
#removeClickOutsideHandler() {
|
|
3866
|
+
if (!this.clickOutsideHandler) return
|
|
3867
|
+
|
|
3868
|
+
document.removeEventListener("click", this.clickOutsideHandler, true);
|
|
3869
|
+
this.clickOutsideHandler = null;
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
#handleClickOutside({ target }) {
|
|
3873
|
+
if (!this.dialog.open) return
|
|
3874
|
+
|
|
3875
|
+
const isClickInsideDialog = this.dialog.contains(target);
|
|
3876
|
+
const isClickOnTrigger = this.triggerButton.contains(target);
|
|
3877
|
+
|
|
3878
|
+
if (!isClickInsideDialog && !isClickOnTrigger) {
|
|
3879
|
+
this.close();
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
#setupKeydownHandler() {
|
|
3884
|
+
if (!this.#browserHandlesClose) { this.addEventListener("keydown", this.#handleKeyDown.bind(this)); }
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
get #browserHandlesClose() {
|
|
3888
|
+
return this.dialog.closedBy === "any"
|
|
3545
3889
|
}
|
|
3546
3890
|
|
|
3547
3891
|
#handleKeyDown(event) {
|
|
@@ -3550,11 +3894,44 @@ class LinkDialog extends HTMLElement {
|
|
|
3550
3894
|
this.close();
|
|
3551
3895
|
}
|
|
3552
3896
|
}
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3899
|
+
class LinkDialog extends ToolbarDialog {
|
|
3900
|
+
connectedCallback() {
|
|
3901
|
+
super.connectedCallback();
|
|
3902
|
+
this.input = this.querySelector("input");
|
|
3903
|
+
|
|
3904
|
+
this.#registerHandlers();
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
updateStateCallback() {
|
|
3908
|
+
this.input.value = this.#selectedLinkUrl;
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
#registerHandlers() {
|
|
3912
|
+
this.dialog.addEventListener("beforetoggle", this.#handleBeforeToggle.bind(this));
|
|
3913
|
+
this.dialog.addEventListener("submit", this.#handleSubmit.bind(this));
|
|
3914
|
+
this.querySelector("[value='unlink']").addEventListener("click", this.#handleUnlink.bind(this));
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
#handleBeforeToggle({ newState }) {
|
|
3918
|
+
this.input.required = newState === "open";
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
#handleSubmit(event) {
|
|
3922
|
+
const command = event.submitter?.value;
|
|
3923
|
+
this.editor.dispatchCommand(command, this.input.value);
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
#handleUnlink() {
|
|
3927
|
+
this.editor.dispatchCommand("unlink");
|
|
3928
|
+
this.close();
|
|
3929
|
+
}
|
|
3553
3930
|
|
|
3554
3931
|
get #selectedLinkUrl() {
|
|
3555
3932
|
let url = "";
|
|
3556
3933
|
|
|
3557
|
-
this
|
|
3934
|
+
this.editor.getEditorState().read(() => {
|
|
3558
3935
|
const selection = $getSelection();
|
|
3559
3936
|
if (!$isRangeSelection(selection)) return
|
|
3560
3937
|
|
|
@@ -3570,16 +3947,110 @@ class LinkDialog extends HTMLElement {
|
|
|
3570
3947
|
|
|
3571
3948
|
return url
|
|
3572
3949
|
}
|
|
3573
|
-
|
|
3574
|
-
get #editor() {
|
|
3575
|
-
return this.closest("lexxy-toolbar").editor
|
|
3576
|
-
}
|
|
3577
3950
|
}
|
|
3578
3951
|
|
|
3579
3952
|
// We should extend the native dialog and avoid the intermediary <dialog> but not
|
|
3580
3953
|
// supported by Safari yet: customElements.define("lexxy-link-dialog", LinkDialog, { extends: "dialog" })
|
|
3581
3954
|
customElements.define("lexxy-link-dialog", LinkDialog);
|
|
3582
3955
|
|
|
3956
|
+
const APPLY_HIGHLIGHT_SELECTOR = "button.lexxy-highlight-button";
|
|
3957
|
+
const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
|
|
3958
|
+
|
|
3959
|
+
// Use Symbol instead of null since $getSelectionStyleValueForProperty
|
|
3960
|
+
// responds differently for backward selections if null is the default
|
|
3961
|
+
// see https://github.com/facebook/lexical/issues/8013
|
|
3962
|
+
const NO_STYLE = Symbol("no_style");
|
|
3963
|
+
|
|
3964
|
+
class HighlightDialog extends ToolbarDialog {
|
|
3965
|
+
connectedCallback() {
|
|
3966
|
+
super.connectedCallback();
|
|
3967
|
+
|
|
3968
|
+
this.#setUpButtons();
|
|
3969
|
+
this.#registerHandlers();
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
updateStateCallback() {
|
|
3973
|
+
this.#updateColorButtonStates($getSelection());
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
#registerHandlers() {
|
|
3977
|
+
this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).addEventListener("click", this.#handleRemoveHighlightClick.bind(this));
|
|
3978
|
+
this.#colorButtons.forEach(button => button.addEventListener("click", this.#handleColorButtonClick.bind(this)));
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
#setUpButtons() {
|
|
3982
|
+
this.#buttonGroups.forEach(buttonGroup => {
|
|
3983
|
+
this.#populateButtonGroup(buttonGroup);
|
|
3984
|
+
});
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
#populateButtonGroup(buttonGroup) {
|
|
3988
|
+
const values = buttonGroup.dataset.values?.split("; ") || [];
|
|
3989
|
+
const attribute = buttonGroup.dataset.buttonGroup;
|
|
3990
|
+
values.forEach((value, index) => {
|
|
3991
|
+
buttonGroup.appendChild(this.#createButton(attribute, value, index));
|
|
3992
|
+
});
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3995
|
+
#createButton(attribute, value, index) {
|
|
3996
|
+
const button = document.createElement("button");
|
|
3997
|
+
button.dataset.style = attribute;
|
|
3998
|
+
button.style.setProperty(attribute, value);
|
|
3999
|
+
button.dataset.value = value;
|
|
4000
|
+
button.classList.add("lexxy-highlight-button");
|
|
4001
|
+
button.name = attribute + "-" + index;
|
|
4002
|
+
return button
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
#handleColorButtonClick(event) {
|
|
4006
|
+
event.preventDefault();
|
|
4007
|
+
|
|
4008
|
+
const button = event.target.closest(APPLY_HIGHLIGHT_SELECTOR);
|
|
4009
|
+
if (!button) return
|
|
4010
|
+
|
|
4011
|
+
const attribute = button.dataset.style;
|
|
4012
|
+
const value = button.dataset.value;
|
|
4013
|
+
|
|
4014
|
+
this.editor.dispatchCommand("toggleHighlight", { [attribute]: value });
|
|
4015
|
+
this.close();
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
#handleRemoveHighlightClick(event) {
|
|
4019
|
+
event.preventDefault();
|
|
4020
|
+
|
|
4021
|
+
this.editor.dispatchCommand("removeHighlight");
|
|
4022
|
+
this.close();
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
#updateColorButtonStates(selection) {
|
|
4026
|
+
if (!$isRangeSelection(selection)) { return }
|
|
4027
|
+
|
|
4028
|
+
// Use non-"" default, so "" indicates mixed highlighting
|
|
4029
|
+
const textColor = $getSelectionStyleValueForProperty(selection, "color", NO_STYLE);
|
|
4030
|
+
const backgroundColor = $getSelectionStyleValueForProperty(selection, "background-color", NO_STYLE);
|
|
4031
|
+
|
|
4032
|
+
this.#colorButtons.forEach(button => {
|
|
4033
|
+
const matchesSelection = button.dataset.value === textColor || button.dataset.value === backgroundColor;
|
|
4034
|
+
button.setAttribute("aria-pressed", matchesSelection);
|
|
4035
|
+
});
|
|
4036
|
+
|
|
4037
|
+
const hasHighlight = textColor !== NO_STYLE || backgroundColor !== NO_STYLE;
|
|
4038
|
+
this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).disabled = !hasHighlight;
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
get #buttonGroups() {
|
|
4042
|
+
return this.querySelectorAll("[data-button-group]")
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
get #colorButtons() {
|
|
4046
|
+
return Array.from(this.querySelectorAll(APPLY_HIGHLIGHT_SELECTOR))
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
|
|
4050
|
+
// We should extend the native dialog and avoid the intermediary <dialog> but not
|
|
4051
|
+
// supported by Safari yet: customElements.define("lexxy-hightlight-dialog", HighlightDialog, { extends: "dialog" })
|
|
4052
|
+
customElements.define("lexxy-highlight-dialog", HighlightDialog);
|
|
4053
|
+
|
|
3583
4054
|
class BaseSource {
|
|
3584
4055
|
// Template method to override
|
|
3585
4056
|
async buildListItems(filter = "") {
|
|
@@ -3781,9 +4252,17 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
3781
4252
|
const fullText = node.getTextContent();
|
|
3782
4253
|
const charBeforeCursor = fullText[offset - 1];
|
|
3783
4254
|
|
|
4255
|
+
// Check if trigger is at the start of the text node (new line case) or preceded by space or newline
|
|
3784
4256
|
if (charBeforeCursor === this.trigger) {
|
|
3785
|
-
|
|
3786
|
-
|
|
4257
|
+
const isAtStart = offset === 1;
|
|
4258
|
+
|
|
4259
|
+
const charBeforeTrigger = offset > 1 ? fullText[offset - 2] : null;
|
|
4260
|
+
const isPrecededBySpaceOrNewline = charBeforeTrigger === " " || charBeforeTrigger === "\n";
|
|
4261
|
+
|
|
4262
|
+
if (isAtStart || isPrecededBySpaceOrNewline) {
|
|
4263
|
+
unregister();
|
|
4264
|
+
this.#showPopover();
|
|
4265
|
+
}
|
|
3787
4266
|
}
|
|
3788
4267
|
}
|
|
3789
4268
|
});
|
|
@@ -4028,7 +4507,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
4028
4507
|
}
|
|
4029
4508
|
|
|
4030
4509
|
#handleSelectedOption(event) {
|
|
4031
|
-
|
|
4510
|
+
event.preventDefault();
|
|
4032
4511
|
event.stopPropagation();
|
|
4033
4512
|
this.#optionWasSelected();
|
|
4034
4513
|
return true
|
|
@@ -4137,6 +4616,11 @@ class CodeLanguagePicker extends HTMLElement {
|
|
|
4137
4616
|
const languages = { ...CODE_LANGUAGE_FRIENDLY_NAME_MAP };
|
|
4138
4617
|
|
|
4139
4618
|
if (!languages.ruby) languages.ruby = "Ruby";
|
|
4619
|
+
if (!languages.php) languages.php = "PHP";
|
|
4620
|
+
if (!languages.go) languages.go = "Go";
|
|
4621
|
+
if (!languages.bash) languages.bash = "Bash";
|
|
4622
|
+
if (!languages.json) languages.json = "JSON";
|
|
4623
|
+
if (!languages.diff) languages.diff = "Diff";
|
|
4140
4624
|
|
|
4141
4625
|
const sortedEntries = Object.entries(languages)
|
|
4142
4626
|
.sort(([ , a ], [ , b ]) => a.localeCompare(b));
|