@37signals/lexxy 0.9.20 → 0.9.22

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A modern rich text editor for Rails.
4
4
 
5
- **[Try it out!](https://basecamp.github.io/lexxy/try-it)**
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
- ![Lexxy editor screenshot](docs/images/home.screenshot.png)
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
+ ![Lexxy editor screenshot](home/docs/images/home.screenshot.png)
36
19
 
37
20
  ## Contributing
38
21
 
package/dist/lexxy.esm.js CHANGED
@@ -10,7 +10,7 @@ import { registerPlainText } from '@lexical/plain-text';
10
10
  import { RichTextExtension, $isQuoteNode, QuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
11
11
  import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
12
12
  import { HistoryExtension } from '@lexical/history';
13
- import { $isCodeNode, CodeHighlightNode, CodeNode, $createCodeNode, $isCodeHighlightNode, $createCodeHighlightNode, normalizeCodeLang, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
13
+ import { $isCodeNode, CodeHighlightNode, CodeNode, $createCodeNode, $isCodeHighlightNode, $createCodeHighlightNode, registerCodeHighlighting, normalizeCodeLang, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
14
14
  import { TRANSFORMERS, registerMarkdownShortcuts } from '@lexical/markdown';
15
15
  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
16
  import { marked } from 'marked';
@@ -6486,7 +6486,7 @@ class Clipboard {
6486
6486
  }
6487
6487
 
6488
6488
  if (this.#isPlainTextOrURLPasted(clipboardData)) {
6489
- this.#pastePlainText(clipboardData);
6489
+ this.#pastePlainTextOrURL(clipboardData);
6490
6490
  event.preventDefault();
6491
6491
  return true
6492
6492
  }
@@ -6524,14 +6524,34 @@ class Clipboard {
6524
6524
  return types.length === 1 && types[0] === "text/plain"
6525
6525
  }
6526
6526
 
6527
+ // Browsers expose a copied URL in several shapes:
6528
+ // Safari [ text/plain, text/uri-list ]
6529
+ // App ShareSheet [ text/uri-list ]
6530
+ // Chromium macOS [ text/plain, text/html, (?:text/link-preview) ]
6527
6531
  #isOnlyURLPasted(clipboardData) {
6528
- // Safari URLs are copied as a text/plain + text/uri-list object
6529
- // App ShareSheet URLs are copied as solo text/uri-list object
6532
+ if (this.#isLexicalClipboardData(clipboardData)) return false
6533
+
6530
6534
  const types = Array.from(clipboardData.types);
6531
- return types.length
6532
- && types.length <= 2
6533
- && types.includes("text/uri-list")
6534
- && (types.length < 2 || types.includes("text/plain"))
6535
+ if (types.includes("text/uri-list")) {
6536
+ return types.every(type => type === "text/plain" || type === "text/uri-list")
6537
+ }
6538
+
6539
+ if (clipboardData.files.length) return false
6540
+
6541
+ const text = clipboardData.getData("text/plain").trim();
6542
+ if (!isAutolinkableURL(text)) return false
6543
+
6544
+ const html = clipboardData.getData("text/html");
6545
+ return !html || this.#htmlIsBareLinkToURL(html, text)
6546
+ }
6547
+
6548
+ #htmlIsBareLinkToURL(html, url) {
6549
+ const doc = parseHtml(html);
6550
+ if (doc.body.textContent.trim() !== url) return false
6551
+
6552
+ const links = doc.body.querySelectorAll("a");
6553
+ if (links.length === 0) return true
6554
+ return links.length === 1 && links[0].getAttribute("href") === url
6535
6555
  }
6536
6556
 
6537
6557
  #isPastingIntoCodeBlock() {
@@ -6565,14 +6585,12 @@ class Clipboard {
6565
6585
  }, { tag: PASTE_TAG });
6566
6586
  }
6567
6587
 
6568
- #pastePlainText(clipboardData) {
6569
- const item = clipboardData.items[0];
6588
+ #pastePlainTextOrURL(clipboardData) {
6589
+ const item = this.#plainTextOrURLItem(clipboardData);
6570
6590
  item.getAsString((text) => {
6571
- if (isAutolinkableURL(text) && this.contents.hasSelectedText()) {
6572
- this.contents.createLinkWithSelectedText(text);
6573
- } else if (isAutolinkableURL(text)) {
6574
- const nodeKey = this.contents.createLink(text);
6575
- this.#dispatchLinkInsertEvent(nodeKey, { url: text });
6591
+ const url = text.trim();
6592
+ if (isAutolinkableURL(url)) {
6593
+ this.#pasteURL(url);
6576
6594
  } else if (this.editorElement.supportsMarkdown) {
6577
6595
  this.#pasteMarkdown(text);
6578
6596
  } else {
@@ -6581,6 +6599,20 @@ class Clipboard {
6581
6599
  });
6582
6600
  }
6583
6601
 
6602
+ #plainTextOrURLItem(clipboardData) {
6603
+ const items = Array.from(clipboardData.items);
6604
+ return items.find((item) => item.type === "text/plain") || items.find((item) => item.type === "text/uri-list")
6605
+ }
6606
+
6607
+ #pasteURL(url) {
6608
+ if (this.contents.hasSelectedText()) {
6609
+ this.contents.createLinkWithSelectedText(url);
6610
+ } else {
6611
+ const nodeKey = this.contents.createLink(url);
6612
+ this.#dispatchLinkInsertEvent(nodeKey, { url });
6613
+ }
6614
+ }
6615
+
6584
6616
  #insertSingleLinkAt(selection, url) {
6585
6617
  if (!$isRangeSelection(selection)) return
6586
6618
 
@@ -7039,6 +7071,26 @@ function $getAllProvisionalParagraphs(rootNode = $getRoot()) {
7039
7071
  return $descendantsMatching(rootNode.getChildren(), $isProvisionalParagraphNode)
7040
7072
  }
7041
7073
 
7074
+ // Registers code highlighting through the extension system so its node
7075
+ // transforms exist before the initial editor state is applied. Registering
7076
+ // after editor creation misses code blocks loaded from the initial value:
7077
+ // their transform pass has already run, and Lexical's catch-up dirtying
7078
+ // only sees the committed (still empty) state.
7079
+ class CodeHighlightingExtension extends LexxyExtension {
7080
+ get enabled() {
7081
+ return this.editorElement.supportsRichText
7082
+ }
7083
+
7084
+ get lexicalExtension() {
7085
+ return defineExtension({
7086
+ name: "lexxy/code-highlighting",
7087
+ register(editor) {
7088
+ return registerCodeHighlighting(editor)
7089
+ }
7090
+ })
7091
+ }
7092
+ }
7093
+
7042
7094
  const TRIX_LANGUAGE_ATTR = "language";
7043
7095
 
7044
7096
  class TrixContentExtension extends LexxyExtension {
@@ -8559,6 +8611,7 @@ class LexicalEditorElement extends HTMLElement {
8559
8611
  get baseExtensions() {
8560
8612
  return [
8561
8613
  ProvisionalParagraphExtension,
8614
+ CodeHighlightingExtension,
8562
8615
  HighlightExtension,
8563
8616
  TrixContentExtension,
8564
8617
  TablesExtension,
@@ -8966,7 +9019,7 @@ class LexicalEditorElement extends HTMLElement {
8966
9019
  registerList(this.editor)
8967
9020
  );
8968
9021
  this.#registerTableComponents();
8969
- this.#registerCodeHiglightingComponents();
9022
+ this.#registerCodeLanguagePicker();
8970
9023
  if (this.supportsMarkdown) {
8971
9024
  const transformers = [ ...TRANSFORMERS, HORIZONTAL_DIVIDER ];
8972
9025
  registered.push(
@@ -8988,8 +9041,7 @@ class LexicalEditorElement extends HTMLElement {
8988
9041
  this.#disposables.push(tableTools);
8989
9042
  }
8990
9043
 
8991
- #registerCodeHiglightingComponents() {
8992
- registerCodeHighlighting(this.editor);
9044
+ #registerCodeLanguagePicker() {
8993
9045
  let codeLanguagePicker = this.querySelector("lexxy-code-language-picker");
8994
9046
  codeLanguagePicker ??= createElement("lexxy-code-language-picker");
8995
9047
  this.append(codeLanguagePicker);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.9.20",
3
+ "version": "0.9.22",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",