@inshapardaz/likhari-react 0.1.25-dev.230 → 0.1.25-dev.234

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.
@@ -1 +1 @@
1
- {"version":3,"file":"AutocompletePlugin.d.ts","sourceRoot":"","sources":["../../src/autocomplete/AutocompletePlugin.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAGrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAsB1D;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,MAAM,EACN,QAAQ,EAAE,cAAc,GACzB,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,8EAA8E;IAC9E,QAAQ,EAAE,aAAa,GAAG,MAAM,CAAC;CAClC,sCAgNA"}
1
+ {"version":3,"file":"AutocompletePlugin.d.ts","sourceRoot":"","sources":["../../src/autocomplete/AutocompletePlugin.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAGrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAyB1D;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,MAAM,EACN,QAAQ,EAAE,cAAc,GACzB,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,8EAA8E;IAC9E,QAAQ,EAAE,aAAa,GAAG,MAAM,CAAC;CAClC,sCAuMA"}
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from 'react';
2
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
3
3
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
4
- import { $getSelection, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_HIGH, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ENTER_COMMAND, KEY_TAB_COMMAND, } from 'lexical';
4
+ import { $getSelection, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_HIGH, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, KEY_TAB_COMMAND, } from 'lexical';
5
5
  import { $replaceMatch } from '../find/findReplaceActions.js';
6
6
  import { onSpellWordsChange } from '../spellcheck/userWords.js';
7
7
  import { WordIndex } from './wordIndex.js';
@@ -99,7 +99,7 @@ export function AutocompletePlugin({ label, stores, language: chosenLanguage, })
99
99
  if (items.length === 0)
100
100
  return hide();
101
101
  const rect = caretRect(editor);
102
- show({ prefix, language, items, index: 0, selected: false, x: rect.left, y: rect.bottom, dir: rtl ? 'rtl' : 'ltr' });
102
+ show({ prefix, language, items, index: 0, selected: false, ...rect, dir: rtl ? 'rtl' : 'ltr' });
103
103
  });
104
104
  };
105
105
  // Handles an arrow key for the list, and cancels the browser's own caret movement.
@@ -123,11 +123,6 @@ export function AutocompletePlugin({ label, stores, language: chosenLanguage, })
123
123
  return;
124
124
  if ((event.key === 'ArrowUp' || event.key === 'ArrowDown') && (current || prefixAtCaret(editor) !== null))
125
125
  return;
126
- if (event.key === 'Escape' && popupRef.current) {
127
- event.preventDefault();
128
- show(null);
129
- return;
130
- }
131
126
  // Left, Right, Home and End move the caret away from the word, so the list closes.
132
127
  if (MOVEMENT_KEYS.has(event.key)) {
133
128
  typing.current = false;
@@ -139,6 +134,11 @@ export function AutocompletePlugin({ label, stores, language: chosenLanguage, })
139
134
  hide();
140
135
  setTimeout(refresh, 0);
141
136
  };
137
+ // Text that arrives without a keydown (input methods, some keyboard layouts) is typing too.
138
+ const onInput = () => {
139
+ typing.current = true;
140
+ setTimeout(refresh, 0);
141
+ };
142
142
  const onMouseDown = () => {
143
143
  typing.current = false;
144
144
  hide();
@@ -147,9 +147,11 @@ export function AutocompletePlugin({ label, stores, language: chosenLanguage, })
147
147
  const attach = (next) => {
148
148
  root?.removeEventListener('keydown', onKeyDown);
149
149
  root?.removeEventListener('mousedown', onMouseDown);
150
+ root?.removeEventListener('input', onInput);
150
151
  root = next;
151
152
  root?.addEventListener('keydown', onKeyDown);
152
153
  root?.addEventListener('mousedown', onMouseDown);
154
+ root?.addEventListener('input', onInput);
153
155
  };
154
156
  const unregisterRoot = editor.registerRootListener((next) => attach(next));
155
157
  const unregisterUpdate = editor.registerUpdateListener(() => {
@@ -166,6 +168,13 @@ export function AutocompletePlugin({ label, stores, language: chosenLanguage, })
166
168
  const unregisterCommands = [
167
169
  editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (event) => move(1, event), COMMAND_PRIORITY_HIGH),
168
170
  editor.registerCommand(KEY_ARROW_UP_COMMAND, (event) => move(-1, event), COMMAND_PRIORITY_HIGH),
171
+ // While the list is open, Escape only closes it. Lexical would otherwise blur the editor.
172
+ editor.registerCommand(KEY_ESCAPE_COMMAND, () => {
173
+ if (!popupRef.current)
174
+ return false;
175
+ show(null);
176
+ return true;
177
+ }, COMMAND_PRIORITY_HIGH),
169
178
  editor.registerCommand(KEY_TAB_COMMAND, (event) => acceptHighlighted(event), COMMAND_PRIORITY_HIGH),
170
179
  editor.registerCommand(KEY_ENTER_COMMAND, (event) => (popupRef.current?.selected ? acceptHighlighted(event) : false), COMMAND_PRIORITY_HIGH),
171
180
  ];
@@ -195,11 +204,34 @@ export function AutocompletePlugin({ label, stores, language: chosenLanguage, })
195
204
  }, [stores]);
196
205
  if (!popup)
197
206
  return null;
198
- return (_jsx("div", { className: "likhari-autocomplete", role: "listbox", "aria-label": label, dir: popup.dir, style: { position: 'fixed', left: popup.x, top: popup.y + 4 }, children: popup.items.map((item, i) => (_jsx("div", { role: "option", "aria-selected": i === popup.index, className: i === popup.index ? 'likhari-autocomplete-item likhari-autocomplete-item--active' : 'likhari-autocomplete-item', onMouseDown: (event) => {
207
+ return _jsx(SuggestionList, { popup: popup, label: label, onPick: (item) => {
208
+ acceptItem(editor, item);
209
+ recordAcceptance(popup.language, item);
210
+ show(null);
211
+ } });
212
+ }
213
+ function SuggestionList({ popup, label, onPick }) {
214
+ const ref = useRef(null);
215
+ // Placed after it is drawn, so its real size is known: kept inside the window, on the side of the
216
+ // caret that has room, and right-to-left lists align their right edge with the caret.
217
+ useLayoutEffect(() => {
218
+ const element = ref.current;
219
+ if (!element)
220
+ return;
221
+ const margin = 8;
222
+ const width = element.offsetWidth;
223
+ const height = element.offsetHeight;
224
+ const preferredLeft = popup.dir === 'rtl' ? popup.right - width : popup.left;
225
+ const left = Math.min(Math.max(preferredLeft, margin), window.innerWidth - width - margin);
226
+ const below = popup.bottom + 4;
227
+ const fitsBelow = below + height <= window.innerHeight - margin;
228
+ const top = fitsBelow ? below : Math.max(popup.top - height - 4, margin);
229
+ element.style.left = `${Math.max(left, margin)}px`;
230
+ element.style.top = `${top}px`;
231
+ });
232
+ return (_jsx("div", { ref: ref, className: "likhari-autocomplete", role: "listbox", "aria-label": label, dir: popup.dir, style: { position: 'fixed' }, children: popup.items.map((item, i) => (_jsx("div", { role: "option", "aria-selected": i === popup.index, className: i === popup.index ? 'likhari-autocomplete-item likhari-autocomplete-item--active' : 'likhari-autocomplete-item', onMouseDown: (event) => {
199
233
  event.preventDefault();
200
- acceptItem(editor, item);
201
- recordAcceptance(popup.language, item);
202
- show(null);
234
+ onPick(item);
203
235
  }, children: item }, item))) }));
204
236
  }
205
237
  /** The typed part of the word at the caret, when the caret is at its end and the word is long enough to complete. */
@@ -227,10 +259,12 @@ function caretRect(editor) {
227
259
  if (selection && selection.rangeCount > 0) {
228
260
  const rect = selection.getRangeAt(0).getBoundingClientRect();
229
261
  if (rect.width > 0 || rect.height > 0)
230
- return { left: rect.left, bottom: rect.bottom };
262
+ return { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom };
231
263
  }
232
264
  const root = editor.getRootElement()?.getBoundingClientRect();
233
- return { left: root?.left ?? 0, bottom: root?.top ?? 0 };
265
+ const left = root?.left ?? 0;
266
+ const top = root?.top ?? 0;
267
+ return { left, right: left, top, bottom: top };
234
268
  }
235
269
  function acceptItem(editor, item) {
236
270
  editor.update(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-react",
3
- "version": "0.1.25-dev.230",
3
+ "version": "0.1.25-dev.234",
4
4
  "description": "React implementation of the Likhari editor, built on @lexical/react.",
5
5
  "license": "MIT",
6
6
  "repository": {