@37signals/lexxy 0.9.21 → 0.9.23
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 +2 -19
- package/dist/lexxy.esm.js +79 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A modern rich text editor for Rails.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[Documentation](https://basecamp.github.io/lexxy/docs/) | <mark>[Try it out!](https://basecamp.github.io/lexxy/sandbox/)</mark>
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
@@ -15,24 +15,7 @@ A modern rich text editor for Rails.
|
|
|
15
15
|
- Preview attachments like PDFs and Videos in the editor.
|
|
16
16
|
- Works seamlessly with Action Text, generating the same canonical HTML format it expects for attachments.
|
|
17
17
|
|
|
18
|
-

|
|
19
|
-
|
|
20
|
-
## Documentation
|
|
21
|
-
|
|
22
|
-
Visit the **[documentation site](https://basecamp.github.io/lexxy)**.
|
|
23
|
-
|
|
24
|
-
## Roadmap
|
|
25
|
-
|
|
26
|
-
This is a beta. Here's what's coming next:
|
|
27
|
-
|
|
28
|
-
- [x] Configurable editors in Action Text: Choose your editor like you choose your database.
|
|
29
|
-
- [x] More editing features:
|
|
30
|
-
- [x] Tables
|
|
31
|
-
- [x] Text highlighting
|
|
32
|
-
- [x] Configuration hooks.
|
|
33
|
-
- [x] Standalone JS package: to use in non-Rails environments.
|
|
34
|
-
- [x] Image galleries: The only remaining feature for full Action Text compatibility
|
|
35
|
-
- [ ] Install task that generates the necessary JS and adds stylesheets.
|
|
18
|
+

|
|
36
19
|
|
|
37
20
|
## Contributing
|
|
38
21
|
|
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { highlightCode, highlightElement } from './lexxy_helpers.esm.js';
|
|
2
2
|
import DOMPurify from 'dompurify';
|
|
3
3
|
import { getStyleObjectFromCSS, getCSSFromStyleObject, $getSelectionStyleValueForProperty, $ensureForwardRangeSelection, $isAtNodeEnd, $patchStyleText, $setBlocksType, $forEachSelectedTextNode } from '@lexical/selection';
|
|
4
|
-
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createTextNode, $getRoot, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isTextNode, $getSiblingCaret, $rewindSiblingCaret, $splitAtPointCaretNext, $normalizeSelection__EXPERIMENTAL, $isChildCaret, $isTextPointCaret, $isExtendableTextPointCaret, $isSiblingCaret, $getCommonAncestor, $findMatchingParent, TextNode, createCommand,
|
|
4
|
+
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createTextNode, defineExtension, $getRoot, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isTextNode, $getSiblingCaret, $rewindSiblingCaret, $splitAtPointCaretNext, $normalizeSelection__EXPERIMENTAL, $isChildCaret, $isTextPointCaret, $isExtendableTextPointCaret, $isSiblingCaret, $getCommonAncestor, $findMatchingParent, TextNode, createCommand, COMMAND_PRIORITY_EDITOR, $getEditor, $getNodeByKey, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $cloneWithProperties, $getNearestRootOrShadowRoot, $createRangeSelection, $setSelection, createState, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, INSERT_LINE_BREAK_COMMAND, COMMAND_PRIORITY_HIGH, INSERT_PARAGRAPH_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $addUpdateTag, ElementNode, $splitNode, $getChildCaretAtIndex, $createLineBreakNode, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, PASTE_COMMAND, $onUpdate, ParagraphNode, RootNode, DRAGSTART_COMMAND, DROP_COMMAND, mergeRegister as mergeRegister$1, $createRangeSelectionFromDom, CLEAR_HISTORY_COMMAND, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, INPUT_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
|
|
5
|
+
import * as lexical from 'lexical';
|
|
6
|
+
export { lexical as Lexical };
|
|
5
7
|
import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
|
|
6
8
|
import { buildEditorFromExtensions } from '@lexical/extension';
|
|
7
9
|
import { ListNode, ListItemNode, $getListDepth, $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $createListNode, $createListItemNode, registerList } from '@lexical/list';
|
|
@@ -10,7 +12,7 @@ import { registerPlainText } from '@lexical/plain-text';
|
|
|
10
12
|
import { RichTextExtension, $isQuoteNode, QuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
11
13
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
12
14
|
import { HistoryExtension } from '@lexical/history';
|
|
13
|
-
import { $isCodeNode, CodeHighlightNode, CodeNode, $createCodeNode, $isCodeHighlightNode, $createCodeHighlightNode,
|
|
15
|
+
import { $isCodeNode, CodeHighlightNode, CodeNode, $createCodeNode, $isCodeHighlightNode, $createCodeHighlightNode, registerCodeHighlighting, normalizeCodeLang, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
|
|
14
16
|
import { TRANSFORMERS, registerMarkdownShortcuts } from '@lexical/markdown';
|
|
15
17
|
import { INSERT_TABLE_COMMAND, $getTableCellNodeFromLexicalNode, TableCellNode, TableNode, TableRowNode, setScrollableTablesActive, registerTablePlugin, registerTableSelectionObserver, TableCellHeaderStates, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $findCellNode, $getElementForTableNode } from '@lexical/table';
|
|
16
18
|
import { marked } from 'marked';
|
|
@@ -1509,8 +1511,11 @@ class LexxyExtension {
|
|
|
1509
1511
|
return []
|
|
1510
1512
|
}
|
|
1511
1513
|
|
|
1512
|
-
|
|
1514
|
+
defineExtension(...args) {
|
|
1515
|
+
return defineExtension(...args)
|
|
1516
|
+
}
|
|
1513
1517
|
|
|
1518
|
+
initializeToolbar(_lexxyToolbar) {
|
|
1514
1519
|
}
|
|
1515
1520
|
|
|
1516
1521
|
setEditorValidity(flags, message) {
|
|
@@ -5994,16 +5999,34 @@ class Contents {
|
|
|
5994
5999
|
return result
|
|
5995
6000
|
}
|
|
5996
6001
|
|
|
5997
|
-
// The query runs from the trigger up to the next
|
|
6002
|
+
// The query runs from the trigger up to the next boundary, even when the
|
|
5998
6003
|
// cursor sits inside an existing word — inserting "@" before "Jack" must
|
|
5999
6004
|
// filter by "Jack" rather than treating the prompt as empty.
|
|
6000
6005
|
#endOffsetAt(fullText, cursorOffset) {
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
return cursorOffset + whitespaceOffset
|
|
6006
|
+
for (let offset = cursorOffset; offset < fullText.length; offset++) {
|
|
6007
|
+
if (this.#isQueryBoundary(fullText, offset)) {
|
|
6008
|
+
return offset
|
|
6009
|
+
}
|
|
6006
6010
|
}
|
|
6011
|
+
|
|
6012
|
+
return fullText.length
|
|
6013
|
+
}
|
|
6014
|
+
|
|
6015
|
+
// Whitespace always ends the query. Punctuation ends it too: with "@" typed
|
|
6016
|
+
// right before a period, extending across it would glue "." onto everything
|
|
6017
|
+
// typed ("B" would query "B."), matching the wrong names or nothing at all.
|
|
6018
|
+
// But punctuation joining two word characters is part of a name — "@" before
|
|
6019
|
+
// "Anne-Marie" or "O'Connor" must keep the whole name as the query.
|
|
6020
|
+
#isQueryBoundary(fullText, offset) {
|
|
6021
|
+
const character = fullText[offset];
|
|
6022
|
+
if (/\s/.test(character)) return true
|
|
6023
|
+
if (!/\p{P}/u.test(character)) return false
|
|
6024
|
+
|
|
6025
|
+
return !(this.#isWordCharacter(fullText[offset - 1]) && this.#isWordCharacter(fullText[offset + 1]))
|
|
6026
|
+
}
|
|
6027
|
+
|
|
6028
|
+
#isWordCharacter(character) {
|
|
6029
|
+
return character != null && /[\p{L}\p{N}]/u.test(character)
|
|
6007
6030
|
}
|
|
6008
6031
|
|
|
6009
6032
|
containsTextBackUntil(string) {
|
|
@@ -7071,6 +7094,26 @@ function $getAllProvisionalParagraphs(rootNode = $getRoot()) {
|
|
|
7071
7094
|
return $descendantsMatching(rootNode.getChildren(), $isProvisionalParagraphNode)
|
|
7072
7095
|
}
|
|
7073
7096
|
|
|
7097
|
+
// Registers code highlighting through the extension system so its node
|
|
7098
|
+
// transforms exist before the initial editor state is applied. Registering
|
|
7099
|
+
// after editor creation misses code blocks loaded from the initial value:
|
|
7100
|
+
// their transform pass has already run, and Lexical's catch-up dirtying
|
|
7101
|
+
// only sees the committed (still empty) state.
|
|
7102
|
+
class CodeHighlightingExtension extends LexxyExtension {
|
|
7103
|
+
get enabled() {
|
|
7104
|
+
return this.editorElement.supportsRichText
|
|
7105
|
+
}
|
|
7106
|
+
|
|
7107
|
+
get lexicalExtension() {
|
|
7108
|
+
return defineExtension({
|
|
7109
|
+
name: "lexxy/code-highlighting",
|
|
7110
|
+
register(editor) {
|
|
7111
|
+
return registerCodeHighlighting(editor)
|
|
7112
|
+
}
|
|
7113
|
+
})
|
|
7114
|
+
}
|
|
7115
|
+
}
|
|
7116
|
+
|
|
7074
7117
|
const TRIX_LANGUAGE_ATTR = "language";
|
|
7075
7118
|
|
|
7076
7119
|
class TrixContentExtension extends LexxyExtension {
|
|
@@ -7202,9 +7245,13 @@ class TablesExtension extends LexxyExtension {
|
|
|
7202
7245
|
}
|
|
7203
7246
|
}),
|
|
7204
7247
|
|
|
7205
|
-
//
|
|
7248
|
+
// A table in Lexxy is a Lexxy table: cell shading can't be set in the
|
|
7249
|
+
// editor, so any cell background only ever comes from foreign content
|
|
7250
|
+
// (pasted spreadsheets, loaded documents). Normalize every cell to no
|
|
7251
|
+
// background so it adopts the current theme. This also clears
|
|
7252
|
+
// Lexical's hardcoded default header background (Lexical #8089).
|
|
7206
7253
|
editor.registerNodeTransform(TableCellNode, (node) => {
|
|
7207
|
-
if (node.getBackgroundColor()
|
|
7254
|
+
if (node.getBackgroundColor() !== "") {
|
|
7208
7255
|
node.setBackgroundColor("");
|
|
7209
7256
|
}
|
|
7210
7257
|
}),
|
|
@@ -8591,6 +8638,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8591
8638
|
get baseExtensions() {
|
|
8592
8639
|
return [
|
|
8593
8640
|
ProvisionalParagraphExtension,
|
|
8641
|
+
CodeHighlightingExtension,
|
|
8594
8642
|
HighlightExtension,
|
|
8595
8643
|
TrixContentExtension,
|
|
8596
8644
|
TablesExtension,
|
|
@@ -8998,7 +9046,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8998
9046
|
registerList(this.editor)
|
|
8999
9047
|
);
|
|
9000
9048
|
this.#registerTableComponents();
|
|
9001
|
-
this.#
|
|
9049
|
+
this.#registerCodeLanguagePicker();
|
|
9002
9050
|
if (this.supportsMarkdown) {
|
|
9003
9051
|
const transformers = [ ...TRANSFORMERS, HORIZONTAL_DIVIDER ];
|
|
9004
9052
|
registered.push(
|
|
@@ -9020,8 +9068,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
9020
9068
|
this.#disposables.push(tableTools);
|
|
9021
9069
|
}
|
|
9022
9070
|
|
|
9023
|
-
#
|
|
9024
|
-
registerCodeHighlighting(this.editor);
|
|
9071
|
+
#registerCodeLanguagePicker() {
|
|
9025
9072
|
let codeLanguagePicker = this.querySelector("lexxy-code-language-picker");
|
|
9026
9073
|
codeLanguagePicker ??= createElement("lexxy-code-language-picker");
|
|
9027
9074
|
this.append(codeLanguagePicker);
|
|
@@ -10652,6 +10699,7 @@ class TableTools extends HTMLElement {
|
|
|
10652
10699
|
connectedCallback() {
|
|
10653
10700
|
this.tableController = new TableController(this.#editorElement);
|
|
10654
10701
|
this.classList.add("lexxy-floating-controls");
|
|
10702
|
+
this.role = "toolbar";
|
|
10655
10703
|
|
|
10656
10704
|
this.#setUpButtons();
|
|
10657
10705
|
this.#hide();
|
|
@@ -10782,16 +10830,28 @@ class TableTools extends HTMLElement {
|
|
|
10782
10830
|
}
|
|
10783
10831
|
|
|
10784
10832
|
#registerKeyboardShortcuts() {
|
|
10785
|
-
this.#listeners.track(this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#
|
|
10833
|
+
this.#listeners.track(this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#focusToolbarOnShortcut, COMMAND_PRIORITY_HIGH));
|
|
10786
10834
|
}
|
|
10787
10835
|
|
|
10788
|
-
#
|
|
10789
|
-
if (
|
|
10790
|
-
|
|
10791
|
-
|
|
10836
|
+
#focusToolbarOnShortcut = (event) => {
|
|
10837
|
+
if (this.#hasSelectedTable && this.#isFocusToolbarShortcut(event)) {
|
|
10838
|
+
event.preventDefault();
|
|
10839
|
+
this.#tableToolsButtons[0]?.focus();
|
|
10840
|
+
return true
|
|
10841
|
+
} else {
|
|
10842
|
+
return false
|
|
10792
10843
|
}
|
|
10793
10844
|
}
|
|
10794
10845
|
|
|
10846
|
+
get #hasSelectedTable() {
|
|
10847
|
+
return this.tableController?.currentTableNodeKey != null
|
|
10848
|
+
}
|
|
10849
|
+
|
|
10850
|
+
#isFocusToolbarShortcut(event) {
|
|
10851
|
+
if (event.key !== "F10") return false
|
|
10852
|
+
return event.altKey || ((event.ctrlKey || event.metaKey) && event.shiftKey)
|
|
10853
|
+
}
|
|
10854
|
+
|
|
10795
10855
|
#handleToolsKeydown = (event) => {
|
|
10796
10856
|
if (event.key === "Escape") {
|
|
10797
10857
|
this.#handleEscapeKey();
|