@lalternative/editor 0.6.1 → 0.8.0

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/dist/index.js CHANGED
@@ -23,8 +23,61 @@ function placeToolbar({
23
23
  }
24
24
 
25
25
  // src/SelectionToolbar.tsx
26
- import { useEffect, useLayoutEffect, useRef, useState } from "react";
26
+ import { useEffect as useEffect2, useLayoutEffect, useRef, useState as useState2 } from "react";
27
27
  import { createPortal } from "react-dom";
28
+
29
+ // src/useViewport.ts
30
+ import { useEffect, useState } from "react";
31
+
32
+ // src/viewport.ts
33
+ function readViewport({
34
+ innerWidth,
35
+ innerHeight,
36
+ visual
37
+ }) {
38
+ if (!visual) {
39
+ return { width: innerWidth, height: innerHeight, offsetTop: 0, bottomInset: 0 };
40
+ }
41
+ const covered = innerHeight - visual.height - visual.offsetTop;
42
+ return {
43
+ width: visual.width,
44
+ height: visual.height,
45
+ offsetTop: visual.offsetTop,
46
+ bottomInset: Math.max(0, Math.round(covered))
47
+ };
48
+ }
49
+
50
+ // src/useViewport.ts
51
+ var SERVER = { width: 0, height: 0, offsetTop: 0, bottomInset: 0 };
52
+ function read() {
53
+ if (typeof window === "undefined") return SERVER;
54
+ return readViewport({
55
+ innerWidth: window.innerWidth,
56
+ innerHeight: window.innerHeight,
57
+ visual: window.visualViewport
58
+ });
59
+ }
60
+ function useViewport() {
61
+ const [metrics, setMetrics] = useState(read);
62
+ useEffect(() => {
63
+ const update = () => setMetrics(read());
64
+ update();
65
+ const visual = window.visualViewport;
66
+ visual?.addEventListener("resize", update);
67
+ visual?.addEventListener("scroll", update);
68
+ window.addEventListener("resize", update);
69
+ window.addEventListener("orientationchange", update);
70
+ return () => {
71
+ visual?.removeEventListener("resize", update);
72
+ visual?.removeEventListener("scroll", update);
73
+ window.removeEventListener("resize", update);
74
+ window.removeEventListener("orientationchange", update);
75
+ };
76
+ }, []);
77
+ return metrics;
78
+ }
79
+
80
+ // src/SelectionToolbar.tsx
28
81
  import { jsx, jsxs } from "react/jsx-runtime";
29
82
  var FALLBACK = { width: 320, height: 40 };
30
83
  function SelectionToolbar({
@@ -37,15 +90,16 @@ function SelectionToolbar({
37
90
  container
38
91
  }) {
39
92
  const ref = useRef(null);
40
- const [size, setSize] = useState(FALLBACK);
41
- const [formatsOpen, setFormatsOpen] = useState(false);
93
+ const [size, setSize] = useState2(FALLBACK);
94
+ const [formatsOpen, setFormatsOpen] = useState2(false);
95
+ const viewport = useViewport();
42
96
  useLayoutEffect(() => {
43
97
  const el = ref.current;
44
98
  if (!el) return;
45
99
  const rect = el.getBoundingClientRect();
46
100
  if (rect.width > 0) setSize({ width: rect.width, height: rect.height });
47
101
  }, [selectedText, actions.length, formats.length]);
48
- useEffect(() => {
102
+ useEffect2(() => {
49
103
  if (!formatsOpen) return;
50
104
  const close = (e) => {
51
105
  if (ref.current && !ref.current.contains(e.target)) setFormatsOpen(false);
@@ -57,7 +111,7 @@ function SelectionToolbar({
57
111
  const place = placeToolbar({
58
112
  selection,
59
113
  toolbar: size,
60
- viewport: { width: window.innerWidth, height: window.innerHeight },
114
+ viewport: { width: viewport.width, height: viewport.height },
61
115
  preferBelow
62
116
  });
63
117
  return createPortal(
@@ -69,7 +123,8 @@ function SelectionToolbar({
69
123
  className: "lalt-toolbar",
70
124
  style: {
71
125
  "--lalt-toolbar-left": `${place.left}px`,
72
- "--lalt-toolbar-top": `${place.top}px`
126
+ "--lalt-toolbar-top": `${place.top}px`,
127
+ "--lalt-keyboard-inset": `${viewport.bottomInset}px`
73
128
  },
74
129
  children: [
75
130
  actions.map((action) => /* @__PURE__ */ jsxs(
@@ -127,12 +182,12 @@ function SelectionToolbar({
127
182
  }
128
183
 
129
184
  // src/useSelectionToolbar.ts
130
- import { useEffect as useEffect2, useState as useState2 } from "react";
185
+ import { useEffect as useEffect3, useState as useState3 } from "react";
131
186
  function useSelectionToolbar(editor) {
132
- const [selection, setSelection] = useState2(null);
133
- useEffect2(() => {
187
+ const [selection, setSelection] = useState3(null);
188
+ useEffect3(() => {
134
189
  if (!editor) return;
135
- const read = () => {
190
+ const read2 = () => {
136
191
  const { state } = editor;
137
192
  const { from, to, empty } = state.selection;
138
193
  if (empty || !editor.isFocused) {
@@ -155,14 +210,14 @@ function useSelectionToolbar(editor) {
155
210
  }
156
211
  });
157
212
  };
158
- read();
159
- editor.on("selectionUpdate", read);
160
- editor.on("blur", read);
161
- editor.on("focus", read);
213
+ read2();
214
+ editor.on("selectionUpdate", read2);
215
+ editor.on("blur", read2);
216
+ editor.on("focus", read2);
162
217
  return () => {
163
- editor.off("selectionUpdate", read);
164
- editor.off("blur", read);
165
- editor.off("focus", read);
218
+ editor.off("selectionUpdate", read2);
219
+ editor.off("blur", read2);
220
+ editor.off("focus", read2);
166
221
  };
167
222
  }, [editor]);
168
223
  return selection;
@@ -215,12 +270,313 @@ function isApple() {
215
270
  return /Mac|iPad|iPhone|iPod/.test(navigator.platform || navigator.userAgent);
216
271
  }
217
272
 
218
- // src/InlineSuggestions.ts
273
+ // src/EditorScreen.tsx
274
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
275
+ var defaultSaveLabels = {
276
+ saving: "Enregistrement en cours",
277
+ saved: "Enregistr\xE9",
278
+ dirty: "Modifications non enregistr\xE9es"
279
+ };
280
+ function EditorScreen({
281
+ children,
282
+ title,
283
+ onTitleChange,
284
+ titlePlaceholder = "Titre",
285
+ titleLabel = "Titre",
286
+ saveState,
287
+ saveLabels = defaultSaveLabels,
288
+ lead,
289
+ actions,
290
+ status,
291
+ aside,
292
+ footer,
293
+ subnav,
294
+ fill = false,
295
+ className
296
+ }) {
297
+ const { bottomInset } = useViewport();
298
+ const withTitle = onTitleChange !== void 0;
299
+ return /* @__PURE__ */ jsxs2(
300
+ "div",
301
+ {
302
+ className: [
303
+ "lalt-screen",
304
+ fill ? "lalt-screen--fill" : "",
305
+ className ?? ""
306
+ ].filter(Boolean).join(" "),
307
+ style: { "--lalt-keyboard": `${bottomInset}px` },
308
+ children: [
309
+ /* @__PURE__ */ jsxs2("header", { className: "lalt-screen__bar", children: [
310
+ lead,
311
+ saveState && /* @__PURE__ */ jsx2(SaveDot, { state: saveState, labels: saveLabels }),
312
+ status && /* @__PURE__ */ jsx2("div", { className: "lalt-screen__status", children: status }),
313
+ actions && /* @__PURE__ */ jsx2("div", { className: "lalt-screen__actions", children: actions })
314
+ ] }),
315
+ subnav && /* @__PURE__ */ jsx2("div", { className: "lalt-screen__subnav", children: subnav }),
316
+ /* @__PURE__ */ jsx2("div", { className: "lalt-screen__scroll", children: /* @__PURE__ */ jsxs2("div", { className: "lalt-screen__column", children: [
317
+ withTitle && /* @__PURE__ */ jsx2(
318
+ "input",
319
+ {
320
+ className: "lalt-screen__title",
321
+ value: title ?? "",
322
+ onChange: (event) => onTitleChange(event.target.value),
323
+ placeholder: titlePlaceholder,
324
+ "aria-label": titleLabel
325
+ }
326
+ ),
327
+ aside && /* @__PURE__ */ jsx2("div", { className: "lalt-screen__aside", children: aside }),
328
+ children,
329
+ footer && /* @__PURE__ */ jsx2("div", { className: "lalt-screen__footer", children: footer })
330
+ ] }) })
331
+ ]
332
+ }
333
+ );
334
+ }
335
+ function SaveDot({ state, labels }) {
336
+ if (state === "idle") return /* @__PURE__ */ jsx2("span", { className: "lalt-screen__save" });
337
+ const label = state === "saving" ? labels.saving : state === "dirty" ? labels.dirty : labels.saved;
338
+ return /* @__PURE__ */ jsxs2("span", { className: "lalt-screen__save", role: "status", "aria-live": "polite", children: [
339
+ /* @__PURE__ */ jsx2("span", { className: `lalt-screen__dot lalt-screen__dot--${state}`, "aria-hidden": true }),
340
+ /* @__PURE__ */ jsx2("span", { className: "lalt-screen__sr", children: label })
341
+ ] });
342
+ }
343
+
344
+ // src/slash.ts
345
+ var defaultSlashLabels = {
346
+ heading1: "Titre 1",
347
+ heading2: "Titre 2",
348
+ bulletList: "Liste \xE0 puces",
349
+ orderedList: "Liste num\xE9rot\xE9e",
350
+ blockquote: "Citation",
351
+ codeBlock: "Bloc de code",
352
+ horizontalRule: "S\xE9parateur"
353
+ };
354
+ function defaultSlashItems({
355
+ labels = defaultSlashLabels,
356
+ descriptions = {},
357
+ icons = {}
358
+ } = {}) {
359
+ const item = (id, shortcut, run) => ({
360
+ id,
361
+ title: labels[id],
362
+ description: descriptions[id],
363
+ shortcut,
364
+ icon: icons[id],
365
+ command: ({ editor, range }) => {
366
+ editor.chain().focus().deleteRange(range).run();
367
+ run(editor);
368
+ }
369
+ });
370
+ return [
371
+ item(
372
+ "heading1",
373
+ "#",
374
+ (editor) => editor.chain().focus().setNode("heading", { level: 1 }).run()
375
+ ),
376
+ item(
377
+ "heading2",
378
+ "##",
379
+ (editor) => editor.chain().focus().setNode("heading", { level: 2 }).run()
380
+ ),
381
+ item(
382
+ "bulletList",
383
+ "-",
384
+ (editor) => editor.chain().focus().toggleBulletList().run()
385
+ ),
386
+ item(
387
+ "orderedList",
388
+ "1.",
389
+ (editor) => editor.chain().focus().toggleOrderedList().run()
390
+ ),
391
+ item(
392
+ "blockquote",
393
+ ">",
394
+ (editor) => editor.chain().focus().toggleBlockquote().run()
395
+ ),
396
+ item(
397
+ "codeBlock",
398
+ "```",
399
+ (editor) => editor.chain().focus().toggleCodeBlock().run()
400
+ ),
401
+ item(
402
+ "horizontalRule",
403
+ "---",
404
+ (editor) => editor.chain().focus().setHorizontalRule().run()
405
+ )
406
+ ];
407
+ }
408
+ function filterSlashItems(items, query) {
409
+ const q = query.trim().toLowerCase();
410
+ if (q === "") return items;
411
+ return items.filter((item) => item.title.toLowerCase().includes(q));
412
+ }
413
+
414
+ // src/SlashMenu.tsx
415
+ import {
416
+ forwardRef,
417
+ useCallback,
418
+ useEffect as useEffect4,
419
+ useImperativeHandle,
420
+ useRef as useRef2,
421
+ useState as useState4
422
+ } from "react";
423
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
424
+ var SlashMenu = forwardRef(
425
+ function SlashMenu2({ items, command, emptyLabel = "Aucune commande" }, ref) {
426
+ const [selectedIndex, setSelectedIndex] = useState4(0);
427
+ const itemsRef = useRef2(items);
428
+ itemsRef.current = items;
429
+ const commandRef = useRef2(command);
430
+ commandRef.current = command;
431
+ const selectedRef = useRef2(selectedIndex);
432
+ selectedRef.current = selectedIndex;
433
+ const ids = items.map((item) => item.id).join(" ");
434
+ useEffect4(() => {
435
+ setSelectedIndex(0);
436
+ selectedRef.current = 0;
437
+ }, [ids]);
438
+ const selectItem = useCallback((index) => {
439
+ const item = itemsRef.current[index];
440
+ if (item) commandRef.current(item);
441
+ }, []);
442
+ const move = useCallback((delta) => {
443
+ const count = itemsRef.current.length;
444
+ if (count === 0) return;
445
+ const next = (selectedRef.current + delta + count) % count;
446
+ selectedRef.current = next;
447
+ setSelectedIndex(next);
448
+ }, []);
449
+ useImperativeHandle(ref, () => ({
450
+ onKeyDown: ({ event }) => {
451
+ if (event.key === "ArrowUp") {
452
+ move(-1);
453
+ return true;
454
+ }
455
+ if (event.key === "ArrowDown") {
456
+ move(1);
457
+ return true;
458
+ }
459
+ if (event.key === "Enter" || event.key === "Tab") {
460
+ if (itemsRef.current.length === 0) return false;
461
+ selectItem(selectedRef.current);
462
+ return true;
463
+ }
464
+ return false;
465
+ }
466
+ }));
467
+ if (items.length === 0) {
468
+ return /* @__PURE__ */ jsx3("div", { className: "lalt-slash lalt-slash--empty", children: emptyLabel });
469
+ }
470
+ return /* @__PURE__ */ jsx3("div", { className: "lalt-slash", role: "listbox", children: items.map((item, index) => /* @__PURE__ */ jsxs3(
471
+ "button",
472
+ {
473
+ type: "button",
474
+ role: "option",
475
+ "aria-selected": index === selectedIndex,
476
+ className: index === selectedIndex ? "lalt-slash__item lalt-slash__item--active" : "lalt-slash__item",
477
+ onMouseDown: (event) => {
478
+ event.preventDefault();
479
+ selectItem(index);
480
+ },
481
+ onMouseEnter: () => {
482
+ selectedRef.current = index;
483
+ setSelectedIndex(index);
484
+ },
485
+ children: [
486
+ item.icon && /* @__PURE__ */ jsx3("span", { className: "lalt-slash__icon", "aria-hidden": true, children: item.icon }),
487
+ /* @__PURE__ */ jsxs3("span", { className: "lalt-slash__text", children: [
488
+ /* @__PURE__ */ jsx3("span", { className: "lalt-slash__title", children: item.title }),
489
+ item.description && /* @__PURE__ */ jsx3("span", { className: "lalt-slash__description", children: item.description })
490
+ ] }),
491
+ item.shortcut && /* @__PURE__ */ jsx3("kbd", { className: "lalt-slash__shortcut", children: item.shortcut })
492
+ ]
493
+ },
494
+ item.id
495
+ )) });
496
+ }
497
+ );
498
+
499
+ // src/SlashCommands.ts
219
500
  import { Extension } from "@tiptap/core";
220
- import { Plugin, PluginKey } from "@tiptap/pm/state";
501
+ import { ReactRenderer } from "@tiptap/react";
502
+ import { PluginKey } from "@tiptap/pm/state";
503
+ import Suggestion from "@tiptap/suggestion";
504
+ var OFFSET = 6;
505
+ var SlashCommands = Extension.create({
506
+ name: "laSlashCommands",
507
+ addOptions() {
508
+ return {
509
+ items: () => [],
510
+ char: "/",
511
+ pluginKey: new PluginKey("laSlashCommands")
512
+ };
513
+ },
514
+ addProseMirrorPlugins() {
515
+ const { items, char, emptyLabel, pluginKey } = this.options;
516
+ const suggestion = {
517
+ char,
518
+ pluginKey,
519
+ startOfLine: false,
520
+ items: ({ query }) => filterSlashItems(items(), query),
521
+ render: () => {
522
+ let component = null;
523
+ let container = null;
524
+ const place = (clientRect) => {
525
+ if (!container || !clientRect) return;
526
+ const rect = clientRect();
527
+ if (!rect) return;
528
+ container.style.top = `${rect.bottom + OFFSET}px`;
529
+ container.style.left = `${rect.left}px`;
530
+ };
531
+ const close = () => {
532
+ container?.remove();
533
+ container = null;
534
+ component?.destroy();
535
+ component = null;
536
+ };
537
+ return {
538
+ onStart: (props) => {
539
+ component = new ReactRenderer(SlashMenu, {
540
+ props: { ...props, emptyLabel },
541
+ editor: props.editor
542
+ });
543
+ container = document.createElement("div");
544
+ container.className = "lalt-slash-anchor";
545
+ container.appendChild(component.element);
546
+ document.body.appendChild(container);
547
+ place(props.clientRect);
548
+ },
549
+ onUpdate: (props) => {
550
+ component?.updateProps({ ...props, emptyLabel });
551
+ place(props.clientRect);
552
+ },
553
+ onKeyDown: (props) => {
554
+ if (props.event.key === "Escape") {
555
+ close();
556
+ return true;
557
+ }
558
+ return component?.ref?.onKeyDown(props) ?? false;
559
+ },
560
+ onExit: close
561
+ };
562
+ }
563
+ };
564
+ return [
565
+ Suggestion({
566
+ editor: this.editor,
567
+ ...suggestion,
568
+ command: ({ editor, range, props }) => props.command({ editor, range })
569
+ })
570
+ ];
571
+ }
572
+ });
573
+
574
+ // src/InlineSuggestions.ts
575
+ import { Extension as Extension2 } from "@tiptap/core";
576
+ import { Plugin, PluginKey as PluginKey2 } from "@tiptap/pm/state";
221
577
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
222
- var inlineSuggestionsKey = new PluginKey("lalt-inline-suggestions");
223
- var InlineSuggestions = Extension.create({
578
+ var inlineSuggestionsKey = new PluginKey2("lalt-inline-suggestions");
579
+ var InlineSuggestions = Extension2.create({
224
580
  name: "lalt-inline-suggestions",
225
581
  addOptions() {
226
582
  return { actions: null };
@@ -385,19 +741,28 @@ function text(node) {
385
741
  return out;
386
742
  }
387
743
  export {
744
+ EditorScreen,
388
745
  InlineSuggestions,
389
746
  SelectionToolbar,
747
+ SlashCommands,
748
+ SlashMenu,
390
749
  blockFormats,
391
750
  blocksFromDoc,
392
751
  defaultFormatLabels,
752
+ defaultSaveLabels,
753
+ defaultSlashItems,
754
+ defaultSlashLabels,
393
755
  diffBlocks,
394
756
  docFromBlocks,
395
757
  docToMarkdown,
396
758
  expandToWords,
759
+ filterSlashItems,
397
760
  inlineSuggestionsKey,
398
761
  placeToolbar,
762
+ readViewport,
399
763
  shapesFromDoc,
400
764
  splitRevision,
401
- useSelectionToolbar
765
+ useSelectionToolbar,
766
+ useViewport
402
767
  };
403
768
  //# sourceMappingURL=index.js.map