@37signals/lexxy 0.9.6-beta.bc0 → 0.9.8-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 CHANGED
@@ -3,7 +3,7 @@
3
3
  A modern rich text editor for Rails.
4
4
 
5
5
  > [!IMPORTANT]
6
- > This is an early beta. It hasn't been battle-tested yet. Please try it out and report any issues you find.
6
+ > This is a beta. It hasn't been battle-tested yet. Please try it out and report any issues you find.
7
7
 
8
8
  **[Try it out!](https://basecamp.github.io/lexxy/try-it)**
9
9
 
@@ -26,7 +26,7 @@ Visit the **[documentation site](https://basecamp.github.io/lexxy)**.
26
26
 
27
27
  ## Roadmap
28
28
 
29
- This is an early beta. Here's what's coming next:
29
+ This is a beta. Here's what's coming next:
30
30
 
31
31
  - [x] Configurable editors in Action Text: Choose your editor like you choose your database.
32
32
  - [x] More editing features:
package/dist/lexxy.esm.js CHANGED
@@ -2186,7 +2186,7 @@ class CommandDispatcher {
2186
2186
 
2187
2187
  dispatchInsertUnorderedList() {
2188
2188
  const selection = $getSelection();
2189
- if (!selection) return
2189
+ if (!$isRangeSelection(selection)) return
2190
2190
 
2191
2191
  const anchorNode = selection.anchor.getNode();
2192
2192
 
@@ -2199,7 +2199,7 @@ class CommandDispatcher {
2199
2199
 
2200
2200
  dispatchInsertOrderedList() {
2201
2201
  const selection = $getSelection();
2202
- if (!selection) return
2202
+ if (!$isRangeSelection(selection)) return
2203
2203
 
2204
2204
  const anchorNode = selection.anchor.getNode();
2205
2205
 
@@ -2491,6 +2491,15 @@ function capitalize(str) {
2491
2491
  return str.charAt(0).toUpperCase() + str.slice(1)
2492
2492
  }
2493
2493
 
2494
+ function debounce(fn, wait) {
2495
+ let timeout;
2496
+
2497
+ return (...args) => {
2498
+ clearTimeout(timeout);
2499
+ timeout = setTimeout(() => fn(...args), wait);
2500
+ }
2501
+ }
2502
+
2494
2503
  function debounceAsync(fn, wait) {
2495
2504
  let timeout;
2496
2505
 
@@ -2533,14 +2542,24 @@ function normalizeFilteredText(string) {
2533
2542
  .normalize("NFD").replace(/[\u0300-\u036f]/g, "") // Remove diacritics
2534
2543
  }
2535
2544
 
2536
- function filterMatches(text, potentialMatch) {
2537
- return normalizeFilteredText(text).includes(normalizeFilteredText(potentialMatch))
2545
+ function filterMatchPosition(text, potentialMatch) {
2546
+ const normalizedText = normalizeFilteredText(text);
2547
+ const normalizedMatch = normalizeFilteredText(potentialMatch);
2548
+
2549
+ if (!normalizedMatch) return 0
2550
+
2551
+ const match = normalizedText.match(new RegExp(`(?:^|\\b)${escapeForRegExp(normalizedMatch)}`));
2552
+ return match ? match.index : -1
2538
2553
  }
2539
2554
 
2540
2555
  function upcaseFirst(string) {
2541
2556
  return string.charAt(0).toUpperCase() + string.slice(1)
2542
2557
  }
2543
2558
 
2559
+ function escapeForRegExp(string) {
2560
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
2561
+ }
2562
+
2544
2563
  // Parses a value that may arrive as a boolean or as a string (e.g. from DOM
2545
2564
  // getAttribute) into a proper boolean. Ensures "false" doesn't evaluate as truthy.
2546
2565
  function parseBoolean(value) {
@@ -5914,9 +5933,9 @@ class AttachmentDragAndDrop {
5914
5933
  // -- Event handlers --------------------------------------------------------
5915
5934
 
5916
5935
  #handleDragStart(event) {
5917
- if (event.target.closest("textarea")) return false
5936
+ if (event.target.closest?.("textarea")) return false
5918
5937
 
5919
- const figure = event.target.closest("figure.attachment[data-lexical-node-key]");
5938
+ const figure = event.target.closest?.("figure.attachment[data-lexical-node-key]");
5920
5939
  if (!figure) return false
5921
5940
 
5922
5941
  this.#draggedNodeKey = figure.dataset.lexicalNodeKey;
@@ -7361,7 +7380,7 @@ class LinkDropdown extends ToolbarDropdown {
7361
7380
  get #selectedLinkUrl() {
7362
7381
  return this.editor.getEditorState().read(() => {
7363
7382
  const linkNode = this.editorElement.selection.nearestNodeOfType(LinkNode);
7364
- return linkNode?.getUrl() ?? null
7383
+ return linkNode?.getURL() ?? ""
7365
7384
  })
7366
7385
  }
7367
7386
  }
@@ -7507,6 +7526,8 @@ class BaseSource {
7507
7526
  }
7508
7527
  }
7509
7528
 
7529
+ const MAX_RENDERED_SUGGESTIONS$1 = 100;
7530
+
7510
7531
  class LocalFilterSource extends BaseSource {
7511
7532
  async buildListItems(filter = "") {
7512
7533
  const promptItems = await this.fetchPromptItems();
@@ -7523,18 +7544,41 @@ class LocalFilterSource extends BaseSource {
7523
7544
  }
7524
7545
 
7525
7546
  #buildListItemsFromPromptItems(promptItems, filter) {
7526
- const listItems = [];
7527
7547
  this.promptItemByListItem = new WeakMap();
7528
- promptItems.forEach((promptItem) => {
7529
- const searchableText = promptItem.getAttribute("search");
7530
7548
 
7531
- if (!filter || filterMatches(searchableText, filter)) {
7532
- const listItem = this.buildListItemElementFor(promptItem);
7533
- this.promptItemByListItem.set(listItem, promptItem);
7534
- listItems.push(listItem);
7549
+ if (!filter) {
7550
+ return this.#buildAllListItems(promptItems)
7551
+ }
7552
+
7553
+ const matches = [];
7554
+ for (const promptItem of promptItems) {
7555
+ const searchableText = promptItem.getAttribute("search");
7556
+ const position = filterMatchPosition(searchableText, filter);
7557
+ if (position >= 0) {
7558
+ matches.push({ promptItem, position });
7535
7559
  }
7536
- });
7560
+ }
7537
7561
 
7562
+ matches.sort((a, b) => a.position - b.position);
7563
+
7564
+ const listItems = [];
7565
+ for (const { promptItem } of matches) {
7566
+ if (listItems.length >= MAX_RENDERED_SUGGESTIONS$1) break
7567
+ const listItem = this.buildListItemElementFor(promptItem);
7568
+ this.promptItemByListItem.set(listItem, promptItem);
7569
+ listItems.push(listItem);
7570
+ }
7571
+ return listItems
7572
+ }
7573
+
7574
+ #buildAllListItems(promptItems) {
7575
+ const listItems = [];
7576
+ for (const promptItem of promptItems) {
7577
+ if (listItems.length >= MAX_RENDERED_SUGGESTIONS$1) break
7578
+ const listItem = this.buildListItemElementFor(promptItem);
7579
+ this.promptItemByListItem.set(listItem, promptItem);
7580
+ listItems.push(listItem);
7581
+ }
7538
7582
  return listItems
7539
7583
  }
7540
7584
  }
@@ -7566,6 +7610,7 @@ class DeferredPromptSource extends LocalFilterSource {
7566
7610
  }
7567
7611
 
7568
7612
  const DEBOUNCE_INTERVAL = 200;
7613
+ const MAX_RENDERED_SUGGESTIONS = 100;
7569
7614
 
7570
7615
  class RemoteFilterSource extends BaseSource {
7571
7616
  constructor(url) {
@@ -7599,6 +7644,8 @@ class RemoteFilterSource extends BaseSource {
7599
7644
  this.promptItemByListItem = new WeakMap();
7600
7645
 
7601
7646
  for (const promptItem of promptItems) {
7647
+ if (listItems.length >= MAX_RENDERED_SUGGESTIONS) break
7648
+
7602
7649
  const listItem = this.buildListItemElementFor(promptItem);
7603
7650
  this.promptItemByListItem.set(listItem, promptItem);
7604
7651
  listItems.push(listItem);
@@ -7609,10 +7656,12 @@ class RemoteFilterSource extends BaseSource {
7609
7656
  }
7610
7657
 
7611
7658
  const NOTHING_FOUND_DEFAULT_MESSAGE = "Nothing found";
7659
+ const FILTER_DEBOUNCE_INTERVAL = 50;
7612
7660
 
7613
7661
  class LexicalPromptElement extends HTMLElement {
7614
7662
  #globalListeners = new ListenerBin()
7615
7663
  #popoverListeners = new ListenerBin()
7664
+ #debouncedFilterOptions = debounce(() => this.#filterOptions(), FILTER_DEBOUNCE_INTERVAL)
7616
7665
 
7617
7666
  constructor() {
7618
7667
  super();
@@ -7770,7 +7819,7 @@ class LexicalPromptElement extends HTMLElement {
7770
7819
 
7771
7820
  this.#popoverListeners.track(
7772
7821
  registerEventListener(this.#editorElement, "keydown", this.#handleKeydownOnPopover),
7773
- registerEventListener(this.#editorElement, "lexxy:change", this.#filterOptions)
7822
+ registerEventListener(this.#editorElement, "lexxy:change", this.#debouncedFilterOptions)
7774
7823
  );
7775
7824
 
7776
7825
  this.#registerKeyListeners();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.9.6-beta.bc0",
3
+ "version": "0.9.8-beta",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",