@37signals/lexxy 0.1.25-beta → 0.1.27-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 +1 -1
- package/dist/lexxy.esm.js +260 -72
- package/dist/stylesheets/lexxy-editor.css +25 -106
- package/package.json +2 -2
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.26.beta' # Need to specify the version since it's a pre-release
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
And then execute:
|
package/dist/lexxy.esm.js
CHANGED
|
@@ -10,16 +10,17 @@ import 'prismjs/components/prism-json';
|
|
|
10
10
|
import 'prismjs/components/prism-diff';
|
|
11
11
|
import DOMPurify from 'dompurify';
|
|
12
12
|
import { getStyleObjectFromCSS, getCSSFromStyleObject, $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/selection';
|
|
13
|
-
import { $isTextNode, TextNode, $isRangeSelection, $getSelection, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_TAB_COMMAND, COMMAND_PRIORITY_NORMAL, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, 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,
|
|
14
|
-
import { $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $
|
|
13
|
+
import { $isTextNode, TextNode, $isRangeSelection, SKIP_DOM_SELECTION_TAG, $getSelection, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_TAB_COMMAND, COMMAND_PRIORITY_NORMAL, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, 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, createEditor, BLUR_COMMAND, FOCUS_COMMAND, KEY_DOWN_COMMAND, KEY_SPACE_COMMAND } from 'lexical';
|
|
14
|
+
import { $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, ListNode, $getListDepth, $createListNode, ListItemNode, registerList } from '@lexical/list';
|
|
15
15
|
import { $isQuoteNode, $isHeadingNode, $createQuoteNode, $createHeadingNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
16
16
|
import { $isCodeNode, CodeNode, normalizeCodeLang, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
|
|
17
17
|
import { $isLinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, LinkNode, AutoLinkNode } from '@lexical/link';
|
|
18
|
-
import { $getTableCellNodeFromLexicalNode, INSERT_TABLE_COMMAND, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode,
|
|
18
|
+
import { $getTableCellNodeFromLexicalNode, INSERT_TABLE_COMMAND, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, TableCellNode, TableNode, TableRowNode, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $getElementForTableNode, $isTableCellNode, TableCellHeaderStates } from '@lexical/table';
|
|
19
19
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
20
20
|
import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown';
|
|
21
21
|
import { createEmptyHistoryState, registerHistory } from '@lexical/history';
|
|
22
22
|
import { DirectUpload } from '@rails/activestorage';
|
|
23
|
+
import { $getNearestNodeOfType } from '@lexical/utils';
|
|
23
24
|
import { marked } from 'marked';
|
|
24
25
|
|
|
25
26
|
// Configure Prism for manual highlighting mode
|
|
@@ -145,6 +146,93 @@ function hasHighlightStyles(cssOrStyles) {
|
|
|
145
146
|
return !!(styles.color || styles["background-color"])
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
function handleRollingTabIndex(elements, event) {
|
|
150
|
+
const previousActiveElement = document.activeElement;
|
|
151
|
+
|
|
152
|
+
if (elements.includes(previousActiveElement)) {
|
|
153
|
+
const finder = new NextElementFinder(elements, event.key);
|
|
154
|
+
|
|
155
|
+
if (finder.selectNext(previousActiveElement)) {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
class NextElementFinder {
|
|
162
|
+
constructor(elements, key) {
|
|
163
|
+
this.elements = elements;
|
|
164
|
+
this.key = key;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
selectNext(fromElement) {
|
|
168
|
+
const nextElement = this.#findNextElement(fromElement);
|
|
169
|
+
|
|
170
|
+
if (nextElement) {
|
|
171
|
+
const inactiveElements = this.elements.filter(element => element !== nextElement);
|
|
172
|
+
this.#unsetTabIndex(inactiveElements);
|
|
173
|
+
this.#focusWithActiveTabIndex(nextElement);
|
|
174
|
+
return true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#findNextElement(fromElement) {
|
|
181
|
+
switch (this.key) {
|
|
182
|
+
case "ArrowRight":
|
|
183
|
+
case "ArrowDown":
|
|
184
|
+
return this.#findNextSibling(fromElement)
|
|
185
|
+
|
|
186
|
+
case "ArrowLeft":
|
|
187
|
+
case "ArrowUp":
|
|
188
|
+
return this.#findPreviousSibling(fromElement)
|
|
189
|
+
|
|
190
|
+
case "Home":
|
|
191
|
+
return this.#findFirst()
|
|
192
|
+
|
|
193
|
+
case "End":
|
|
194
|
+
return this.#findLast()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#findFirst(elements = this.elements) {
|
|
199
|
+
return elements.find(isActiveAndVisible)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#findLast(elements = this.elements) {
|
|
203
|
+
return elements.findLast(isActiveAndVisible)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#findNextSibling(element) {
|
|
207
|
+
const afterElements = this.elements.slice(this.#indexOf(element) + 1);
|
|
208
|
+
return this.#findFirst(afterElements)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#findPreviousSibling(element) {
|
|
212
|
+
const beforeElements = this.elements.slice(0, this.#indexOf(element));
|
|
213
|
+
return this.#findLast(beforeElements)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#indexOf(element) {
|
|
217
|
+
return this.elements.indexOf(element)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#focusWithActiveTabIndex(element) {
|
|
221
|
+
if (isActiveAndVisible(element)) {
|
|
222
|
+
element.tabIndex = 0;
|
|
223
|
+
element.focus();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#unsetTabIndex(elements) {
|
|
228
|
+
elements.forEach(element => element.tabIndex = -1);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isActiveAndVisible(element) {
|
|
233
|
+
return element && !element.disabled && element.checkVisibility()
|
|
234
|
+
}
|
|
235
|
+
|
|
148
236
|
class LexicalToolbarElement extends HTMLElement {
|
|
149
237
|
static observedAttributes = [ "connected" ]
|
|
150
238
|
|
|
@@ -156,17 +244,14 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
156
244
|
|
|
157
245
|
connectedCallback() {
|
|
158
246
|
requestAnimationFrame(() => this.#refreshToolbarOverflow());
|
|
159
|
-
|
|
160
|
-
this
|
|
161
|
-
this._resizeObserver.observe(this);
|
|
247
|
+
this.setAttribute("role", "toolbar");
|
|
248
|
+
this.#installResizeObserver();
|
|
162
249
|
}
|
|
163
250
|
|
|
164
251
|
disconnectedCallback() {
|
|
165
|
-
|
|
166
|
-
this._resizeObserver.disconnect();
|
|
167
|
-
this._resizeObserver = null;
|
|
168
|
-
}
|
|
252
|
+
this.#uninstallResizeObserver();
|
|
169
253
|
this.#unbindHotkeys();
|
|
254
|
+
this.#unbindFocusListeners();
|
|
170
255
|
}
|
|
171
256
|
|
|
172
257
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
@@ -180,11 +265,12 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
180
265
|
this.editor = editorElement.editor;
|
|
181
266
|
this.#bindButtons();
|
|
182
267
|
this.#bindHotkeys();
|
|
183
|
-
this.#
|
|
268
|
+
this.#resetTabIndexValues();
|
|
184
269
|
this.#setItemPositionValues();
|
|
185
270
|
this.#monitorSelectionChanges();
|
|
186
271
|
this.#monitorHistoryChanges();
|
|
187
272
|
this.#refreshToolbarOverflow();
|
|
273
|
+
this.#bindFocusListeners();
|
|
188
274
|
|
|
189
275
|
this.toggleAttribute("connected", true);
|
|
190
276
|
}
|
|
@@ -194,24 +280,39 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
194
280
|
this.connectedCallback();
|
|
195
281
|
}
|
|
196
282
|
|
|
283
|
+
#installResizeObserver() {
|
|
284
|
+
this.resizeObserver = new ResizeObserver(() => this.#refreshToolbarOverflow());
|
|
285
|
+
this.resizeObserver.observe(this);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#uninstallResizeObserver() {
|
|
289
|
+
if (this.resizeObserver) {
|
|
290
|
+
this.resizeObserver.disconnect();
|
|
291
|
+
this.resizeObserver = null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
197
295
|
#bindButtons() {
|
|
198
296
|
this.addEventListener("click", this.#handleButtonClicked.bind(this));
|
|
199
297
|
}
|
|
200
298
|
|
|
201
|
-
#handleButtonClicked(
|
|
202
|
-
this.#handleTargetClicked(
|
|
299
|
+
#handleButtonClicked(event) {
|
|
300
|
+
this.#handleTargetClicked(event, "[data-command]", this.#dispatchButtonCommand.bind(this));
|
|
203
301
|
}
|
|
204
302
|
|
|
205
|
-
#handleTargetClicked(
|
|
206
|
-
const button = target.closest(selector);
|
|
303
|
+
#handleTargetClicked(event, selector, callback) {
|
|
304
|
+
const button = event.target.closest(selector);
|
|
207
305
|
if (button) {
|
|
208
|
-
callback(button);
|
|
306
|
+
callback(event, button);
|
|
209
307
|
}
|
|
210
308
|
}
|
|
211
309
|
|
|
212
|
-
#dispatchButtonCommand(
|
|
213
|
-
const
|
|
214
|
-
|
|
310
|
+
#dispatchButtonCommand(event, { dataset: { command, payload } }) {
|
|
311
|
+
const isKeyboard = event instanceof PointerEvent && event.pointerId === -1;
|
|
312
|
+
|
|
313
|
+
this.editor.update(() => {
|
|
314
|
+
this.editor.dispatchCommand(command, payload);
|
|
315
|
+
}, { tag: isKeyboard ? SKIP_DOM_SELECTION_TAG : undefined } );
|
|
215
316
|
}
|
|
216
317
|
|
|
217
318
|
#bindHotkeys() {
|
|
@@ -246,9 +347,38 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
246
347
|
return [ ...modifiers, pressedKey ].join("+")
|
|
247
348
|
}
|
|
248
349
|
|
|
249
|
-
#
|
|
250
|
-
this
|
|
251
|
-
|
|
350
|
+
#bindFocusListeners() {
|
|
351
|
+
this.editorElement.addEventListener("lexxy:focus", this.#handleFocus);
|
|
352
|
+
this.editorElement.addEventListener("lexxy:blur", this.#handleFocusOut);
|
|
353
|
+
this.addEventListener("focusout", this.#handleFocusOut);
|
|
354
|
+
this.addEventListener("keydown", this.#handleKeydown);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#unbindFocusListeners() {
|
|
358
|
+
this.editorElement.removeEventListener("lexxy:focus", this.#handleFocus);
|
|
359
|
+
this.editorElement.removeEventListener("lexxy:blur", this.#handleFocusOut);
|
|
360
|
+
this.removeEventListener("focusout", this.#handleFocusOut);
|
|
361
|
+
this.removeEventListener("keydown", this.#handleKeydown);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
#handleFocus = () => {
|
|
365
|
+
this.#resetTabIndexValues();
|
|
366
|
+
this.#focusableItems[0].tabIndex = 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#handleFocusOut = () => {
|
|
370
|
+
if (!this.contains(document.activeElement)) {
|
|
371
|
+
this.#resetTabIndexValues();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#handleKeydown = (event) => {
|
|
376
|
+
handleRollingTabIndex(this.#focusableItems, event);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#resetTabIndexValues() {
|
|
380
|
+
this.#focusableItems.forEach((button) => {
|
|
381
|
+
button.tabIndex = -1;
|
|
252
382
|
});
|
|
253
383
|
}
|
|
254
384
|
|
|
@@ -358,6 +488,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
358
488
|
|
|
359
489
|
const isOverflowing = this.#overflowMenu.children.length > 0;
|
|
360
490
|
this.toggleAttribute("overflowing", isOverflowing);
|
|
491
|
+
this.#overflowMenu.toggleAttribute("disabled", !isOverflowing);
|
|
361
492
|
}
|
|
362
493
|
|
|
363
494
|
#compactMenu() {
|
|
@@ -409,6 +540,10 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
409
540
|
return Array.from(this.querySelectorAll(":scope > button"))
|
|
410
541
|
}
|
|
411
542
|
|
|
543
|
+
get #focusableItems() {
|
|
544
|
+
return Array.from(this.querySelectorAll(":scope button, :scope > details > summary"))
|
|
545
|
+
}
|
|
546
|
+
|
|
412
547
|
get #toolbarItems() {
|
|
413
548
|
return Array.from(this.querySelectorAll(":scope > *:not(.lexxy-editor__toolbar-overflow)"))
|
|
414
549
|
}
|
|
@@ -1386,7 +1521,7 @@ class CommandDispatcher {
|
|
|
1386
1521
|
}
|
|
1387
1522
|
|
|
1388
1523
|
#registerKeyboardCommands() {
|
|
1389
|
-
this.editor.registerCommand(KEY_TAB_COMMAND, this.#
|
|
1524
|
+
this.editor.registerCommand(KEY_TAB_COMMAND, this.#handleTabKey.bind(this), COMMAND_PRIORITY_NORMAL);
|
|
1390
1525
|
}
|
|
1391
1526
|
|
|
1392
1527
|
#registerDragAndDropHandlers() {
|
|
@@ -1436,18 +1571,28 @@ class CommandDispatcher {
|
|
|
1436
1571
|
this.editor.focus();
|
|
1437
1572
|
}
|
|
1438
1573
|
|
|
1439
|
-
#
|
|
1574
|
+
#handleTabKey(event) {
|
|
1440
1575
|
if (this.selection.isInsideList) {
|
|
1441
|
-
event
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
} else {
|
|
1445
|
-
return this.editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined)
|
|
1446
|
-
}
|
|
1576
|
+
return this.#handleTabForList(event)
|
|
1577
|
+
} else if (this.selection.isInsideCodeBlock) {
|
|
1578
|
+
return this.#handleTabForCode()
|
|
1447
1579
|
}
|
|
1448
1580
|
return false
|
|
1449
1581
|
}
|
|
1450
1582
|
|
|
1583
|
+
#handleTabForList(event) {
|
|
1584
|
+
if (event.shiftKey && !this.selection.isIndentedList) return false
|
|
1585
|
+
|
|
1586
|
+
event.preventDefault();
|
|
1587
|
+
const command = event.shiftKey? OUTDENT_CONTENT_COMMAND : INDENT_CONTENT_COMMAND;
|
|
1588
|
+
return this.editor.dispatchCommand(command)
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
#handleTabForCode() {
|
|
1592
|
+
const selection = $getSelection();
|
|
1593
|
+
return $isRangeSelection(selection) && selection.isCollapsed()
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1451
1596
|
// Not using TOGGLE_LINK_COMMAND because it's not handled unless you use React/LinkPlugin
|
|
1452
1597
|
#toggleLink(url) {
|
|
1453
1598
|
this.editor.update(() => {
|
|
@@ -1500,26 +1645,19 @@ class Selection {
|
|
|
1500
1645
|
this.#containEditorFocus();
|
|
1501
1646
|
}
|
|
1502
1647
|
|
|
1503
|
-
clear() {
|
|
1504
|
-
this.current = null;
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
1648
|
set current(selection) {
|
|
1508
|
-
|
|
1509
|
-
this
|
|
1510
|
-
|
|
1511
|
-
this.#syncSelectedClasses();
|
|
1512
|
-
});
|
|
1513
|
-
} else {
|
|
1514
|
-
this.editor.update(() => {
|
|
1515
|
-
this.#syncSelectedClasses();
|
|
1516
|
-
this._current = null;
|
|
1517
|
-
});
|
|
1518
|
-
}
|
|
1649
|
+
this.editor.update(() => {
|
|
1650
|
+
this.#syncSelectedClasses();
|
|
1651
|
+
});
|
|
1519
1652
|
}
|
|
1520
1653
|
|
|
1521
|
-
get
|
|
1522
|
-
|
|
1654
|
+
get hasNodeSelection() {
|
|
1655
|
+
let result = false;
|
|
1656
|
+
this.editor.getEditorState().read(() => {
|
|
1657
|
+
const selection = $getSelection();
|
|
1658
|
+
result = selection !== null && $isNodeSelection(selection);
|
|
1659
|
+
});
|
|
1660
|
+
return result
|
|
1523
1661
|
}
|
|
1524
1662
|
|
|
1525
1663
|
get cursorPosition() {
|
|
@@ -1624,6 +1762,29 @@ class Selection {
|
|
|
1624
1762
|
return getNearestListItemNode(anchorNode) !== null
|
|
1625
1763
|
}
|
|
1626
1764
|
|
|
1765
|
+
get isIndentedList() {
|
|
1766
|
+
const selection = $getSelection();
|
|
1767
|
+
if (!$isRangeSelection(selection)) return false
|
|
1768
|
+
|
|
1769
|
+
const nodes = selection.getNodes();
|
|
1770
|
+
for (const node of nodes) {
|
|
1771
|
+
const closestListNode = $getNearestNodeOfType(node, ListNode);
|
|
1772
|
+
if (closestListNode && $getListDepth(closestListNode) > 1) {
|
|
1773
|
+
return true
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
return false
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
get isInsideCodeBlock() {
|
|
1781
|
+
const selection = $getSelection();
|
|
1782
|
+
if (!$isRangeSelection(selection)) return false
|
|
1783
|
+
|
|
1784
|
+
const anchorNode = selection.anchor.getNode();
|
|
1785
|
+
return $getNearestNodeOfType(anchorNode, CodeNode) !== null
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1627
1788
|
get nodeAfterCursor() {
|
|
1628
1789
|
const { anchorNode, offset } = this.#getCollapsedSelectionData();
|
|
1629
1790
|
if (!anchorNode) return null
|
|
@@ -1689,18 +1850,18 @@ class Selection {
|
|
|
1689
1850
|
}
|
|
1690
1851
|
|
|
1691
1852
|
get #currentlySelectedKeys() {
|
|
1692
|
-
if (this.
|
|
1853
|
+
if (this.currentlySelectedKeys) { return this.currentlySelectedKeys }
|
|
1693
1854
|
|
|
1694
|
-
this.
|
|
1855
|
+
this.currentlySelectedKeys = new Set();
|
|
1695
1856
|
|
|
1696
1857
|
const selection = $getSelection();
|
|
1697
1858
|
if (selection && $isNodeSelection(selection)) {
|
|
1698
1859
|
for (const node of selection.getNodes()) {
|
|
1699
|
-
this.
|
|
1860
|
+
this.currentlySelectedKeys.add(node.getKey());
|
|
1700
1861
|
}
|
|
1701
1862
|
}
|
|
1702
1863
|
|
|
1703
|
-
return this.
|
|
1864
|
+
return this.currentlySelectedKeys
|
|
1704
1865
|
}
|
|
1705
1866
|
|
|
1706
1867
|
#processSelectionChangeCommands() {
|
|
@@ -1830,7 +1991,7 @@ class Selection {
|
|
|
1830
1991
|
this.#highlightNewItems();
|
|
1831
1992
|
|
|
1832
1993
|
this.previouslySelectedKeys = this.#currentlySelectedKeys;
|
|
1833
|
-
this.
|
|
1994
|
+
this.currentlySelectedKeys = null;
|
|
1834
1995
|
}
|
|
1835
1996
|
|
|
1836
1997
|
#clearPreviouslyHighlightedItems() {
|
|
@@ -1852,7 +2013,7 @@ class Selection {
|
|
|
1852
2013
|
}
|
|
1853
2014
|
|
|
1854
2015
|
async #selectPreviousNode() {
|
|
1855
|
-
if (this.
|
|
2016
|
+
if (this.hasNodeSelection) {
|
|
1856
2017
|
await this.#withCurrentNode((currentNode) => currentNode.selectPrevious());
|
|
1857
2018
|
} else {
|
|
1858
2019
|
this.#selectInLexical(this.nodeBeforeCursor);
|
|
@@ -1860,7 +2021,7 @@ class Selection {
|
|
|
1860
2021
|
}
|
|
1861
2022
|
|
|
1862
2023
|
async #selectNextNode() {
|
|
1863
|
-
if (this.
|
|
2024
|
+
if (this.hasNodeSelection) {
|
|
1864
2025
|
await this.#withCurrentNode((currentNode) => currentNode.selectNext(0, 0));
|
|
1865
2026
|
} else {
|
|
1866
2027
|
this.#selectInLexical(this.nodeAfterCursor);
|
|
@@ -1868,7 +2029,7 @@ class Selection {
|
|
|
1868
2029
|
}
|
|
1869
2030
|
|
|
1870
2031
|
async #selectPreviousTopLevelNode() {
|
|
1871
|
-
if (this.
|
|
2032
|
+
if (this.hasNodeSelection) {
|
|
1872
2033
|
await this.#withCurrentNode((currentNode) => currentNode.selectPrevious());
|
|
1873
2034
|
} else {
|
|
1874
2035
|
this.#selectInLexical(this.topLevelNodeBeforeCursor);
|
|
@@ -1876,7 +2037,7 @@ class Selection {
|
|
|
1876
2037
|
}
|
|
1877
2038
|
|
|
1878
2039
|
async #selectNextTopLevelNode() {
|
|
1879
|
-
if (this.
|
|
2040
|
+
if (this.hasNodeSelection) {
|
|
1880
2041
|
await this.#withCurrentNode((currentNode) => currentNode.selectNext(0, 0));
|
|
1881
2042
|
} else {
|
|
1882
2043
|
this.#selectInLexical(this.topLevelNodeAfterCursor);
|
|
@@ -1885,10 +2046,9 @@ class Selection {
|
|
|
1885
2046
|
|
|
1886
2047
|
async #withCurrentNode(fn) {
|
|
1887
2048
|
await nextFrame();
|
|
1888
|
-
if (this.
|
|
2049
|
+
if (this.hasNodeSelection) {
|
|
1889
2050
|
this.editor.update(() => {
|
|
1890
|
-
|
|
1891
|
-
fn(this.current.getNodes()[0]);
|
|
2051
|
+
fn($getSelection().getNodes()[0]);
|
|
1892
2052
|
this.editor.focus();
|
|
1893
2053
|
});
|
|
1894
2054
|
}
|
|
@@ -2133,6 +2293,17 @@ class Selection {
|
|
|
2133
2293
|
}
|
|
2134
2294
|
}
|
|
2135
2295
|
|
|
2296
|
+
// Prevent the hardcoded background color
|
|
2297
|
+
// A background color value is set by Lexical if background is null:
|
|
2298
|
+
// https://github.com/facebook/lexical/blob/5bbbe849bd229e1db0e7b536e6a919520ada7bb2/packages/lexical-table/src/LexicalTableCellNode.ts#L187
|
|
2299
|
+
function registerHeaderBackgroundTransform(editor) {
|
|
2300
|
+
return editor.registerNodeTransform(TableCellNode, (node) => {
|
|
2301
|
+
if (node.getBackgroundColor() === null) {
|
|
2302
|
+
node.setBackgroundColor("");
|
|
2303
|
+
}
|
|
2304
|
+
})
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2136
2307
|
class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
2137
2308
|
static getType() {
|
|
2138
2309
|
return "custom_action_text_attachment"
|
|
@@ -2781,8 +2952,8 @@ class Contents {
|
|
|
2781
2952
|
let focusNode = null;
|
|
2782
2953
|
|
|
2783
2954
|
this.editor.update(() => {
|
|
2784
|
-
if (
|
|
2785
|
-
const nodesToRemove =
|
|
2955
|
+
if (this.#selection.hasNodeSelection) {
|
|
2956
|
+
const nodesToRemove = $getSelection().getNodes();
|
|
2786
2957
|
if (nodesToRemove.length === 0) return
|
|
2787
2958
|
|
|
2788
2959
|
focusNode = this.#findAdjacentNodeTo(nodesToRemove);
|
|
@@ -2794,7 +2965,6 @@ class Contents {
|
|
|
2794
2965
|
|
|
2795
2966
|
this.editor.update(() => {
|
|
2796
2967
|
this.#selectAfterDeletion(focusNode);
|
|
2797
|
-
this.#selection.clear();
|
|
2798
2968
|
this.editor.focus();
|
|
2799
2969
|
});
|
|
2800
2970
|
}
|
|
@@ -3820,6 +3990,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
3820
3990
|
registerTablePlugin(this.editor);
|
|
3821
3991
|
this.tableHandler = createElement("lexxy-table-handler");
|
|
3822
3992
|
this.append(this.tableHandler);
|
|
3993
|
+
|
|
3994
|
+
this.#addUnregisterHandler(registerHeaderBackgroundTransform(this.editor));
|
|
3823
3995
|
}
|
|
3824
3996
|
|
|
3825
3997
|
#registerCodeHiglightingComponents() {
|
|
@@ -3971,8 +4143,6 @@ class ToolbarDropdown extends HTMLElement {
|
|
|
3971
4143
|
|
|
3972
4144
|
this.container.addEventListener("toggle", this.#handleToggle.bind(this));
|
|
3973
4145
|
this.container.addEventListener("keydown", this.#handleKeyDown.bind(this));
|
|
3974
|
-
|
|
3975
|
-
this.#setTabIndexValues();
|
|
3976
4146
|
}
|
|
3977
4147
|
|
|
3978
4148
|
disconnectedCallback() {
|
|
@@ -4000,14 +4170,14 @@ class ToolbarDropdown extends HTMLElement {
|
|
|
4000
4170
|
}
|
|
4001
4171
|
}
|
|
4002
4172
|
|
|
4003
|
-
#handleOpen(
|
|
4004
|
-
this.trigger = trigger;
|
|
4173
|
+
#handleOpen() {
|
|
4005
4174
|
this.#interactiveElements[0].focus();
|
|
4006
4175
|
this.#setupClickOutsideHandler();
|
|
4176
|
+
|
|
4177
|
+
this.#resetTabIndexValues();
|
|
4007
4178
|
}
|
|
4008
4179
|
|
|
4009
4180
|
#handleClose() {
|
|
4010
|
-
this.trigger = null;
|
|
4011
4181
|
this.#removeClickOutsideHandler();
|
|
4012
4182
|
this.editor.focus();
|
|
4013
4183
|
}
|
|
@@ -4037,16 +4207,20 @@ class ToolbarDropdown extends HTMLElement {
|
|
|
4037
4207
|
}
|
|
4038
4208
|
}
|
|
4039
4209
|
|
|
4040
|
-
async #
|
|
4210
|
+
async #resetTabIndexValues() {
|
|
4041
4211
|
await nextFrame();
|
|
4042
|
-
this.#
|
|
4043
|
-
element.setAttribute("tabindex", 0);
|
|
4212
|
+
this.#buttons.forEach((element, index) => {
|
|
4213
|
+
element.setAttribute("tabindex", index === 0 ? 0 : "-1");
|
|
4044
4214
|
});
|
|
4045
4215
|
}
|
|
4046
4216
|
|
|
4047
4217
|
get #interactiveElements() {
|
|
4048
4218
|
return Array.from(this.querySelectorAll("button, input"))
|
|
4049
4219
|
}
|
|
4220
|
+
|
|
4221
|
+
get #buttons() {
|
|
4222
|
+
return Array.from(this.querySelectorAll("button"))
|
|
4223
|
+
}
|
|
4050
4224
|
}
|
|
4051
4225
|
|
|
4052
4226
|
class LinkDropdown extends ToolbarDropdown {
|
|
@@ -4242,15 +4416,19 @@ class TableHandler extends HTMLElement {
|
|
|
4242
4416
|
return $getTableColumnIndexFromTableCellNode(currentCell)
|
|
4243
4417
|
}
|
|
4244
4418
|
|
|
4419
|
+
get #tableHandlerButtons() {
|
|
4420
|
+
return Array.from(this.buttonsContainer.querySelectorAll("button, details > summary"))
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4245
4423
|
#registerKeyboardShortcuts() {
|
|
4246
|
-
this.unregisterKeyboardShortcuts = this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#handleKeyDown
|
|
4424
|
+
this.unregisterKeyboardShortcuts = this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#handleKeyDown, COMMAND_PRIORITY_HIGH);
|
|
4247
4425
|
}
|
|
4248
4426
|
|
|
4249
4427
|
#unregisterKeyboardShortcuts() {
|
|
4250
4428
|
this.unregisterKeyboardShortcuts();
|
|
4251
4429
|
}
|
|
4252
4430
|
|
|
4253
|
-
#handleKeyDown(event) {
|
|
4431
|
+
#handleKeyDown = (event) => {
|
|
4254
4432
|
if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === "F10") {
|
|
4255
4433
|
const firstButton = this.buttonsContainer?.querySelector("button, [tabindex]:not([tabindex='-1'])");
|
|
4256
4434
|
this.#setFocusStateOnSelectedCell();
|
|
@@ -4268,6 +4446,14 @@ class TableHandler extends HTMLElement {
|
|
|
4268
4446
|
}
|
|
4269
4447
|
}
|
|
4270
4448
|
|
|
4449
|
+
#handleTableHandlerKeydown = (event) => {
|
|
4450
|
+
if (event.key === "Escape") {
|
|
4451
|
+
this.#editor.focus();
|
|
4452
|
+
} else {
|
|
4453
|
+
handleRollingTabIndex(this.#tableHandlerButtons, event);
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4271
4457
|
#setUpButtons() {
|
|
4272
4458
|
this.buttonsContainer = createElement("div", {
|
|
4273
4459
|
className: "lexxy-table-handle-buttons"
|
|
@@ -4278,6 +4464,7 @@ class TableHandler extends HTMLElement {
|
|
|
4278
4464
|
|
|
4279
4465
|
this.moreMenu = this.#createMoreMenu();
|
|
4280
4466
|
this.buttonsContainer.appendChild(this.moreMenu);
|
|
4467
|
+
this.buttonsContainer.addEventListener("keydown", this.#handleTableHandlerKeydown);
|
|
4281
4468
|
|
|
4282
4469
|
this.#editorElement.appendChild(this.buttonsContainer);
|
|
4283
4470
|
}
|
|
@@ -4373,6 +4560,7 @@ class TableHandler extends HTMLElement {
|
|
|
4373
4560
|
const container = createElement("details", {
|
|
4374
4561
|
className: "lexxy-table-control lexxy-table-control__more-menu"
|
|
4375
4562
|
});
|
|
4563
|
+
container.setAttribute("name", "lexxy-dropdown");
|
|
4376
4564
|
|
|
4377
4565
|
container.tabIndex = -1;
|
|
4378
4566
|
|
|
@@ -54,15 +54,17 @@
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
table {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
th, td {
|
|
58
|
+
&.table-cell--selected {
|
|
59
|
+
background-color: var(--lexxy-color-table-cell-selected-bg);
|
|
60
|
+
}
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
&.lexxy-content__table-cell--selected {
|
|
63
|
+
background-color: var(--lexxy-color-table-cell-selected-bg);
|
|
64
|
+
border-color: var(--lexxy-color-table-cell-selected-border);
|
|
65
|
+
}
|
|
64
66
|
}
|
|
65
|
-
|
|
67
|
+
|
|
66
68
|
&.lexxy-content__table--selection {
|
|
67
69
|
::selection {
|
|
68
70
|
background: transparent;
|
|
@@ -376,85 +378,12 @@
|
|
|
376
378
|
}
|
|
377
379
|
}
|
|
378
380
|
|
|
379
|
-
/* Table dropdown
|
|
380
|
-
/* -------------------------------------------------------------------------- */
|
|
381
|
-
|
|
382
|
-
:where(lexxy-table-dropdown) {
|
|
383
|
-
display: flex;
|
|
384
|
-
flex-direction: column;
|
|
385
|
-
gap: 1ch;
|
|
386
|
-
|
|
387
|
-
.lexxy-editor__table-create {
|
|
388
|
-
display: flex;
|
|
389
|
-
flex-direction: column;
|
|
390
|
-
flex-wrap: wrap;
|
|
391
|
-
gap: 0;
|
|
392
|
-
|
|
393
|
-
.lexxy-editor__table-buttons {
|
|
394
|
-
background-color: var(--lexxy-color-ink-lighter);
|
|
395
|
-
display: flex;
|
|
396
|
-
flex-direction: column;
|
|
397
|
-
gap: 1px;
|
|
398
|
-
padding: 1px;
|
|
399
|
-
|
|
400
|
-
div {
|
|
401
|
-
display: flex;
|
|
402
|
-
flex-direction: row;
|
|
403
|
-
gap: 1px;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
button {
|
|
407
|
-
aspect-ratio: 1.5 / 1;
|
|
408
|
-
border: 0;
|
|
409
|
-
border-radius: 0;
|
|
410
|
-
color: var(--lexxy-color-ink);
|
|
411
|
-
font-family: var(--lexxy-font-base);
|
|
412
|
-
font-size: var(--lexxy-text-small);
|
|
413
|
-
font-weight: normal;
|
|
414
|
-
inline-size: 4ch;
|
|
415
|
-
margin: 0;
|
|
416
|
-
|
|
417
|
-
&.active {
|
|
418
|
-
background-color: var(--lexxy-color-ink-lightest);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
label {
|
|
424
|
-
align-items: center;
|
|
425
|
-
display: flex;
|
|
426
|
-
gap: 0.5ch;
|
|
427
|
-
padding: 0.5ch 0;
|
|
428
|
-
margin-block-start: 1ch;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
&:has(input[type="checkbox"]:checked) {
|
|
432
|
-
.lexxy-editor__table-buttons {
|
|
433
|
-
div:first-child button,
|
|
434
|
-
button:first-child {
|
|
435
|
-
filter: brightness(0.95);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
.lexxy-editor__table-edit {
|
|
442
|
-
display: flex;
|
|
443
|
-
flex-direction: column;
|
|
444
|
-
flex-wrap: wrap;
|
|
445
|
-
gap: 0;
|
|
446
|
-
|
|
447
|
-
button {
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
381
|
/* Table handle buttons
|
|
454
382
|
/* -------------------------------------------------------------------------- */
|
|
455
383
|
|
|
456
384
|
:where(.lexxy-table-handle-buttons) {
|
|
457
385
|
--button-size: 2.5lh;
|
|
386
|
+
|
|
458
387
|
color: var(--lexxy-color-ink-inverted);
|
|
459
388
|
display: none;
|
|
460
389
|
flex-direction: row;
|
|
@@ -463,7 +392,7 @@
|
|
|
463
392
|
line-height: 1;
|
|
464
393
|
position: absolute;
|
|
465
394
|
transform: translate(-50%, -120%);
|
|
466
|
-
z-index:
|
|
395
|
+
z-index: 2;
|
|
467
396
|
|
|
468
397
|
.lexxy-table-control {
|
|
469
398
|
align-items: center;
|
|
@@ -475,7 +404,8 @@
|
|
|
475
404
|
padding: 2px;
|
|
476
405
|
white-space: nowrap;
|
|
477
406
|
|
|
478
|
-
button
|
|
407
|
+
button,
|
|
408
|
+
summary {
|
|
479
409
|
aspect-ratio: 1 / 1;
|
|
480
410
|
align-items: center;
|
|
481
411
|
background-color: transparent;
|
|
@@ -487,13 +417,22 @@
|
|
|
487
417
|
font-weight: bold;
|
|
488
418
|
justify-content: center;
|
|
489
419
|
line-height: 1;
|
|
420
|
+
list-style: none;
|
|
490
421
|
min-block-size: var(--button-size);
|
|
491
422
|
min-inline-size: var(--button-size);
|
|
492
423
|
padding: 0;
|
|
424
|
+
user-select: none;
|
|
425
|
+
-webkit-user-select: none;
|
|
426
|
+
|
|
427
|
+
&:hover {
|
|
428
|
+
background-color: var(--lexxy-color-ink-medium);
|
|
429
|
+
}
|
|
493
430
|
|
|
494
|
-
&:
|
|
431
|
+
&:focus,
|
|
495
432
|
&:focus-visible {
|
|
496
433
|
background-color: var(--lexxy-color-ink-medium);
|
|
434
|
+
outline: var(--lexxy-focus-ring-size) solid var(--lexxy-focus-ring-color);
|
|
435
|
+
outline-offset: var(--lexxy-focus-ring-offset);
|
|
497
436
|
}
|
|
498
437
|
|
|
499
438
|
svg {
|
|
@@ -510,33 +449,13 @@
|
|
|
510
449
|
|
|
511
450
|
.lexxy-table-control__more-menu {
|
|
512
451
|
gap: 0;
|
|
513
|
-
padding:
|
|
452
|
+
padding: 0.25ch;
|
|
514
453
|
position: relative;
|
|
515
454
|
|
|
516
455
|
summary {
|
|
517
|
-
aspect-ratio: 1 / 1;
|
|
518
|
-
align-items: center;
|
|
519
|
-
background: transparent;
|
|
520
|
-
border-radius: var(--lexxy-radius);
|
|
521
|
-
border: 0;
|
|
522
|
-
box-sizing: border-box;
|
|
523
|
-
display: flex;
|
|
524
|
-
font-size: inherit;
|
|
525
|
-
justify-content: center;
|
|
526
|
-
list-style: none;
|
|
527
|
-
min-block-size: var(--button-size);
|
|
528
|
-
min-inline-size: var(--button-size);
|
|
529
|
-
padding: 0;
|
|
530
|
-
user-select: none;
|
|
531
|
-
-webkit-user-select: none;
|
|
532
|
-
|
|
533
456
|
&::-webkit-details-marker {
|
|
534
457
|
display: none;
|
|
535
458
|
}
|
|
536
|
-
|
|
537
|
-
&:hover {
|
|
538
|
-
background: var(--lexxy-color-ink-medium);
|
|
539
|
-
}
|
|
540
459
|
}
|
|
541
460
|
|
|
542
461
|
.lexxy-table-control__more-menu-details {
|
|
@@ -553,7 +472,7 @@
|
|
|
553
472
|
border-radius: 0.75ch;
|
|
554
473
|
display: flex;
|
|
555
474
|
flex-direction: column;
|
|
556
|
-
padding:
|
|
475
|
+
padding: 0.25ch;
|
|
557
476
|
}
|
|
558
477
|
|
|
559
478
|
button {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@37signals/lexxy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27-beta",
|
|
4
4
|
"description": "Lexxy - A modern rich text editor for Rails.",
|
|
5
5
|
"module": "dist/lexxy.esm.js",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "rollup -c",
|
|
27
27
|
"build:npm": "rollup -c rollup.config.npm.mjs",
|
|
28
|
-
"watch": "
|
|
28
|
+
"watch": "rollup -wc --watch.onEnd=\"rails restart\"",
|
|
29
29
|
"lint": "eslint",
|
|
30
30
|
"prerelease": "yarn build:npm",
|
|
31
31
|
"release": "yarn build:npm && yarn publish"
|