@37signals/lexxy 0.1.12-beta → 0.1.14-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 +7 -1
- package/dist/lexxy.esm.js +344 -177
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -87,7 +87,11 @@ For the CSS, you can include it with the standard Rails helper:
|
|
|
87
87
|
<%= stylesheet_link_tag "lexxy" %>
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
You can copy the CSS to your project and adapt it to your needs.
|
|
91
|
+
|
|
92
|
+
#### Custom styles and dark mode
|
|
93
|
+
|
|
94
|
+
All of Lexxy's color styles are defiend as CSS variables in `app/stylesheets/lexxy-variables.css`. This enables a straightforward way to customize Lexxy to match your application's theme. You can see an example implementation of a custom dark mode style in the Sandbox's stylesheet at `test/dummy/app/assets/stylesheets/sandbox.css`.
|
|
91
95
|
|
|
92
96
|
#### Rendered Action Text content
|
|
93
97
|
|
|
@@ -104,6 +108,8 @@ To apply syntax highlighting to rendered Action Text content, you need to call t
|
|
|
104
108
|
```javascript
|
|
105
109
|
import { Controller } from "@hotwired/stimulus"
|
|
106
110
|
import { highlightAll } from "lexxy"
|
|
111
|
+
// Or if you installed via a javascript bundler:
|
|
112
|
+
// import { highlightAll } from "@37signals/lexxy"
|
|
107
113
|
|
|
108
114
|
export default class extends Controller {
|
|
109
115
|
connect() {
|
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
import DOMPurify from 'dompurify';
|
|
2
|
-
import { $getSelection, $isRangeSelection,
|
|
2
|
+
import { $getSelection, $isRangeSelection, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, $isNodeSelection, $getRoot, $isLineBreakNode, $isTextNode, $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, $createTextNode, $insertNodes, $isParagraphNode, $createLineBreakNode, CLEAR_HISTORY_COMMAND, $addUpdateTag, SKIP_DOM_SELECTION_TAG, createEditor, KEY_ENTER_COMMAND, COMMAND_PRIORITY_NORMAL, COMMAND_PRIORITY_HIGH, KEY_TAB_COMMAND, KEY_SPACE_COMMAND } from 'lexical';
|
|
3
3
|
import { $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, ListNode, ListItemNode, registerList } from '@lexical/list';
|
|
4
4
|
import { $isQuoteNode, $isHeadingNode, $createQuoteNode, $createHeadingNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
5
|
-
import { $isCodeNode,
|
|
5
|
+
import { $isCodeNode, CodeNode, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP, normalizeCodeLang } from '@lexical/code';
|
|
6
6
|
import { $isLinkNode, $toggleLink, $createLinkNode, LinkNode, AutoLinkNode } from '@lexical/link';
|
|
7
7
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
8
8
|
import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown';
|
|
9
|
-
import {
|
|
9
|
+
import { createEmptyHistoryState, registerHistory } from '@lexical/history';
|
|
10
10
|
import { DirectUpload } from '@rails/activestorage';
|
|
11
11
|
import { marked } from 'marked';
|
|
12
12
|
import 'prismjs/components/prism-ruby';
|
|
13
13
|
|
|
14
14
|
DOMPurify.addHook("uponSanitizeElement", (node, data) => {
|
|
15
15
|
if (data.tagName === "strong" || data.tagName === "em") {
|
|
16
|
-
node.removeAttribute(
|
|
16
|
+
node.removeAttribute("class");
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
function getNonce() {
|
|
21
21
|
const element = document.head.querySelector("meta[name=csp-nonce]");
|
|
22
22
|
return element?.content
|
|
23
|
-
}
|
|
23
|
+
}
|
|
24
24
|
|
|
25
25
|
function getNearestListItemNode(node) {
|
|
26
26
|
let current = node;
|
|
@@ -47,7 +47,7 @@ function isPrintableCharacter(event) {
|
|
|
47
47
|
if (event.ctrlKey || event.metaKey || event.altKey) return false
|
|
48
48
|
|
|
49
49
|
// Ignore special keys
|
|
50
|
-
if (event.key.length > 1 && event.key !==
|
|
50
|
+
if (event.key.length > 1 && event.key !== "Enter" && event.key !== "Space") return false
|
|
51
51
|
|
|
52
52
|
// Accept single character keys (letters, numbers, punctuation)
|
|
53
53
|
return event.key.length === 1
|
|
@@ -61,12 +61,17 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
connectedCallback() {
|
|
64
|
-
this.#refreshToolbarOverflow();
|
|
65
|
-
|
|
64
|
+
requestAnimationFrame(() => this.#refreshToolbarOverflow());
|
|
65
|
+
|
|
66
|
+
this._resizeObserver = new ResizeObserver(() => this.#refreshToolbarOverflow());
|
|
67
|
+
this._resizeObserver.observe(this);
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
disconnectedCallback() {
|
|
69
|
-
|
|
71
|
+
if (this._resizeObserver) {
|
|
72
|
+
this._resizeObserver.disconnect();
|
|
73
|
+
this._resizeObserver = null;
|
|
74
|
+
}
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
setEditor(editorElement) {
|
|
@@ -76,6 +81,8 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
76
81
|
this.#bindHotkeys();
|
|
77
82
|
this.#assignButtonTabindex();
|
|
78
83
|
this.#monitorSelectionChanges();
|
|
84
|
+
this.#monitorHistoryChanges();
|
|
85
|
+
this.#refreshToolbarOverflow();
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
#bindButtons() {
|
|
@@ -101,7 +108,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
101
108
|
|
|
102
109
|
// Not using popover because of CSS anchoring still not widely available.
|
|
103
110
|
#toggleDialog(button) {
|
|
104
|
-
const dialog =
|
|
111
|
+
const dialog = this.querySelector("lexxy-link-dialog .link-dialog").parentNode;
|
|
105
112
|
|
|
106
113
|
if (dialog.open) {
|
|
107
114
|
dialog.close();
|
|
@@ -111,7 +118,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
#bindHotkeys() {
|
|
114
|
-
this.editorElement.addEventListener(
|
|
121
|
+
this.editorElement.addEventListener("keydown", (event) => {
|
|
115
122
|
const buttons = this.querySelectorAll("[data-hotkey]");
|
|
116
123
|
buttons.forEach((button) => {
|
|
117
124
|
const hotkeys = button.dataset.hotkey.toLowerCase().split(/\s+/);
|
|
@@ -127,13 +134,13 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
127
134
|
#keyCombinationFor(event) {
|
|
128
135
|
const pressedKey = event.key.toLowerCase();
|
|
129
136
|
const modifiers = [
|
|
130
|
-
event.ctrlKey ?
|
|
131
|
-
event.metaKey ?
|
|
132
|
-
event.altKey ?
|
|
133
|
-
event.shiftKey ?
|
|
137
|
+
event.ctrlKey ? "ctrl" : null,
|
|
138
|
+
event.metaKey ? "cmd" : null,
|
|
139
|
+
event.altKey ? "alt" : null,
|
|
140
|
+
event.shiftKey ? "shift" : null,
|
|
134
141
|
].filter(Boolean);
|
|
135
142
|
|
|
136
|
-
return [ ...modifiers, pressedKey ].join(
|
|
143
|
+
return [ ...modifiers, pressedKey ].join("+")
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
#assignButtonTabindex() {
|
|
@@ -151,6 +158,30 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
151
158
|
});
|
|
152
159
|
}
|
|
153
160
|
|
|
161
|
+
#monitorHistoryChanges() {
|
|
162
|
+
this.editor.registerUpdateListener(() => {
|
|
163
|
+
this.#updateUndoRedoButtonStates();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#updateUndoRedoButtonStates() {
|
|
168
|
+
this.editor.getEditorState().read(() => {
|
|
169
|
+
const historyState = this.editorElement.historyState;
|
|
170
|
+
if (historyState) {
|
|
171
|
+
this.#setButtonDisabled("undo", historyState.undoStack.length === 0);
|
|
172
|
+
this.#setButtonDisabled("redo", historyState.redoStack.length === 0);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
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
|
+
|
|
154
185
|
#updateButtonStates() {
|
|
155
186
|
const selection = $getSelection();
|
|
156
187
|
if (!$isRangeSelection(selection)) return
|
|
@@ -162,6 +193,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
162
193
|
|
|
163
194
|
const isBold = selection.hasFormat("bold");
|
|
164
195
|
const isItalic = selection.hasFormat("italic");
|
|
196
|
+
const isStrikethrough = selection.hasFormat("strikethrough");
|
|
165
197
|
const isInCode = $isCodeNode(topLevelElement) || selection.hasFormat("code");
|
|
166
198
|
const isInList = this.#isInList(anchorNode);
|
|
167
199
|
const listType = getListType(anchorNode);
|
|
@@ -171,25 +203,15 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
171
203
|
|
|
172
204
|
this.#setButtonPressed("bold", isBold);
|
|
173
205
|
this.#setButtonPressed("italic", isItalic);
|
|
206
|
+
this.#setButtonPressed("strikethrough", isStrikethrough);
|
|
174
207
|
this.#setButtonPressed("code", isInCode);
|
|
175
208
|
this.#setButtonPressed("unordered-list", isInList && listType === "bullet");
|
|
176
209
|
this.#setButtonPressed("ordered-list", isInList && listType === "number");
|
|
177
210
|
this.#setButtonPressed("quote", isInQuote);
|
|
178
211
|
this.#setButtonPressed("heading", isInHeading);
|
|
179
212
|
this.#setButtonPressed("link", isInLink);
|
|
180
|
-
}
|
|
181
213
|
|
|
182
|
-
|
|
183
|
-
const nodes = selection.getNodes();
|
|
184
|
-
return nodes.some(node => {
|
|
185
|
-
if ($isCodeHighlightNode(node)) return true
|
|
186
|
-
// Check parent for text nodes inside code highlight
|
|
187
|
-
if ($isTextNode(node)) {
|
|
188
|
-
const parent = node.getParent();
|
|
189
|
-
if (parent && $isCodeHighlightNode(parent)) return true
|
|
190
|
-
}
|
|
191
|
-
return false
|
|
192
|
-
})
|
|
214
|
+
this.#updateUndoRedoButtonStates();
|
|
193
215
|
}
|
|
194
216
|
|
|
195
217
|
#isInList(node) {
|
|
@@ -249,17 +271,17 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
249
271
|
|
|
250
272
|
for (const button of buttons) {
|
|
251
273
|
if (this.#toolbarIsOverflowing()) {
|
|
252
|
-
this.#overflowMenu.
|
|
274
|
+
this.#overflowMenu.prepend(button);
|
|
253
275
|
movedToOverflow = true;
|
|
254
276
|
} else {
|
|
255
|
-
if (movedToOverflow) this.#overflowMenu.
|
|
277
|
+
if (movedToOverflow) this.#overflowMenu.prepend(button);
|
|
256
278
|
break
|
|
257
279
|
}
|
|
258
280
|
}
|
|
259
281
|
}
|
|
260
282
|
|
|
261
283
|
get #buttons() {
|
|
262
|
-
return Array.from(this.querySelectorAll(":scope > button"))
|
|
284
|
+
return Array.from(this.querySelectorAll(":scope > button, :scope > [role=separator]"))
|
|
263
285
|
}
|
|
264
286
|
|
|
265
287
|
static get defaultTemplate() {
|
|
@@ -272,12 +294,18 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
272
294
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17.1 4h-1.5l-3.2 16h1.5l-.4 2h-7l.4-2h1.5l3.2-16h-1.5l.4-2h7l-.4 2z"/></svg>
|
|
273
295
|
</button>
|
|
274
296
|
|
|
297
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="strikethrough" data-command="strikethrough" title="Strikethrough">
|
|
298
|
+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
299
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.70588 16.1591C4.81459 19.7901 7.48035 22 11.6668 22C15.9854 22 18.724 19.6296 18.724 15.8779C18.724 15.5007 18.6993 15.1427 18.6474 14.8066H14.3721C14.8637 15.2085 15.0799 15.7037 15.0799 16.3471C15.0799 17.7668 13.7532 18.7984 11.8113 18.7984C9.88053 18.7984 8.38582 17.7531 8.21659 16.1591H4.70588ZM5.23953 9.31962H9.88794C9.10723 8.88889 8.75888 8.33882 8.75888 7.57339C8.75888 6.13992 9.96576 5.18793 11.7631 5.18793C13.5852 5.18793 14.8761 6.1797 14.9959 7.81344H18.4102C18.3485 4.31824 15.8038 2 11.752 2C7.867 2 5.09129 4.35802 5.09129 7.92044C5.09129 8.41838 5.14071 8.88477 5.23953 9.31962ZM2.23529 10.6914C1.90767 10.6914 1.59347 10.8359 1.36181 11.0931C1.13015 11.3504 1 11.6993 1 12.0631C1 12.4269 1.13015 12.7758 1.36181 13.0331C1.59347 13.2903 1.90767 13.4348 2.23529 13.4348H20.7647C21.0923 13.4348 21.4065 13.2903 21.6382 13.0331C21.8699 12.7758 22 12.4269 22 12.0631C22 11.6993 21.8699 11.3504 21.6382 11.0931C21.4065 10.8359 21.0923 10.6914 20.7647 10.6914H2.23529Z"/>
|
|
300
|
+
</svg>
|
|
301
|
+
</button>
|
|
302
|
+
|
|
275
303
|
<button class="lexxy-editor__toolbar-button" type="button" name="link" title="Link" data-dialog-target="link-dialog" data-hotkey="cmd+k ctrl+k">
|
|
276
304
|
<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>
|
|
277
305
|
</button>
|
|
278
306
|
|
|
279
307
|
<lexxy-link-dialog class="lexxy-link-dialog">
|
|
280
|
-
<dialog
|
|
308
|
+
<dialog class="link-dialog" closedby="any">
|
|
281
309
|
<form method="dialog">
|
|
282
310
|
<input type="url" placeholder="Enter a URL…" class="input" required>
|
|
283
311
|
<div class="lexxy-dialog-actions">
|
|
@@ -312,6 +340,20 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
312
340
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M16 8a2 2 0 110 4 2 2 0 010-4z""/><path d="M22 2a1 1 0 011 1v18a1 1 0 01-1 1H2a1 1 0 01-1-1V3a1 1 0 011-1h20zM3 18.714L9 11l5.25 6.75L17 15l4 4V4H3v14.714z"/></svg>
|
|
313
341
|
</button>
|
|
314
342
|
|
|
343
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="divider" data-command="insertHorizontalDivider" title="Insert a divider">
|
|
344
|
+
<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
|
+
</button>
|
|
346
|
+
|
|
347
|
+
<div class="lexxy-editor__toolbar-spacer" role="separator"></div>
|
|
348
|
+
|
|
349
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="undo" data-command="undo" title="Undo" data-hotkey="cmd+z ctrl+z">
|
|
350
|
+
<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
|
+
</button>
|
|
352
|
+
|
|
353
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="redo" data-command="redo" title="Redo" data-hotkey="cmd+shift+z ctrl+shift+z ctrl+y">
|
|
354
|
+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M18.2599 8.26531C15.9672 6.56386 13.1237 5.77629 10.2823 6.05535C7.4408 6.33452 4.80455 7.66079 2.88681 9.77605C1.32245 11.5016 0.326407 13.6516 0.0127834 15.9352C-0.105117 16.7939 0.608975 17.4997 1.47567 17.4997C2.34228 17.4997 3.02969 16.7915 3.19149 15.9401C3.47682 14.4379 4.17156 13.0321 5.212 11.8844C6.60637 10.3464 8.52287 9.38139 10.589 9.17839C12.655 8.97546 14.7227 9.54856 16.3897 10.7858C17.5237 11.6275 18.4165 12.7361 18.9991 13.9997H15.4063C14.578 13.9997 13.9066 14.6714 13.9063 15.4997C13.9063 16.3281 14.5779 16.9997 15.4063 16.9997H22.4063C23.2348 16.9997 23.9063 16.3281 23.9063 15.4997V8.49968C23.9061 7.67144 23.2346 6.99968 22.4063 6.99968C21.578 6.99968 20.9066 7.67144 20.9063 8.49968V11.0212C20.1897 9.9704 19.2984 9.03613 18.2599 8.26531Z"/></svg>
|
|
355
|
+
</button>
|
|
356
|
+
|
|
315
357
|
<details class="lexxy-editor__toolbar-overflow">
|
|
316
358
|
<summary class="lexxy-editor__toolbar-button" aria-label="Show more toolbar buttons">•••</summary>
|
|
317
359
|
<div class="lexxy-editor__toolbar-overflow-menu" aria-label="More toolbar buttons"></div>
|
|
@@ -326,69 +368,62 @@ var theme = {
|
|
|
326
368
|
text: {
|
|
327
369
|
bold: "lexxy-content__bold",
|
|
328
370
|
italic: "lexxy-content__italic",
|
|
371
|
+
strikethrough: "lexxy-content__strikethrough",
|
|
329
372
|
underline: "lexxy-content__underline",
|
|
330
373
|
},
|
|
331
374
|
codeHighlight: {
|
|
332
|
-
atrule:
|
|
333
|
-
attr:
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
boolean:
|
|
337
|
-
bold:
|
|
338
|
-
builtin:
|
|
339
|
-
cdata:
|
|
340
|
-
char:
|
|
341
|
-
class:
|
|
342
|
-
|
|
343
|
-
color:
|
|
344
|
-
comment:
|
|
345
|
-
constant:
|
|
346
|
-
coord:
|
|
347
|
-
decorator:
|
|
348
|
-
deleted:
|
|
349
|
-
doctype:
|
|
350
|
-
entity:
|
|
351
|
-
function:
|
|
352
|
-
hexcode:
|
|
353
|
-
important:
|
|
354
|
-
inserted:
|
|
355
|
-
italic:
|
|
356
|
-
keyword:
|
|
357
|
-
namespace:
|
|
358
|
-
number:
|
|
359
|
-
operator:
|
|
360
|
-
parameter:
|
|
361
|
-
prolog:
|
|
362
|
-
property:
|
|
363
|
-
punctuation:
|
|
364
|
-
regex:
|
|
365
|
-
script:
|
|
366
|
-
selector:
|
|
367
|
-
string:
|
|
368
|
-
style:
|
|
369
|
-
symbol:
|
|
370
|
-
tag:
|
|
371
|
-
title:
|
|
372
|
-
url:
|
|
373
|
-
variable:
|
|
375
|
+
atrule: "code-token__attr",
|
|
376
|
+
attr: "code-token__attr",
|
|
377
|
+
"attr-name": "code-token__attr",
|
|
378
|
+
"attr-value": "code-token__selector",
|
|
379
|
+
boolean: "code-token__property",
|
|
380
|
+
bold: "code-token__variable",
|
|
381
|
+
builtin: "code-token__selector",
|
|
382
|
+
cdata: "code-token__comment",
|
|
383
|
+
char: "code-token__selector",
|
|
384
|
+
class: "code-token__function",
|
|
385
|
+
"class-name": "code-token__function",
|
|
386
|
+
color: "code-token__property",
|
|
387
|
+
comment: "code-token__comment",
|
|
388
|
+
constant: "code-token__property",
|
|
389
|
+
coord: "code-token__property",
|
|
390
|
+
decorator: "code-token__function",
|
|
391
|
+
deleted: "code-token__property",
|
|
392
|
+
doctype: "code-token__comment",
|
|
393
|
+
entity: "code-token__operator",
|
|
394
|
+
function: "code-token__function",
|
|
395
|
+
hexcode: "code-token__property",
|
|
396
|
+
important: "code-token__variable",
|
|
397
|
+
inserted: "code-token__selector",
|
|
398
|
+
italic: "code-token__comment",
|
|
399
|
+
keyword: "code-token__attr",
|
|
400
|
+
namespace: "code-token__variable",
|
|
401
|
+
number: "code-token__property",
|
|
402
|
+
operator: "code-token__operator",
|
|
403
|
+
parameter: "code-token__variable",
|
|
404
|
+
prolog: "code-token__comment",
|
|
405
|
+
property: "code-token__property",
|
|
406
|
+
punctuation: "code-token__punctuation",
|
|
407
|
+
regex: "code-token__variable",
|
|
408
|
+
script: "code-token__function",
|
|
409
|
+
selector: "code-token__selector",
|
|
410
|
+
string: "code-token__selector",
|
|
411
|
+
style: "code-token__function",
|
|
412
|
+
symbol: "code-token__property",
|
|
413
|
+
tag: "code-token__property",
|
|
414
|
+
title: "code-token__function",
|
|
415
|
+
url: "code-token__operator",
|
|
416
|
+
variable: "code-token__variable",
|
|
374
417
|
}
|
|
375
418
|
};
|
|
376
419
|
|
|
377
|
-
function bytesToHumanSize(bytes) {
|
|
378
|
-
if (bytes === 0) return "0 B"
|
|
379
|
-
const sizes = [ "B", "KB", "MB", "GB", "TB", "PB" ];
|
|
380
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
381
|
-
const value = bytes / Math.pow(1024, i);
|
|
382
|
-
return `${ value.toFixed(2) } ${ sizes[i] }`
|
|
383
|
-
}
|
|
384
|
-
|
|
385
420
|
const VISUALLY_RELEVANT_ELEMENTS_SELECTOR = [
|
|
386
421
|
"img", "video", "audio", "iframe", "embed", "object", "picture", "source", "canvas", "svg", "math",
|
|
387
|
-
"form", "input", "textarea", "select", "button", "code", "blockquote"
|
|
422
|
+
"form", "input", "textarea", "select", "button", "code", "blockquote", "hr"
|
|
388
423
|
].join(",");
|
|
389
424
|
|
|
390
425
|
const ALLOWED_HTML_TAGS = [ "a", "action-text-attachment", "b", "blockquote", "br", "code", "em",
|
|
391
|
-
"figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "i", "img", "li", "ol", "p", "pre", "q", "strong", "ul" ];
|
|
426
|
+
"figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "li", "ol", "p", "pre", "q", "s", "strong", "ul" ];
|
|
392
427
|
|
|
393
428
|
const ALLOWED_HTML_ATTRIBUTES = [ "alt", "caption", "class", "content", "content-type", "contenteditable",
|
|
394
429
|
"data-direct-upload-id", "data-sgid", "filename", "filesize", "height", "href", "presentation",
|
|
@@ -412,9 +447,9 @@ function parseHtml(html) {
|
|
|
412
447
|
}
|
|
413
448
|
|
|
414
449
|
function createAttachmentFigure(contentType, isPreviewable, fileName) {
|
|
415
|
-
const extension = fileName ? fileName.split(
|
|
450
|
+
const extension = fileName ? fileName.split(".").pop().toLowerCase() : "unknown";
|
|
416
451
|
return createElement("figure", {
|
|
417
|
-
className: `attachment attachment--${isPreviewable ?
|
|
452
|
+
className: `attachment attachment--${isPreviewable ? "preview" : "file"} attachment--${extension}`,
|
|
418
453
|
"data-content-type": contentType
|
|
419
454
|
})
|
|
420
455
|
}
|
|
@@ -454,13 +489,21 @@ function generateDomId(prefix) {
|
|
|
454
489
|
return `${prefix}-${randomPart}`
|
|
455
490
|
}
|
|
456
491
|
|
|
492
|
+
function bytesToHumanSize(bytes) {
|
|
493
|
+
if (bytes === 0) return "0 B"
|
|
494
|
+
const sizes = [ "B", "KB", "MB", "GB", "TB", "PB" ];
|
|
495
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
496
|
+
const value = bytes / Math.pow(1024, i);
|
|
497
|
+
return `${ value.toFixed(2) } ${ sizes[i] }`
|
|
498
|
+
}
|
|
499
|
+
|
|
457
500
|
class ActionTextAttachmentNode extends DecoratorNode {
|
|
458
501
|
static getType() {
|
|
459
502
|
return "action_text_attachment"
|
|
460
503
|
}
|
|
461
504
|
|
|
462
505
|
static clone(node) {
|
|
463
|
-
return new ActionTextAttachmentNode({ ...node }, node.__key)
|
|
506
|
+
return new ActionTextAttachmentNode({ ...node }, node.__key)
|
|
464
507
|
}
|
|
465
508
|
|
|
466
509
|
static importJSON(serializedNode) {
|
|
@@ -501,6 +544,22 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
501
544
|
}),
|
|
502
545
|
priority: 1
|
|
503
546
|
}
|
|
547
|
+
},
|
|
548
|
+
"video": (video) => {
|
|
549
|
+
const videoSource = video.getAttribute("src") || video.querySelector("source")?.src;
|
|
550
|
+
const fileName = videoSource?.split("/")?.pop();
|
|
551
|
+
const contentType = video.querySelector("source")?.getAttribute("content-type") || "video/*";
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
conversion: () => ({
|
|
555
|
+
node: new ActionTextAttachmentNode({
|
|
556
|
+
src: videoSource,
|
|
557
|
+
fileName: fileName,
|
|
558
|
+
contentType: contentType
|
|
559
|
+
})
|
|
560
|
+
}),
|
|
561
|
+
priority: 1
|
|
562
|
+
}
|
|
504
563
|
}
|
|
505
564
|
}
|
|
506
565
|
}
|
|
@@ -514,7 +573,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
514
573
|
this.altText = altText || "";
|
|
515
574
|
this.caption = caption || "";
|
|
516
575
|
this.contentType = contentType || "";
|
|
517
|
-
this.fileName = fileName;
|
|
576
|
+
this.fileName = fileName || "";
|
|
518
577
|
this.fileSize = fileSize;
|
|
519
578
|
this.width = width;
|
|
520
579
|
this.height = height;
|
|
@@ -598,7 +657,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
598
657
|
}
|
|
599
658
|
|
|
600
659
|
#createDOMForImage() {
|
|
601
|
-
return createElement("img", { src: this.src, alt: this.altText, ...this.#imageDimensions})
|
|
660
|
+
return createElement("img", { src: this.src, alt: this.altText, ...this.#imageDimensions })
|
|
602
661
|
}
|
|
603
662
|
|
|
604
663
|
get #imageDimensions() {
|
|
@@ -610,18 +669,21 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
610
669
|
}
|
|
611
670
|
|
|
612
671
|
#createDOMForFile() {
|
|
613
|
-
const extension = this.fileName ? this.fileName.split(
|
|
614
|
-
return createElement("span", { className: "attachment__icon", textContent: `${extension}`})
|
|
672
|
+
const extension = this.fileName ? this.fileName.split(".").pop().toLowerCase() : "unknown";
|
|
673
|
+
return createElement("span", { className: "attachment__icon", textContent: `${extension}` })
|
|
615
674
|
}
|
|
616
675
|
|
|
617
676
|
#createDOMForNotImage() {
|
|
618
677
|
const figcaption = createElement("figcaption", { className: "attachment__caption" });
|
|
619
678
|
|
|
620
679
|
const nameTag = createElement("strong", { className: "attachment__name", textContent: this.caption || this.fileName });
|
|
621
|
-
const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.fileSize) });
|
|
622
680
|
|
|
623
681
|
figcaption.appendChild(nameTag);
|
|
624
|
-
|
|
682
|
+
|
|
683
|
+
if (this.fileSize) {
|
|
684
|
+
const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.fileSize) });
|
|
685
|
+
figcaption.appendChild(sizeSpan);
|
|
686
|
+
}
|
|
625
687
|
|
|
626
688
|
return figcaption
|
|
627
689
|
}
|
|
@@ -648,9 +710,6 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
648
710
|
return caption
|
|
649
711
|
}
|
|
650
712
|
|
|
651
|
-
#updateCaption(input) {
|
|
652
|
-
}
|
|
653
|
-
|
|
654
713
|
#handleCaptionInputBlurred(event) {
|
|
655
714
|
const input = event.target;
|
|
656
715
|
|
|
@@ -694,7 +753,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
694
753
|
}
|
|
695
754
|
|
|
696
755
|
static clone(node) {
|
|
697
|
-
return new ActionTextAttachmentUploadNode({ ...node }, node.__key)
|
|
756
|
+
return new ActionTextAttachmentUploadNode({ ...node }, node.__key)
|
|
698
757
|
}
|
|
699
758
|
|
|
700
759
|
constructor({ file, uploadUrl, blobUrlTemplate, editor, progress }, key) {
|
|
@@ -747,7 +806,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
747
806
|
}
|
|
748
807
|
|
|
749
808
|
#getFileExtension() {
|
|
750
|
-
return this.file.name.split(
|
|
809
|
+
return this.file.name.split(".").pop().toLowerCase()
|
|
751
810
|
}
|
|
752
811
|
|
|
753
812
|
#createCaption() {
|
|
@@ -777,7 +836,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
777
836
|
directUploadWillStoreFileWithXHR: (request) => {
|
|
778
837
|
request.upload.addEventListener("progress", (event) => {
|
|
779
838
|
this.editor.update(() => {
|
|
780
|
-
progressBar.value = Math.round(
|
|
839
|
+
progressBar.value = Math.round(event.loaded / event.total * 100);
|
|
781
840
|
});
|
|
782
841
|
});
|
|
783
842
|
}
|
|
@@ -842,17 +901,89 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
842
901
|
}
|
|
843
902
|
}
|
|
844
903
|
|
|
904
|
+
class HorizontalDividerNode extends DecoratorNode {
|
|
905
|
+
static getType() {
|
|
906
|
+
return "horizontal_divider"
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
static clone(node) {
|
|
910
|
+
return new HorizontalDividerNode(node.__key)
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
static importJSON(serializedNode) {
|
|
914
|
+
return new HorizontalDividerNode()
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
static importDOM() {
|
|
918
|
+
return {
|
|
919
|
+
"hr": (hr) => {
|
|
920
|
+
return {
|
|
921
|
+
conversion: () => ({
|
|
922
|
+
node: new HorizontalDividerNode()
|
|
923
|
+
}),
|
|
924
|
+
priority: 1
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
constructor(key) {
|
|
931
|
+
super(key);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
createDOM() {
|
|
935
|
+
const figure = createElement("figure", { className: "horizontal-divider" });
|
|
936
|
+
const hr = createElement("hr");
|
|
937
|
+
|
|
938
|
+
figure.addEventListener("click", (event) => {
|
|
939
|
+
dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() });
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
figure.appendChild(hr);
|
|
943
|
+
|
|
944
|
+
return figure
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
updateDOM() {
|
|
948
|
+
return true
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
isInline() {
|
|
952
|
+
return false
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
exportDOM() {
|
|
956
|
+
const hr = createElement("hr");
|
|
957
|
+
return { element: hr }
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
exportJSON() {
|
|
961
|
+
return {
|
|
962
|
+
type: "horizontal_divider",
|
|
963
|
+
version: 1
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
decorate() {
|
|
968
|
+
return null
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
845
972
|
const COMMANDS = [
|
|
846
973
|
"bold",
|
|
847
|
-
"rotateHeadingFormat",
|
|
848
974
|
"italic",
|
|
975
|
+
"strikethrough",
|
|
849
976
|
"link",
|
|
850
977
|
"unlink",
|
|
978
|
+
"rotateHeadingFormat",
|
|
851
979
|
"insertUnorderedList",
|
|
852
980
|
"insertOrderedList",
|
|
853
981
|
"insertQuoteBlock",
|
|
854
982
|
"insertCodeBlock",
|
|
855
|
-
"
|
|
983
|
+
"insertHorizontalDivider",
|
|
984
|
+
"uploadAttachments",
|
|
985
|
+
"undo",
|
|
986
|
+
"redo"
|
|
856
987
|
];
|
|
857
988
|
|
|
858
989
|
class CommandDispatcher {
|
|
@@ -883,6 +1014,10 @@ class CommandDispatcher {
|
|
|
883
1014
|
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic");
|
|
884
1015
|
}
|
|
885
1016
|
|
|
1017
|
+
dispatchStrikethrough() {
|
|
1018
|
+
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough");
|
|
1019
|
+
}
|
|
1020
|
+
|
|
886
1021
|
dispatchLink(url) {
|
|
887
1022
|
this.#toggleLink(url);
|
|
888
1023
|
}
|
|
@@ -893,7 +1028,7 @@ class CommandDispatcher {
|
|
|
893
1028
|
|
|
894
1029
|
dispatchInsertUnorderedList() {
|
|
895
1030
|
const selection = $getSelection();
|
|
896
|
-
if (!selection) return
|
|
1031
|
+
if (!selection) return
|
|
897
1032
|
|
|
898
1033
|
const anchorNode = selection.anchor.getNode();
|
|
899
1034
|
|
|
@@ -906,7 +1041,7 @@ class CommandDispatcher {
|
|
|
906
1041
|
|
|
907
1042
|
dispatchInsertOrderedList() {
|
|
908
1043
|
const selection = $getSelection();
|
|
909
|
-
if (!selection) return
|
|
1044
|
+
if (!selection) return
|
|
910
1045
|
|
|
911
1046
|
const anchorNode = selection.anchor.getNode();
|
|
912
1047
|
|
|
@@ -931,6 +1066,12 @@ class CommandDispatcher {
|
|
|
931
1066
|
});
|
|
932
1067
|
}
|
|
933
1068
|
|
|
1069
|
+
dispatchInsertHorizontalDivider() {
|
|
1070
|
+
this.editor.update(() => {
|
|
1071
|
+
this.contents.insertAtCursor(new HorizontalDividerNode());
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
|
|
934
1075
|
dispatchRotateHeadingFormat() {
|
|
935
1076
|
this.editor.update(() => {
|
|
936
1077
|
const selection = $getSelection();
|
|
@@ -978,6 +1119,14 @@ class CommandDispatcher {
|
|
|
978
1119
|
setTimeout(() => input.remove(), 1000);
|
|
979
1120
|
}
|
|
980
1121
|
|
|
1122
|
+
dispatchUndo() {
|
|
1123
|
+
this.editor.dispatchCommand(UNDO_COMMAND, undefined);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
dispatchRedo() {
|
|
1127
|
+
this.editor.dispatchCommand(REDO_COMMAND, undefined);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
981
1130
|
#registerCommands() {
|
|
982
1131
|
for (const command of COMMANDS) {
|
|
983
1132
|
const methodName = `dispatch${capitalize(command)}`;
|
|
@@ -1335,11 +1484,11 @@ class Selection {
|
|
|
1335
1484
|
// above when navigating UP/DOWN when Lexical shows its fake cursor on custom decorator nodes.
|
|
1336
1485
|
this.editorContentElement.addEventListener("keydown", (event) => {
|
|
1337
1486
|
if (event.key === "ArrowUp") {
|
|
1338
|
-
const lexicalCursor = this.editor.getRootElement().querySelector(
|
|
1487
|
+
const lexicalCursor = this.editor.getRootElement().querySelector("[data-lexical-cursor]");
|
|
1339
1488
|
|
|
1340
1489
|
if (lexicalCursor) {
|
|
1341
1490
|
let currentElement = lexicalCursor.previousElementSibling;
|
|
1342
|
-
while (currentElement && currentElement.hasAttribute(
|
|
1491
|
+
while (currentElement && currentElement.hasAttribute("data-lexical-cursor")) {
|
|
1343
1492
|
currentElement = currentElement.previousElementSibling;
|
|
1344
1493
|
}
|
|
1345
1494
|
|
|
@@ -1350,11 +1499,11 @@ class Selection {
|
|
|
1350
1499
|
}
|
|
1351
1500
|
|
|
1352
1501
|
if (event.key === "ArrowDown") {
|
|
1353
|
-
const lexicalCursor = this.editor.getRootElement().querySelector(
|
|
1502
|
+
const lexicalCursor = this.editor.getRootElement().querySelector("[data-lexical-cursor]");
|
|
1354
1503
|
|
|
1355
1504
|
if (lexicalCursor) {
|
|
1356
1505
|
let currentElement = lexicalCursor.nextElementSibling;
|
|
1357
|
-
while (currentElement && currentElement.hasAttribute(
|
|
1506
|
+
while (currentElement && currentElement.hasAttribute("data-lexical-cursor")) {
|
|
1358
1507
|
currentElement = currentElement.nextElementSibling;
|
|
1359
1508
|
}
|
|
1360
1509
|
|
|
@@ -1542,7 +1691,7 @@ class Selection {
|
|
|
1542
1691
|
}
|
|
1543
1692
|
|
|
1544
1693
|
#isRectUnreliable(rect) {
|
|
1545
|
-
return
|
|
1694
|
+
return rect.width === 0 && rect.height === 0 || rect.top === 0 && rect.left === 0
|
|
1546
1695
|
}
|
|
1547
1696
|
|
|
1548
1697
|
#createAndInsertMarker(range) {
|
|
@@ -1573,7 +1722,7 @@ class Selection {
|
|
|
1573
1722
|
|
|
1574
1723
|
#calculateCursorPosition(rect, range) {
|
|
1575
1724
|
const rootRect = this.editor.getRootElement().getBoundingClientRect();
|
|
1576
|
-
|
|
1725
|
+
const x = rect.left - rootRect.left;
|
|
1577
1726
|
let y = rect.top - rootRect.top;
|
|
1578
1727
|
|
|
1579
1728
|
const fontSize = this.#getFontSizeForCursor(range);
|
|
@@ -1788,6 +1937,23 @@ class Contents {
|
|
|
1788
1937
|
});
|
|
1789
1938
|
}
|
|
1790
1939
|
|
|
1940
|
+
insertAtCursor(node) {
|
|
1941
|
+
this.editor.update(() => {
|
|
1942
|
+
const selection = $getSelection();
|
|
1943
|
+
const selectedNodes = selection?.getNodes();
|
|
1944
|
+
|
|
1945
|
+
if ($isRangeSelection(selection)) {
|
|
1946
|
+
$insertNodes([ node ]);
|
|
1947
|
+
} else if ($isNodeSelection(selection) && selectedNodes && selectedNodes.length > 0) {
|
|
1948
|
+
const lastNode = selectedNodes[selectedNodes.length - 1];
|
|
1949
|
+
lastNode.insertAfter(node);
|
|
1950
|
+
} else {
|
|
1951
|
+
const root = $getRoot();
|
|
1952
|
+
root.append(node);
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1791
1957
|
insertNodeWrappingEachSelectedLine(newNodeFn) {
|
|
1792
1958
|
this.editor.update(() => {
|
|
1793
1959
|
const selection = $getSelection();
|
|
@@ -1859,22 +2025,6 @@ class Contents {
|
|
|
1859
2025
|
return result
|
|
1860
2026
|
}
|
|
1861
2027
|
|
|
1862
|
-
hasSelectedWords() {
|
|
1863
|
-
let result = false;
|
|
1864
|
-
|
|
1865
|
-
this.editor.update(() => {
|
|
1866
|
-
const selection = $getSelection();
|
|
1867
|
-
if (!$isRangeSelection(selection)) return
|
|
1868
|
-
|
|
1869
|
-
// Check if we have selected text within a line (not entire lines)
|
|
1870
|
-
result = !selection.isCollapsed() &&
|
|
1871
|
-
selection.anchor.getNode().getTopLevelElement() ===
|
|
1872
|
-
selection.focus.getNode().getTopLevelElement();
|
|
1873
|
-
});
|
|
1874
|
-
|
|
1875
|
-
return result
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
2028
|
unwrapSelectedListItems() {
|
|
1879
2029
|
this.editor.update(() => {
|
|
1880
2030
|
const selection = $getSelection();
|
|
@@ -1899,7 +2049,7 @@ class Contents {
|
|
|
1899
2049
|
|
|
1900
2050
|
const selection = $getSelection();
|
|
1901
2051
|
if ($isRangeSelection(selection)) {
|
|
1902
|
-
selection.insertNodes([linkNode]);
|
|
2052
|
+
selection.insertNodes([ linkNode ]);
|
|
1903
2053
|
linkNodeKey = linkNode.getKey();
|
|
1904
2054
|
}
|
|
1905
2055
|
});
|
|
@@ -1965,7 +2115,7 @@ class Contents {
|
|
|
1965
2115
|
}
|
|
1966
2116
|
|
|
1967
2117
|
replaceTextBackUntil(stringToReplace, replacementNodes) {
|
|
1968
|
-
replacementNodes = Array.isArray(replacementNodes) ? replacementNodes : [replacementNodes];
|
|
2118
|
+
replacementNodes = Array.isArray(replacementNodes) ? replacementNodes : [ replacementNodes ];
|
|
1969
2119
|
|
|
1970
2120
|
this.editor.update(() => {
|
|
1971
2121
|
const { anchorNode, offset } = this.#getTextAnchorData();
|
|
@@ -2016,55 +2166,30 @@ class Contents {
|
|
|
2016
2166
|
const blobUrlTemplate = this.editorElement.blobUrlTemplate;
|
|
2017
2167
|
|
|
2018
2168
|
this.editor.update(() => {
|
|
2019
|
-
const selection = $getSelection();
|
|
2020
|
-
const anchorNode = selection?.anchor.getNode();
|
|
2021
|
-
const currentParagraph = anchorNode?.getTopLevelElement();
|
|
2022
|
-
|
|
2023
2169
|
const uploadedImageNode = new ActionTextAttachmentUploadNode({ file: file, uploadUrl: uploadUrl, blobUrlTemplate: blobUrlTemplate, editor: this.editor });
|
|
2024
|
-
|
|
2025
|
-
if (currentParagraph && $isParagraphNode(currentParagraph) && currentParagraph.getChildrenSize() === 0) {
|
|
2026
|
-
// If we're inside an empty paragraph, replace it
|
|
2027
|
-
currentParagraph.replace(uploadedImageNode);
|
|
2028
|
-
} else if (currentParagraph && $isElementNode(currentParagraph)) {
|
|
2029
|
-
currentParagraph.insertAfter(uploadedImageNode);
|
|
2030
|
-
} else {
|
|
2031
|
-
$insertNodes([uploadedImageNode]);
|
|
2032
|
-
}
|
|
2170
|
+
this.insertAtCursor(uploadedImageNode);
|
|
2033
2171
|
}, { tag: HISTORY_MERGE_TAG });
|
|
2034
2172
|
}
|
|
2035
2173
|
|
|
2036
|
-
deleteSelectedNodes() {
|
|
2174
|
+
async deleteSelectedNodes() {
|
|
2175
|
+
let focusNode = null;
|
|
2176
|
+
|
|
2037
2177
|
this.editor.update(() => {
|
|
2038
2178
|
if ($isNodeSelection(this.#selection.current)) {
|
|
2039
2179
|
const nodesToRemove = this.#selection.current.getNodes();
|
|
2040
2180
|
if (nodesToRemove.length === 0) return
|
|
2041
2181
|
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
const parent = node.getParent();
|
|
2047
|
-
if (!$isElementNode(parent)) return
|
|
2048
|
-
|
|
2049
|
-
const children = parent.getChildren();
|
|
2050
|
-
const index = children.indexOf(node);
|
|
2051
|
-
|
|
2052
|
-
if (index >= 0) {
|
|
2053
|
-
parent.splice(index, 1, []);
|
|
2054
|
-
}
|
|
2055
|
-
});
|
|
2056
|
-
|
|
2057
|
-
// Check if root is empty after all removals
|
|
2058
|
-
const root = $getRoot();
|
|
2059
|
-
if (root.getChildrenSize() === 0) {
|
|
2060
|
-
root.append($createParagraphNode());
|
|
2061
|
-
}
|
|
2182
|
+
focusNode = this.#findAdjacentNodeTo(nodesToRemove);
|
|
2183
|
+
this.#deleteNodes(nodesToRemove);
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2062
2186
|
|
|
2063
|
-
|
|
2064
|
-
this.editor.focus();
|
|
2187
|
+
await nextFrame();
|
|
2065
2188
|
|
|
2066
|
-
|
|
2067
|
-
|
|
2189
|
+
this.editor.update(() => {
|
|
2190
|
+
this.#selectAfterDeletion(focusNode);
|
|
2191
|
+
this.#selection.clear();
|
|
2192
|
+
this.editor.focus();
|
|
2068
2193
|
});
|
|
2069
2194
|
}
|
|
2070
2195
|
|
|
@@ -2122,7 +2247,7 @@ class Contents {
|
|
|
2122
2247
|
wrappingNode.append(...topLevelElement.getChildren());
|
|
2123
2248
|
topLevelElement.replace(wrappingNode);
|
|
2124
2249
|
} else {
|
|
2125
|
-
$insertNodes([newNodeFn()]);
|
|
2250
|
+
$insertNodes([ newNodeFn() ]);
|
|
2126
2251
|
}
|
|
2127
2252
|
}
|
|
2128
2253
|
|
|
@@ -2199,6 +2324,45 @@ class Contents {
|
|
|
2199
2324
|
nodesToDelete.forEach((node) => node.remove());
|
|
2200
2325
|
}
|
|
2201
2326
|
|
|
2327
|
+
#deleteNodes(nodes) {
|
|
2328
|
+
// Use splice() instead of node.remove() for proper removal and
|
|
2329
|
+
// reconciliation. Would have issues with removing unintended decorator nodes
|
|
2330
|
+
// with node.remove()
|
|
2331
|
+
nodes.forEach((node) => {
|
|
2332
|
+
const parent = node.getParent();
|
|
2333
|
+
if (!$isElementNode(parent)) return
|
|
2334
|
+
|
|
2335
|
+
const children = parent.getChildren();
|
|
2336
|
+
const index = children.indexOf(node);
|
|
2337
|
+
|
|
2338
|
+
if (index >= 0) {
|
|
2339
|
+
parent.splice(index, 1, []);
|
|
2340
|
+
}
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
#findAdjacentNodeTo(nodes) {
|
|
2345
|
+
const firstNode = nodes[0];
|
|
2346
|
+
const lastNode = nodes[nodes.length - 1];
|
|
2347
|
+
|
|
2348
|
+
return firstNode?.getPreviousSibling() || lastNode?.getNextSibling()
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
#selectAfterDeletion(focusNode) {
|
|
2352
|
+
const root = $getRoot();
|
|
2353
|
+
if (root.getChildrenSize() === 0) {
|
|
2354
|
+
const newParagraph = $createParagraphNode();
|
|
2355
|
+
root.append(newParagraph);
|
|
2356
|
+
newParagraph.selectStart();
|
|
2357
|
+
} else if (focusNode) {
|
|
2358
|
+
if ($isTextNode(focusNode) || $isParagraphNode(focusNode)) {
|
|
2359
|
+
focusNode.selectEnd();
|
|
2360
|
+
} else {
|
|
2361
|
+
focusNode.selectNext(0, 0);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2202
2366
|
#collectSelectedListItems(selection) {
|
|
2203
2367
|
const nodes = selection.getNodes();
|
|
2204
2368
|
const listItems = new Set();
|
|
@@ -2290,8 +2454,8 @@ class Contents {
|
|
|
2290
2454
|
firstParagraph.selectStart();
|
|
2291
2455
|
const currentSelection = $getSelection();
|
|
2292
2456
|
if (currentSelection && $isRangeSelection(currentSelection)) {
|
|
2293
|
-
currentSelection.anchor.set(firstParagraph.getKey(), 0,
|
|
2294
|
-
currentSelection.focus.set(lastParagraph.getKey(), lastParagraph.getChildrenSize(),
|
|
2457
|
+
currentSelection.anchor.set(firstParagraph.getKey(), 0, "element");
|
|
2458
|
+
currentSelection.focus.set(lastParagraph.getKey(), lastParagraph.getChildrenSize(), "element");
|
|
2295
2459
|
}
|
|
2296
2460
|
}
|
|
2297
2461
|
|
|
@@ -2345,14 +2509,14 @@ class Contents {
|
|
|
2345
2509
|
const last = children[children.length - 1];
|
|
2346
2510
|
const beforeLast = children[children.length - 2];
|
|
2347
2511
|
|
|
2348
|
-
if (
|
|
2512
|
+
if ($isTextNode(last) && last.getTextContent() === "" && (beforeLast && !$isTextNode(beforeLast))) {
|
|
2349
2513
|
paragraph.append($createLineBreakNode());
|
|
2350
2514
|
}
|
|
2351
2515
|
}
|
|
2352
2516
|
}
|
|
2353
2517
|
|
|
2354
2518
|
#createCustomAttachmentNodeWithHtml(html, options = {}) {
|
|
2355
|
-
const attachmentConfig = typeof options ===
|
|
2519
|
+
const attachmentConfig = typeof options === "object" ? options : {};
|
|
2356
2520
|
|
|
2357
2521
|
return new CustomActionTextAttachmentNode({
|
|
2358
2522
|
sgid: attachmentConfig.sgid || null,
|
|
@@ -2367,7 +2531,7 @@ class Contents {
|
|
|
2367
2531
|
}
|
|
2368
2532
|
|
|
2369
2533
|
#shouldUploadFile(file) {
|
|
2370
|
-
return dispatch(this.editorElement,
|
|
2534
|
+
return dispatch(this.editorElement, "lexxy:file-accept", { file }, true)
|
|
2371
2535
|
}
|
|
2372
2536
|
}
|
|
2373
2537
|
|
|
@@ -2375,7 +2539,7 @@ function isUrl(string) {
|
|
|
2375
2539
|
try {
|
|
2376
2540
|
new URL(string);
|
|
2377
2541
|
return true
|
|
2378
|
-
} catch
|
|
2542
|
+
} catch {
|
|
2379
2543
|
return false
|
|
2380
2544
|
}
|
|
2381
2545
|
}
|
|
@@ -2450,7 +2614,7 @@ class Clipboard {
|
|
|
2450
2614
|
#handlePastedFiles(clipboardData) {
|
|
2451
2615
|
if (!this.editorElement.supportsAttachments) return
|
|
2452
2616
|
|
|
2453
|
-
const html = clipboardData.getData(
|
|
2617
|
+
const html = clipboardData.getData("text/html");
|
|
2454
2618
|
if (html) return // Ignore if image copied from browser since we will load it as a remote image
|
|
2455
2619
|
|
|
2456
2620
|
this.#preservingScrollPosition(() => {
|
|
@@ -2480,7 +2644,7 @@ class Clipboard {
|
|
|
2480
2644
|
class LexicalEditorElement extends HTMLElement {
|
|
2481
2645
|
static formAssociated = true
|
|
2482
2646
|
static debug = true
|
|
2483
|
-
static commands = [ "bold", "italic", "" ]
|
|
2647
|
+
static commands = [ "bold", "italic", "strikethrough" ]
|
|
2484
2648
|
|
|
2485
2649
|
static observedAttributes = [ "connected", "required" ]
|
|
2486
2650
|
|
|
@@ -2642,6 +2806,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
2642
2806
|
CodeHighlightNode,
|
|
2643
2807
|
LinkNode,
|
|
2644
2808
|
AutoLinkNode,
|
|
2809
|
+
HorizontalDividerNode,
|
|
2645
2810
|
|
|
2646
2811
|
CustomActionTextAttachmentNode,
|
|
2647
2812
|
];
|
|
@@ -2736,7 +2901,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
2736
2901
|
|
|
2737
2902
|
#registerComponents() {
|
|
2738
2903
|
registerRichText(this.editor);
|
|
2739
|
-
|
|
2904
|
+
this.historyState = createEmptyHistoryState();
|
|
2905
|
+
registerHistory(this.editor, this.historyState, 20);
|
|
2740
2906
|
registerList(this.editor);
|
|
2741
2907
|
this.#registerCodeHiglightingComponents();
|
|
2742
2908
|
registerMarkdownShortcuts(this.editor, TRANSFORMERS);
|
|
@@ -2813,6 +2979,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
2813
2979
|
#createDefaultToolbar() {
|
|
2814
2980
|
const toolbar = createElement("lexxy-toolbar");
|
|
2815
2981
|
toolbar.innerHTML = LexicalToolbarElement.defaultTemplate;
|
|
2982
|
+
toolbar.setAttribute("data-attachments", this.supportsAttachments); // Drives toolbar CSS styles
|
|
2816
2983
|
this.prepend(toolbar);
|
|
2817
2984
|
return toolbar
|
|
2818
2985
|
}
|
|
@@ -3366,7 +3533,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
3366
3533
|
popoverContainer.classList.add("lexxy-prompt-menu");
|
|
3367
3534
|
popoverContainer.style.position = "absolute";
|
|
3368
3535
|
popoverContainer.setAttribute("nonce", getNonce());
|
|
3369
|
-
popoverContainer.append(...
|
|
3536
|
+
popoverContainer.append(...await this.source.buildListItems());
|
|
3370
3537
|
popoverContainer.addEventListener("click", this.#handlePopoverClick);
|
|
3371
3538
|
this.#editorElement.appendChild(popoverContainer);
|
|
3372
3539
|
return popoverContainer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@37signals/lexxy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14-beta",
|
|
4
4
|
"description": "Lexxy - A modern rich text editor for Rails.",
|
|
5
5
|
"module": "dist/lexxy.esm.js",
|
|
6
6
|
"type": "module",
|
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
"author": "Jorge Manrubia <jorge@37signals.com>",
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"devDependencies": {
|
|
15
|
+
"@eslint/js": "^9.15.0",
|
|
15
16
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
16
17
|
"@rollup/plugin-terser": "^0.4.4",
|
|
18
|
+
"eslint": "^9.15.0",
|
|
17
19
|
"rollup": "^4.44.1",
|
|
18
20
|
"rollup-plugin-gzip": "^4.1.1"
|
|
19
21
|
},
|
|
@@ -21,6 +23,7 @@
|
|
|
21
23
|
"build": "rollup -c",
|
|
22
24
|
"build:npm": "rollup -c rollup.config.npm.mjs",
|
|
23
25
|
"watch": "rollup -wc",
|
|
26
|
+
"lint": "eslint",
|
|
24
27
|
"prerelease": "yarn build:npm",
|
|
25
28
|
"release": "yarn build:npm && yarn publish"
|
|
26
29
|
},
|