@react-x11/components 0.7.1 → 0.9.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/README.md +295 -51
- package/dist/charts/node.d.ts +1 -1
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +6 -2
- package/dist/charts/node.js.map +1 -1
- package/dist/color-picker/ColorPicker.d.ts.map +1 -1
- package/dist/color-picker/ColorPicker.js +43 -33
- package/dist/color-picker/ColorPicker.js.map +1 -1
- package/dist/embed/client.d.ts +48 -3
- package/dist/embed/client.d.ts.map +1 -1
- package/dist/embed/client.js +79 -5
- package/dist/embed/client.js.map +1 -1
- package/dist/embed/index.d.ts +1 -1
- package/dist/embed/index.d.ts.map +1 -1
- package/dist/embed/index.js +1 -1
- package/dist/embed/index.js.map +1 -1
- package/dist/html/node.d.ts +3 -1
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +7 -5
- package/dist/html/node.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/{markdown → internal/markdown}/ast.d.ts +12 -0
- package/dist/internal/markdown/ast.d.ts.map +1 -0
- package/dist/internal/markdown/ast.js +22 -0
- package/dist/internal/markdown/ast.js.map +1 -0
- package/dist/internal/markdown/parse.d.ts.map +1 -0
- package/dist/{markdown → internal/markdown}/parse.js +112 -22
- package/dist/internal/markdown/parse.js.map +1 -0
- package/dist/internal/markdown/stringify.d.ts +4 -0
- package/dist/internal/markdown/stringify.d.ts.map +1 -0
- package/dist/internal/markdown/stringify.js +684 -0
- package/dist/internal/markdown/stringify.js.map +1 -0
- package/dist/internal/markdown/tags.d.ts.map +1 -0
- package/dist/internal/markdown/tags.js.map +1 -0
- package/dist/internal/scroll.d.ts +3 -2
- package/dist/internal/scroll.d.ts.map +1 -1
- package/dist/internal/scroll.js +33 -15
- package/dist/internal/scroll.js.map +1 -1
- package/dist/internal/units.d.ts +2 -0
- package/dist/internal/units.d.ts.map +1 -0
- package/dist/internal/units.js +35 -0
- package/dist/internal/units.js.map +1 -0
- package/dist/internal/window.d.ts.map +1 -1
- package/dist/internal/window.js +4 -2
- package/dist/internal/window.js.map +1 -1
- package/dist/maps/anchors.d.ts +98 -0
- package/dist/maps/anchors.d.ts.map +1 -0
- package/dist/maps/anchors.js +531 -0
- package/dist/maps/anchors.js.map +1 -0
- package/dist/maps/controller.d.ts +283 -0
- package/dist/maps/controller.d.ts.map +1 -0
- package/dist/maps/controller.js +743 -0
- package/dist/maps/controller.js.map +1 -0
- package/dist/maps/gl/buckets.d.ts +95 -0
- package/dist/maps/gl/buckets.d.ts.map +1 -0
- package/dist/maps/gl/buckets.js +362 -0
- package/dist/maps/gl/buckets.js.map +1 -0
- package/dist/maps/gl/build-worker.d.ts +13 -0
- package/dist/maps/gl/build-worker.d.ts.map +1 -0
- package/dist/maps/gl/build-worker.js +44 -0
- package/dist/maps/gl/build-worker.js.map +1 -0
- package/dist/maps/gl/color.d.ts +8 -0
- package/dist/maps/gl/color.d.ts.map +1 -0
- package/dist/maps/gl/color.js +57 -0
- package/dist/maps/gl/color.js.map +1 -0
- package/dist/maps/gl/cover.d.ts +51 -0
- package/dist/maps/gl/cover.d.ts.map +1 -0
- package/dist/maps/gl/cover.js +124 -0
- package/dist/maps/gl/cover.js.map +1 -0
- package/dist/maps/gl/index.d.ts +3 -0
- package/dist/maps/gl/index.d.ts.map +1 -0
- package/dist/maps/gl/index.js +20 -0
- package/dist/maps/gl/index.js.map +1 -0
- package/dist/maps/gl/markers.d.ts +28 -0
- package/dist/maps/gl/markers.d.ts.map +1 -0
- package/dist/maps/gl/markers.js +60 -0
- package/dist/maps/gl/markers.js.map +1 -0
- package/dist/maps/gl/overlays.d.ts +68 -0
- package/dist/maps/gl/overlays.d.ts.map +1 -0
- package/dist/maps/gl/overlays.js +300 -0
- package/dist/maps/gl/overlays.js.map +1 -0
- package/dist/maps/gl/pane.d.ts +12 -0
- package/dist/maps/gl/pane.d.ts.map +1 -0
- package/dist/maps/gl/pane.js +37 -0
- package/dist/maps/gl/pane.js.map +1 -0
- package/dist/maps/gl/placement.d.ts +111 -0
- package/dist/maps/gl/placement.d.ts.map +1 -0
- package/dist/maps/gl/placement.js +567 -0
- package/dist/maps/gl/placement.js.map +1 -0
- package/dist/maps/gl/renderer.d.ts +352 -0
- package/dist/maps/gl/renderer.d.ts.map +1 -0
- package/dist/maps/gl/renderer.js +1127 -0
- package/dist/maps/gl/renderer.js.map +1 -0
- package/dist/maps/gl/shaders.d.ts +120 -0
- package/dist/maps/gl/shaders.d.ts.map +1 -0
- package/dist/maps/gl/shaders.js +451 -0
- package/dist/maps/gl/shaders.js.map +1 -0
- package/dist/maps/gl/store.d.ts +115 -0
- package/dist/maps/gl/store.d.ts.map +1 -0
- package/dist/maps/gl/store.js +469 -0
- package/dist/maps/gl/store.js.map +1 -0
- package/dist/maps/gl/text.d.ts +154 -0
- package/dist/maps/gl/text.d.ts.map +1 -0
- package/dist/maps/gl/text.js +382 -0
- package/dist/maps/gl/text.js.map +1 -0
- package/dist/maps/gl/view.d.ts +54 -0
- package/dist/maps/gl/view.d.ts.map +1 -0
- package/dist/maps/gl/view.js +974 -0
- package/dist/maps/gl/view.js.map +1 -0
- package/dist/maps/index.d.ts +1 -1
- package/dist/maps/index.d.ts.map +1 -1
- package/dist/maps/index.js +216 -52
- package/dist/maps/index.js.map +1 -1
- package/dist/maps/labels.d.ts +37 -16
- package/dist/maps/labels.d.ts.map +1 -1
- package/dist/maps/labels.js +188 -119
- package/dist/maps/labels.js.map +1 -1
- package/dist/maps/node.d.ts +162 -56
- package/dist/maps/node.d.ts.map +1 -1
- package/dist/maps/node.js +607 -515
- package/dist/maps/node.js.map +1 -1
- package/dist/maps/overlay.d.ts +72 -9
- package/dist/maps/overlay.d.ts.map +1 -1
- package/dist/maps/overlay.js +108 -28
- package/dist/maps/overlay.js.map +1 -1
- package/dist/maps/paint.d.ts +3 -0
- package/dist/maps/paint.d.ts.map +1 -1
- package/dist/maps/paint.js.map +1 -1
- package/dist/maps/proj.d.ts +14 -0
- package/dist/maps/proj.d.ts.map +1 -1
- package/dist/maps/proj.js +28 -0
- package/dist/maps/proj.js.map +1 -1
- package/dist/maps/renderer.d.ts +69 -0
- package/dist/maps/renderer.d.ts.map +1 -0
- package/dist/maps/renderer.js +66 -0
- package/dist/maps/renderer.js.map +1 -0
- package/dist/maps/sources.d.ts +25 -3
- package/dist/maps/sources.d.ts.map +1 -1
- package/dist/maps/sources.js +17 -0
- package/dist/maps/sources.js.map +1 -1
- package/dist/maps/theme.d.ts +19 -0
- package/dist/maps/theme.d.ts.map +1 -0
- package/dist/maps/theme.js +44 -0
- package/dist/maps/theme.js.map +1 -0
- package/dist/maps/tiles.d.ts +117 -17
- package/dist/maps/tiles.d.ts.map +1 -1
- package/dist/maps/tiles.js +163 -30
- package/dist/maps/tiles.js.map +1 -1
- package/dist/maps/types.d.ts +188 -20
- package/dist/maps/types.d.ts.map +1 -1
- package/dist/markdown/expressions.d.ts +1 -1
- package/dist/markdown/expressions.d.ts.map +1 -1
- package/dist/markdown/expressions.js.map +1 -1
- package/dist/markdown/index.d.ts +7 -6
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +9 -7
- package/dist/markdown/index.js.map +1 -1
- package/dist/markdown/spans.d.ts +1 -1
- package/dist/markdown/spans.d.ts.map +1 -1
- package/dist/media-player/index.d.ts +11 -3
- package/dist/media-player/index.d.ts.map +1 -1
- package/dist/media-player/index.js +13 -2
- package/dist/media-player/index.js.map +1 -1
- package/dist/rich-text-editor/clipboard.d.ts +23 -0
- package/dist/rich-text-editor/clipboard.d.ts.map +1 -0
- package/dist/rich-text-editor/clipboard.js +307 -0
- package/dist/rich-text-editor/clipboard.js.map +1 -0
- package/dist/rich-text-editor/collab.d.ts +32 -0
- package/dist/rich-text-editor/collab.d.ts.map +1 -0
- package/dist/rich-text-editor/collab.js +55 -0
- package/dist/rich-text-editor/collab.js.map +1 -0
- package/dist/rich-text-editor/commands.d.ts +50 -0
- package/dist/rich-text-editor/commands.d.ts.map +1 -0
- package/dist/rich-text-editor/commands.js +376 -0
- package/dist/rich-text-editor/commands.js.map +1 -0
- package/dist/rich-text-editor/drag.d.ts +68 -0
- package/dist/rich-text-editor/drag.d.ts.map +1 -0
- package/dist/rich-text-editor/drag.js +148 -0
- package/dist/rich-text-editor/drag.js.map +1 -0
- package/dist/rich-text-editor/html.d.ts +145 -0
- package/dist/rich-text-editor/html.d.ts.map +1 -0
- package/dist/rich-text-editor/html.js +516 -0
- package/dist/rich-text-editor/html.js.map +1 -0
- package/dist/rich-text-editor/index.d.ts +263 -0
- package/dist/rich-text-editor/index.d.ts.map +1 -0
- package/dist/rich-text-editor/index.js +822 -0
- package/dist/rich-text-editor/index.js.map +1 -0
- package/dist/rich-text-editor/inline.d.ts +94 -0
- package/dist/rich-text-editor/inline.d.ts.map +1 -0
- package/dist/rich-text-editor/inline.js +252 -0
- package/dist/rich-text-editor/inline.js.map +1 -0
- package/dist/rich-text-editor/keymap.d.ts +71 -0
- package/dist/rich-text-editor/keymap.d.ts.map +1 -0
- package/dist/rich-text-editor/keymap.js +393 -0
- package/dist/rich-text-editor/keymap.js.map +1 -0
- package/dist/rich-text-editor/keys.d.ts +29 -0
- package/dist/rich-text-editor/keys.d.ts.map +1 -0
- package/dist/rich-text-editor/keys.js +93 -0
- package/dist/rich-text-editor/keys.js.map +1 -0
- package/dist/rich-text-editor/look.d.ts +71 -0
- package/dist/rich-text-editor/look.d.ts.map +1 -0
- package/dist/rich-text-editor/look.js +342 -0
- package/dist/rich-text-editor/look.js.map +1 -0
- package/dist/rich-text-editor/markdown.d.ts +18 -0
- package/dist/rich-text-editor/markdown.d.ts.map +1 -0
- package/dist/rich-text-editor/markdown.js +496 -0
- package/dist/rich-text-editor/markdown.js.map +1 -0
- package/dist/rich-text-editor/nodes.d.ts +135 -0
- package/dist/rich-text-editor/nodes.d.ts.map +1 -0
- package/dist/rich-text-editor/nodes.js +397 -0
- package/dist/rich-text-editor/nodes.js.map +1 -0
- package/dist/rich-text-editor/render.d.ts +68 -0
- package/dist/rich-text-editor/render.d.ts.map +1 -0
- package/dist/rich-text-editor/render.js +500 -0
- package/dist/rich-text-editor/render.js.map +1 -0
- package/dist/rich-text-editor/schema.d.ts +16 -0
- package/dist/rich-text-editor/schema.d.ts.map +1 -0
- package/dist/rich-text-editor/schema.js +386 -0
- package/dist/rich-text-editor/schema.js.map +1 -0
- package/dist/rich-text-editor/store.d.ts +11 -0
- package/dist/rich-text-editor/store.d.ts.map +1 -0
- package/dist/rich-text-editor/store.js +43 -0
- package/dist/rich-text-editor/store.js.map +1 -0
- package/dist/rich-text-editor/suggest.d.ts +127 -0
- package/dist/rich-text-editor/suggest.d.ts.map +1 -0
- package/dist/rich-text-editor/suggest.js +0 -0
- package/dist/rich-text-editor/suggest.js.map +1 -0
- package/dist/rich-text-editor/tables.d.ts +41 -0
- package/dist/rich-text-editor/tables.d.ts.map +1 -0
- package/dist/rich-text-editor/tables.js +204 -0
- package/dist/rich-text-editor/tables.js.map +1 -0
- package/dist/rich-text-editor/toolbar.d.ts +58 -0
- package/dist/rich-text-editor/toolbar.d.ts.map +1 -0
- package/dist/rich-text-editor/toolbar.js +548 -0
- package/dist/rich-text-editor/toolbar.js.map +1 -0
- package/dist/rich-text-editor/view.d.ts +368 -0
- package/dist/rich-text-editor/view.d.ts.map +1 -0
- package/dist/rich-text-editor/view.js +1968 -0
- package/dist/rich-text-editor/view.js.map +1 -0
- package/dist/rich-text-editor/virtual.d.ts +54 -0
- package/dist/rich-text-editor/virtual.d.ts.map +1 -0
- package/dist/rich-text-editor/virtual.js +262 -0
- package/dist/rich-text-editor/virtual.js.map +1 -0
- package/dist/richtext/node.d.ts +25 -2
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +46 -24
- package/dist/richtext/node.js.map +1 -1
- package/dist/table/index.d.ts.map +1 -1
- package/dist/table/index.js +14 -5
- package/dist/table/index.js.map +1 -1
- package/dist/terminal/backends.d.ts +4 -1
- package/dist/terminal/backends.d.ts.map +1 -1
- package/dist/terminal/backends.js.map +1 -1
- package/dist/terminal/index.d.ts +18 -7
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +28 -11
- package/dist/terminal/index.js.map +1 -1
- package/dist/terminal/vt/node.d.ts +56 -3
- package/dist/terminal/vt/node.d.ts.map +1 -1
- package/dist/terminal/vt/node.js +106 -26
- package/dist/terminal/vt/node.js.map +1 -1
- package/dist/terminal/vt/renderer.d.ts +4 -0
- package/dist/terminal/vt/renderer.d.ts.map +1 -1
- package/dist/terminal/vt/renderer.js +14 -5
- package/dist/terminal/vt/renderer.js.map +1 -1
- package/dist/tree/index.d.ts.map +1 -1
- package/dist/tree/index.js +6 -4
- package/dist/tree/index.js.map +1 -1
- package/package.json +23 -4
- package/src/charts/node.ts +6 -2
- package/src/color-picker/ColorPicker.ts +107 -81
- package/src/embed/client.ts +102 -8
- package/src/embed/index.ts +2 -0
- package/src/html/node.ts +7 -5
- package/src/index.ts +2 -0
- package/src/{markdown → internal/markdown}/ast.ts +24 -7
- package/src/{markdown → internal/markdown}/parse.ts +134 -24
- package/src/internal/markdown/stringify.ts +772 -0
- package/src/internal/scroll.ts +39 -18
- package/src/internal/units.ts +34 -0
- package/src/internal/window.ts +4 -2
- package/src/maps/anchors.ts +607 -0
- package/src/maps/controller.ts +999 -0
- package/src/maps/gl/buckets.ts +457 -0
- package/src/maps/gl/build-worker.ts +55 -0
- package/src/maps/gl/color.ts +62 -0
- package/src/maps/gl/cover.ts +170 -0
- package/src/maps/gl/index.ts +23 -0
- package/src/maps/gl/markers.ts +83 -0
- package/src/maps/gl/overlays.ts +379 -0
- package/src/maps/gl/pane.ts +42 -0
- package/src/maps/gl/placement.ts +816 -0
- package/src/maps/gl/renderer.ts +1576 -0
- package/src/maps/gl/shaders.ts +486 -0
- package/src/maps/gl/store.ts +566 -0
- package/src/maps/gl/text.ts +495 -0
- package/src/maps/gl/view.ts +1224 -0
- package/src/maps/index.ts +270 -81
- package/src/maps/labels.ts +214 -121
- package/src/maps/node.ts +763 -629
- package/src/maps/overlay.ts +148 -43
- package/src/maps/paint.ts +3 -0
- package/src/maps/proj.ts +31 -0
- package/src/maps/renderer.ts +155 -0
- package/src/maps/sources.ts +38 -3
- package/src/maps/theme.ts +51 -0
- package/src/maps/tiles.ts +191 -33
- package/src/maps/types.ts +188 -20
- package/src/markdown/expressions.ts +5 -1
- package/src/markdown/index.ts +14 -11
- package/src/markdown/spans.ts +1 -1
- package/src/media-player/index.ts +18 -4
- package/src/rich-text-editor/clipboard.ts +439 -0
- package/src/rich-text-editor/collab.ts +91 -0
- package/src/rich-text-editor/commands.ts +448 -0
- package/src/rich-text-editor/dom-globals.d.ts +23 -0
- package/src/rich-text-editor/drag.ts +211 -0
- package/src/rich-text-editor/html.ts +619 -0
- package/src/rich-text-editor/index.ts +1331 -0
- package/src/rich-text-editor/inline.ts +345 -0
- package/src/rich-text-editor/keymap.ts +510 -0
- package/src/rich-text-editor/keys.ts +119 -0
- package/src/rich-text-editor/look.ts +427 -0
- package/src/rich-text-editor/markdown.ts +653 -0
- package/src/rich-text-editor/nodes.ts +497 -0
- package/src/rich-text-editor/render.ts +800 -0
- package/src/rich-text-editor/schema.ts +431 -0
- package/src/rich-text-editor/store.ts +39 -0
- package/src/rich-text-editor/suggest.ts +597 -0
- package/src/rich-text-editor/tables.ts +272 -0
- package/src/rich-text-editor/toolbar.ts +724 -0
- package/src/rich-text-editor/view.ts +2287 -0
- package/src/rich-text-editor/virtual.ts +347 -0
- package/src/richtext/node.ts +59 -30
- package/src/table/index.ts +14 -5
- package/src/terminal/backends.ts +4 -1
- package/src/terminal/index.ts +43 -15
- package/src/terminal/vt/node.ts +128 -29
- package/src/terminal/vt/renderer.ts +27 -5
- package/src/tree/index.ts +6 -4
- package/dist/markdown/ast.d.ts.map +0 -1
- package/dist/markdown/ast.js +0 -17
- package/dist/markdown/ast.js.map +0 -1
- package/dist/markdown/parse.d.ts.map +0 -1
- package/dist/markdown/parse.js.map +0 -1
- package/dist/markdown/tags.d.ts.map +0 -1
- package/dist/markdown/tags.js.map +0 -1
- /package/dist/{markdown → internal/markdown}/parse.d.ts +0 -0
- /package/dist/{markdown → internal/markdown}/tags.d.ts +0 -0
- /package/dist/{markdown → internal/markdown}/tags.js +0 -0
- /package/src/{markdown → internal/markdown}/tags.ts +0 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
// Suggestions: a list that opens at a trigger character as it is typed — `@`
|
|
2
|
+
// for people, `#` for issues, `:` for emoji, `/` for a block menu — kept by a
|
|
3
|
+
// plugin and drawn by `<RichTextEditor>`.
|
|
4
|
+
//
|
|
5
|
+
// The division of labour is ProseMirror's own, and it is what keeps the list
|
|
6
|
+
// working on every rung of the ladder (docs/prd-rich-text-editor.md):
|
|
7
|
+
//
|
|
8
|
+
// - **The plugin holds the state**: which trigger is open, what has been
|
|
9
|
+
// typed after it, the rows that answer it and the highlighted one — all of
|
|
10
|
+
// it plugin state, changed only by transactions. An app that owns the
|
|
11
|
+
// EditorState sees the list change like anything else, and a re-render
|
|
12
|
+
// cannot lose it, because a render is not where it lives.
|
|
13
|
+
// - **The plugin takes the keys**, through `handleKeyDown`, and only while
|
|
14
|
+
// rows are showing: Up and Down move the highlight, Enter and Tab take the
|
|
15
|
+
// row, Escape closes the list. It sits ahead of the default keymap, so
|
|
16
|
+
// Enter takes a row instead of splitting the paragraph.
|
|
17
|
+
// - **The plugin asks for the rows**, from its plugin view: a list is
|
|
18
|
+
// filtered here, a function is called, and an answer to a query the user
|
|
19
|
+
// has since typed past is dropped — `<CodeEditor>`'s completion rule, for
|
|
20
|
+
// the same reason.
|
|
21
|
+
// - **The component draws**, from nothing but this state: a `<popup>` hung
|
|
22
|
+
// off the trigger, whose rows run `acceptSuggestion`.
|
|
23
|
+
//
|
|
24
|
+
// When a list opens is the decision with a user-visible edge: **as the
|
|
25
|
+
// trigger's word is typed, never as the caret moves.** The value is markdown,
|
|
26
|
+
// so a mention stays text (`@ada`), and a document is soon full of words that
|
|
27
|
+
// start with a trigger. Opening whenever the caret visits one would pop a list
|
|
28
|
+
// up at every click; opening on an edit at the caret is the rule GitHub's
|
|
29
|
+
// comment box keeps, and `<CodeEditor>`'s completion.
|
|
30
|
+
import type { ReactNode } from 'react';
|
|
31
|
+
import { closeHistory } from 'prosemirror-history';
|
|
32
|
+
import type { Node as PMNode } from 'prosemirror-model';
|
|
33
|
+
import { Plugin, PluginKey, Selection, TextSelection } from 'prosemirror-state';
|
|
34
|
+
import type { Command, EditorState, Transaction } from 'prosemirror-state';
|
|
35
|
+
import { StepMap } from 'prosemirror-transform';
|
|
36
|
+
import { Decoration, DecorationSet } from 'prosemirror-view';
|
|
37
|
+
import type { EditorView } from 'prosemirror-view';
|
|
38
|
+
|
|
39
|
+
/** One row of a suggestion list. */
|
|
40
|
+
export interface SuggestionItem {
|
|
41
|
+
/** What the row shows — and, in a list the editor filters, what the query
|
|
42
|
+
* is matched against. */
|
|
43
|
+
label: string;
|
|
44
|
+
/** Muted text after the label: a handle, a description, a shortcut. */
|
|
45
|
+
detail?: string;
|
|
46
|
+
/**
|
|
47
|
+
* What replaces the trigger and the query: text, or a node of the
|
|
48
|
+
* document's schema — text carrying a link mark, say, or a mention node of
|
|
49
|
+
* an app's own schema. Default: the trigger and the label, `@Ada`. A space
|
|
50
|
+
* follows it, unless one already does.
|
|
51
|
+
*/
|
|
52
|
+
insert?: string | PMNode;
|
|
53
|
+
/**
|
|
54
|
+
* Run instead of inserting anything, on the state the trigger and the
|
|
55
|
+
* query have been deleted from — a block menu's "Heading 1". What it
|
|
56
|
+
* dispatches joins the deletion, so the choice is one undo step.
|
|
57
|
+
*/
|
|
58
|
+
command?: Command;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** What a suggester's `items` function is asked with. */
|
|
62
|
+
export interface SuggestionQuery {
|
|
63
|
+
/** What has been typed after the trigger. */
|
|
64
|
+
query: string;
|
|
65
|
+
/** The trigger. */
|
|
66
|
+
char: string;
|
|
67
|
+
/** The state the query was read from — for its schema, to build a node. */
|
|
68
|
+
state: EditorState;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** What `renderItem` is handed beside its row. */
|
|
72
|
+
export interface SuggestionRow {
|
|
73
|
+
/** The highlighted row — the one Enter takes — drawn on the accent. */
|
|
74
|
+
selected: boolean;
|
|
75
|
+
/** What has been typed after the trigger. */
|
|
76
|
+
query: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A trigger character, and where the rows it opens come from. `Item` is the
|
|
81
|
+
* app's own row type when a row carries more than a label — an avatar, a
|
|
82
|
+
* presence — for `renderItem` to draw.
|
|
83
|
+
*/
|
|
84
|
+
export interface Suggester<Item extends SuggestionItem = SuggestionItem> {
|
|
85
|
+
/** What opens the list: `'@'`, `'#'`, `':'`, `'/'`. */
|
|
86
|
+
char: string;
|
|
87
|
+
/**
|
|
88
|
+
* The rows. An array is filtered by what has been typed — labels that
|
|
89
|
+
* start with it first, then labels with a word that does, then any that
|
|
90
|
+
* contain it. A function is called with each new query and its rows are
|
|
91
|
+
* shown as given; it may answer with a promise, and an answer that comes
|
|
92
|
+
* after the query has moved on is dropped.
|
|
93
|
+
*/
|
|
94
|
+
items:
|
|
95
|
+
| readonly Item[]
|
|
96
|
+
| ((query: SuggestionQuery) => readonly Item[] | Promise<readonly Item[]>);
|
|
97
|
+
/**
|
|
98
|
+
* A row of the app's own, in place of the label and its detail: an avatar,
|
|
99
|
+
* a presence dot, a shortcut drawn as keys. The editor still draws the
|
|
100
|
+
* highlight behind it and takes a press on it, and an array is still
|
|
101
|
+
* filtered on the label.
|
|
102
|
+
*/
|
|
103
|
+
renderItem?(item: Item, row: SuggestionRow): ReactNode;
|
|
104
|
+
/** Only at the very start of a textblock — a block menu. Default false:
|
|
105
|
+
* at the start of any word. */
|
|
106
|
+
startOfLine?: boolean;
|
|
107
|
+
/** The query may have spaces in it — a full name. Default false: a space
|
|
108
|
+
* ends it. Two spaces in a row end it either way. */
|
|
109
|
+
allowSpaces?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** An open list — what `suggestionState` answers and the component draws. */
|
|
113
|
+
export interface SuggestionState {
|
|
114
|
+
/** Which suggester opened it: an index into the plugin's list. */
|
|
115
|
+
readonly index: number;
|
|
116
|
+
readonly char: string;
|
|
117
|
+
/** What has been typed after the trigger — the whole word the caret is
|
|
118
|
+
* in, so a row taken mid-word replaces all of it. */
|
|
119
|
+
readonly query: string;
|
|
120
|
+
/** The trigger and the query, in the document: what a row replaces. */
|
|
121
|
+
readonly from: number;
|
|
122
|
+
readonly to: number;
|
|
123
|
+
/** The rows — null until the first answer comes. */
|
|
124
|
+
readonly items: readonly SuggestionItem[] | null;
|
|
125
|
+
/** The highlighted row. */
|
|
126
|
+
readonly selected: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface PluginState {
|
|
130
|
+
active: SuggestionState | null;
|
|
131
|
+
/** Where a list was closed — by Escape, or by a choice. A trigger at one
|
|
132
|
+
* of these positions does not open again while its character is there. */
|
|
133
|
+
closed: readonly number[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type Meta =
|
|
137
|
+
| {
|
|
138
|
+
type: 'items';
|
|
139
|
+
index: number;
|
|
140
|
+
from: number;
|
|
141
|
+
query: string;
|
|
142
|
+
items: readonly SuggestionItem[];
|
|
143
|
+
}
|
|
144
|
+
| { type: 'select'; index: number }
|
|
145
|
+
| { type: 'close' };
|
|
146
|
+
|
|
147
|
+
const key = new PluginKey<PluginState>('suggestions');
|
|
148
|
+
|
|
149
|
+
/** Rows to a page: PageUp and PageDown move by it, and the list shows it. */
|
|
150
|
+
export const SUGGESTION_PAGE = 8;
|
|
151
|
+
/** The most rows a list keeps. */
|
|
152
|
+
const MAX_ITEMS = 100;
|
|
153
|
+
/** How many closed triggers are remembered. */
|
|
154
|
+
const MAX_CLOSED = 16;
|
|
155
|
+
|
|
156
|
+
// --- where a list opens --------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
/** An inline leaf — an image, a line break — in a textblock's text. */
|
|
159
|
+
const LEAF = '';
|
|
160
|
+
/** What a trigger may follow: the start of the block, a space, an inline
|
|
161
|
+
* leaf, or opening punctuation — so `ada@example.com` is an address. */
|
|
162
|
+
const BEFORE_TRIGGER = /[\s([{<"'“‘«]/u;
|
|
163
|
+
/** What ends a query that may not hold spaces. */
|
|
164
|
+
const WORD_END = /[\s]/u;
|
|
165
|
+
|
|
166
|
+
interface Match {
|
|
167
|
+
index: number;
|
|
168
|
+
char: string;
|
|
169
|
+
query: string;
|
|
170
|
+
from: number;
|
|
171
|
+
to: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Has what was typed after a trigger stopped being a query? */
|
|
175
|
+
function queryEnded(typed: string, spaces: boolean): boolean {
|
|
176
|
+
if (typed.includes(LEAF)) return true;
|
|
177
|
+
if (!spaces) return WORD_END.test(typed);
|
|
178
|
+
return /^\s|\s\s|\n/u.test(typed);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The trigger whose word the caret is in, if any: for each suggester, the
|
|
183
|
+
* nearest occurrence of its character before the caret, where a word can
|
|
184
|
+
* start, with nothing between it and the caret that ends a query — and
|
|
185
|
+
* outside code, where `@` is a character. The nearest of those wins.
|
|
186
|
+
*/
|
|
187
|
+
function findMatch(
|
|
188
|
+
state: EditorState,
|
|
189
|
+
suggesters: readonly Suggester[],
|
|
190
|
+
): Match | null {
|
|
191
|
+
const sel = state.selection;
|
|
192
|
+
if (!(sel instanceof TextSelection) || !sel.empty) return null;
|
|
193
|
+
const $head = sel.$head;
|
|
194
|
+
const block = $head.parent;
|
|
195
|
+
if (!block.isTextblock || block.type.spec.code) return null;
|
|
196
|
+
const text = block.textBetween(0, block.content.size, undefined, LEAF);
|
|
197
|
+
const caret = $head.parentOffset;
|
|
198
|
+
const start = $head.start();
|
|
199
|
+
let best: Match | null = null;
|
|
200
|
+
for (let index = 0; index < suggesters.length; index++) {
|
|
201
|
+
const s = suggesters[index];
|
|
202
|
+
const char = s.char;
|
|
203
|
+
if (!char || caret < char.length) continue;
|
|
204
|
+
const at = text.lastIndexOf(char, caret - char.length);
|
|
205
|
+
if (at < 0 || (best && start + at <= best.from)) continue;
|
|
206
|
+
if (s.startOfLine ? at !== 0 : at > 0 && !BEFORE_TRIGGER.test(text[at - 1]))
|
|
207
|
+
continue;
|
|
208
|
+
const spaces = !!s.allowSpaces;
|
|
209
|
+
if (queryEnded(text.slice(at + char.length, caret), spaces)) continue;
|
|
210
|
+
if (block.childAfter(at).node?.marks.some((m) => m.type.spec.code))
|
|
211
|
+
continue;
|
|
212
|
+
// without spaces the query is the whole word, the part past the caret
|
|
213
|
+
// too; with them it is what has been typed so far
|
|
214
|
+
let end = caret;
|
|
215
|
+
if (!spaces) {
|
|
216
|
+
while (
|
|
217
|
+
end < text.length &&
|
|
218
|
+
!WORD_END.test(text[end]) &&
|
|
219
|
+
!text.startsWith(char, end)
|
|
220
|
+
)
|
|
221
|
+
end++;
|
|
222
|
+
}
|
|
223
|
+
best = {
|
|
224
|
+
index,
|
|
225
|
+
char,
|
|
226
|
+
query: text.slice(at + char.length, end),
|
|
227
|
+
from: start + at,
|
|
228
|
+
to: start + end,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return best;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Did `tr` change the document between `from` and `to` — positions in the
|
|
235
|
+
* document it made? A deletion there counts: it leaves an empty range. */
|
|
236
|
+
function editedBetween(tr: Transaction, from: number, to: number): boolean {
|
|
237
|
+
if (!tr.docChanged) return false;
|
|
238
|
+
const { maps } = tr.mapping;
|
|
239
|
+
for (let i = 0; i < maps.length; i++) {
|
|
240
|
+
const later = tr.mapping.slice(i + 1);
|
|
241
|
+
let hit = false;
|
|
242
|
+
maps[i].forEach((_oldStart, _oldEnd, newStart, newEnd) => {
|
|
243
|
+
if (later.map(newStart, -1) <= to && later.map(newEnd, 1) >= from)
|
|
244
|
+
hit = true;
|
|
245
|
+
});
|
|
246
|
+
if (hit) return true;
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function nextState(
|
|
252
|
+
tr: Transaction,
|
|
253
|
+
prev: PluginState,
|
|
254
|
+
state: EditorState,
|
|
255
|
+
suggesters: readonly Suggester[],
|
|
256
|
+
): PluginState {
|
|
257
|
+
const meta = tr.getMeta(key) as Meta | undefined;
|
|
258
|
+
let closed = prev.closed;
|
|
259
|
+
let active = prev.active;
|
|
260
|
+
if (tr.docChanged) {
|
|
261
|
+
// a closed trigger goes with its character; the open one follows its own
|
|
262
|
+
closed = closed.flatMap((pos) => {
|
|
263
|
+
const r = tr.mapping.mapResult(pos);
|
|
264
|
+
return r.deletedAfter ? [] : [r.pos];
|
|
265
|
+
});
|
|
266
|
+
if (active) {
|
|
267
|
+
const r = tr.mapping.mapResult(active.from);
|
|
268
|
+
active = r.deletedAfter ? null : { ...active, from: r.pos };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (meta?.type === 'close') {
|
|
272
|
+
// where the trigger was — or, when a choice replaced it, where what
|
|
273
|
+
// replaced it starts, so a mention that begins with its own trigger does
|
|
274
|
+
// not reopen on itself
|
|
275
|
+
if (prev.active) {
|
|
276
|
+
closed = [...closed, tr.mapping.map(prev.active.from, -1)].slice(
|
|
277
|
+
-MAX_CLOSED,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return { active: null, closed };
|
|
281
|
+
}
|
|
282
|
+
const match = findMatch(state, suggesters);
|
|
283
|
+
if (!match || closed.includes(match.from)) {
|
|
284
|
+
return !prev.active && closed === prev.closed
|
|
285
|
+
? prev
|
|
286
|
+
: { active: null, closed };
|
|
287
|
+
}
|
|
288
|
+
const same =
|
|
289
|
+
!!active && active.from === match.from && active.index === match.index;
|
|
290
|
+
// a new list opens on an edit in its word, and not on an edit that is not
|
|
291
|
+
// the user's — a reset, a collaborator's change
|
|
292
|
+
if (
|
|
293
|
+
!same &&
|
|
294
|
+
(tr.getMeta('addToHistory') === false ||
|
|
295
|
+
!editedBetween(tr, match.from, match.to))
|
|
296
|
+
) {
|
|
297
|
+
return { active: null, closed };
|
|
298
|
+
}
|
|
299
|
+
let items = same ? active!.items : null;
|
|
300
|
+
let selected = same && active!.query === match.query ? active!.selected : 0;
|
|
301
|
+
if (
|
|
302
|
+
meta?.type === 'items' &&
|
|
303
|
+
meta.index === match.index &&
|
|
304
|
+
meta.from === match.from &&
|
|
305
|
+
meta.query === match.query
|
|
306
|
+
) {
|
|
307
|
+
items = meta.items;
|
|
308
|
+
selected = 0;
|
|
309
|
+
} else if (meta?.type === 'select') {
|
|
310
|
+
selected = meta.index;
|
|
311
|
+
}
|
|
312
|
+
if (items) selected = Math.max(0, Math.min(selected, items.length - 1));
|
|
313
|
+
return { active: { ...match, items, selected }, closed };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// --- the rows ------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* A list, by what has been typed: labels that start with it (as typed, then
|
|
320
|
+
* in any case), then labels with a later word that does, then labels that
|
|
321
|
+
* merely contain it — in the list's own order within each. What the editor
|
|
322
|
+
* does with a suggester whose `items` is an array; exported for an app that
|
|
323
|
+
* filters its own.
|
|
324
|
+
*/
|
|
325
|
+
export function filterSuggestions<Item extends SuggestionItem>(
|
|
326
|
+
items: readonly Item[],
|
|
327
|
+
query: string,
|
|
328
|
+
): Item[] {
|
|
329
|
+
if (!query) return items.slice(0, MAX_ITEMS);
|
|
330
|
+
const folded = query.toLowerCase();
|
|
331
|
+
const ranked: Array<{ item: Item; rank: number; order: number }> = [];
|
|
332
|
+
items.forEach((item, order) => {
|
|
333
|
+
const label = item.label;
|
|
334
|
+
const lower = label.toLowerCase();
|
|
335
|
+
if (!lower.includes(folded)) return;
|
|
336
|
+
const rank = label.startsWith(query)
|
|
337
|
+
? 0
|
|
338
|
+
: lower.startsWith(folded)
|
|
339
|
+
? 1
|
|
340
|
+
: lower.split(/[\s\-_./@#]+/u).some((word) => word.startsWith(folded))
|
|
341
|
+
? 2
|
|
342
|
+
: 3;
|
|
343
|
+
ranked.push({ item, rank, order });
|
|
344
|
+
});
|
|
345
|
+
ranked.sort((a, b) => a.rank - b.rank || a.order - b.order);
|
|
346
|
+
return ranked.slice(0, MAX_ITEMS).map((r) => r.item);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** The plugin view: asks for the rows whenever the query changes, and hands
|
|
350
|
+
* back the answer to the query that is still open — no other. */
|
|
351
|
+
function asker(
|
|
352
|
+
view: EditorView,
|
|
353
|
+
get: () => readonly Suggester[],
|
|
354
|
+
): { update(): void; destroy(): void } {
|
|
355
|
+
let asked = '';
|
|
356
|
+
let serial = 0;
|
|
357
|
+
const ask = (): void => {
|
|
358
|
+
const active = key.getState(view.state)?.active ?? null;
|
|
359
|
+
if (!active) {
|
|
360
|
+
asked = '';
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const stamp = `${active.index}${active.from}${active.query}`;
|
|
364
|
+
if (stamp === asked) return;
|
|
365
|
+
asked = stamp;
|
|
366
|
+
const id = ++serial;
|
|
367
|
+
const suggester = get()[active.index];
|
|
368
|
+
if (!suggester) return;
|
|
369
|
+
const { index, from, query, char } = active;
|
|
370
|
+
const state = view.state;
|
|
371
|
+
// always a turn later, answered or not: rows land in a transaction of
|
|
372
|
+
// their own, never inside the one being applied
|
|
373
|
+
Promise.resolve()
|
|
374
|
+
.then(() =>
|
|
375
|
+
// a query typed past before its turn came is never asked at all
|
|
376
|
+
id !== serial
|
|
377
|
+
? null
|
|
378
|
+
: typeof suggester.items === 'function'
|
|
379
|
+
? suggester.items({ query, char, state })
|
|
380
|
+
: filterSuggestions(suggester.items, query),
|
|
381
|
+
)
|
|
382
|
+
.then((items) => {
|
|
383
|
+
if (!items || id !== serial || view.isDestroyed) return;
|
|
384
|
+
const now = key.getState(view.state)?.active;
|
|
385
|
+
if (
|
|
386
|
+
!now ||
|
|
387
|
+
now.index !== index ||
|
|
388
|
+
now.from !== from ||
|
|
389
|
+
now.query !== query
|
|
390
|
+
)
|
|
391
|
+
return;
|
|
392
|
+
const meta: Meta = {
|
|
393
|
+
type: 'items',
|
|
394
|
+
index,
|
|
395
|
+
from,
|
|
396
|
+
query,
|
|
397
|
+
items: items.slice(0, MAX_ITEMS),
|
|
398
|
+
};
|
|
399
|
+
view.dispatch(view.state.tr.setMeta(key, meta));
|
|
400
|
+
})
|
|
401
|
+
// a source that throws has nothing to suggest
|
|
402
|
+
.catch(() => {});
|
|
403
|
+
};
|
|
404
|
+
ask();
|
|
405
|
+
return {
|
|
406
|
+
update: ask,
|
|
407
|
+
destroy: () => {
|
|
408
|
+
serial++;
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// --- the commands ----------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
/** The open list, or null — what `<RichTextEditor>` draws its popup from,
|
|
416
|
+
* and what an app drawing its own reads. */
|
|
417
|
+
export function suggestionState(state: EditorState): SuggestionState | null {
|
|
418
|
+
return key.getState(state)?.active ?? null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** The suggester whose list is open, or null — for its `renderItem`. */
|
|
422
|
+
export function suggesterFor(state: EditorState): Suggester | null {
|
|
423
|
+
const active = suggestionState(state);
|
|
424
|
+
if (!active) return null;
|
|
425
|
+
const get = key.get(state)?.spec.suggesters as
|
|
426
|
+
(() => readonly Suggester[]) | undefined;
|
|
427
|
+
return get?.()[active.index] ?? null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Close the list. It does not open again for this trigger while the
|
|
431
|
+
* trigger is there; typing a new one opens a new list. */
|
|
432
|
+
export const dismissSuggestion: Command = (state, dispatch) => {
|
|
433
|
+
if (!suggestionState(state)) return false;
|
|
434
|
+
dispatch?.(state.tr.setMeta(key, { type: 'close' } satisfies Meta));
|
|
435
|
+
return true;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/** Highlight row `index`, held to the rows there are. */
|
|
439
|
+
export function selectSuggestion(index: number): Command {
|
|
440
|
+
return (state, dispatch) => {
|
|
441
|
+
const active = suggestionState(state);
|
|
442
|
+
if (!active?.items?.length) return false;
|
|
443
|
+
const to = Math.max(0, Math.min(index, active.items.length - 1));
|
|
444
|
+
if (to !== active.selected)
|
|
445
|
+
dispatch?.(state.tr.setMeta(key, { type: 'select', index: to }));
|
|
446
|
+
return true;
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** A command's transaction, made on the state after `tr`, as part of `tr`. */
|
|
451
|
+
function join(tr: Transaction, inner: Transaction): void {
|
|
452
|
+
for (const step of inner.steps) tr.step(step);
|
|
453
|
+
if (inner.selectionSet)
|
|
454
|
+
tr.setSelection(inner.selection.map(tr.doc, StepMap.empty));
|
|
455
|
+
if (inner.storedMarksSet) tr.setStoredMarks(inner.storedMarks);
|
|
456
|
+
if (inner.scrolledIntoView) tr.scrollIntoView();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Take a row — the highlighted one, unless `index` says which. Its `insert`
|
|
461
|
+
* (or the trigger and its label) replaces the trigger and the query, with a
|
|
462
|
+
* space after it; or its `command` runs where they were.
|
|
463
|
+
*/
|
|
464
|
+
export function acceptSuggestion(index?: number): Command {
|
|
465
|
+
return (state, dispatch, view) => {
|
|
466
|
+
const active = suggestionState(state);
|
|
467
|
+
const item = active?.items?.[index ?? active.selected];
|
|
468
|
+
if (!active || !item) return false;
|
|
469
|
+
if (!dispatch) return true;
|
|
470
|
+
const { from, to } = active;
|
|
471
|
+
// its own undo step: undoing a choice gives back what was typed, however
|
|
472
|
+
// soon after the typing it came
|
|
473
|
+
const tr = closeHistory(
|
|
474
|
+
state.tr.setMeta(key, { type: 'close' } satisfies Meta),
|
|
475
|
+
);
|
|
476
|
+
if (item.command) {
|
|
477
|
+
tr.delete(from, to);
|
|
478
|
+
const run = item.command;
|
|
479
|
+
const { state: after, transactions } = state.applyTransaction(tr);
|
|
480
|
+
if (transactions.length > 1) {
|
|
481
|
+
// another plugin appended to the deletion, so the command's state is
|
|
482
|
+
// not `tr`'s document and its steps cannot join: two steps, then.
|
|
483
|
+
// `after` is what applying `tr` makes — the view's next state, or
|
|
484
|
+
// the one an app that owns the state hands back — so the command's
|
|
485
|
+
// transaction applies after `tr` on either.
|
|
486
|
+
dispatch(tr);
|
|
487
|
+
run(after, dispatch, view);
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
let joined = false;
|
|
491
|
+
run(
|
|
492
|
+
after,
|
|
493
|
+
(inner) => {
|
|
494
|
+
if (joined || inner.before !== tr.doc) return;
|
|
495
|
+
joined = true;
|
|
496
|
+
join(tr, inner);
|
|
497
|
+
},
|
|
498
|
+
view,
|
|
499
|
+
);
|
|
500
|
+
dispatch(tr.scrollIntoView());
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
const insert = item.insert ?? active.char + item.label;
|
|
504
|
+
if (typeof insert === 'string') tr.insertText(insert, from, to);
|
|
505
|
+
else tr.replaceRangeWith(from, to, insert);
|
|
506
|
+
const end = tr.mapping.map(to);
|
|
507
|
+
const $end = tr.doc.resolve(end);
|
|
508
|
+
if ($end.parent.inlineContent) {
|
|
509
|
+
// the way a word ends: with a space, or the one already after it
|
|
510
|
+
const next = $end.nodeAfter;
|
|
511
|
+
if (!(next?.isText && /^\s/u.test(next.text ?? '')))
|
|
512
|
+
tr.insertText(' ', end);
|
|
513
|
+
tr.setSelection(TextSelection.create(tr.doc, end + 1));
|
|
514
|
+
} else {
|
|
515
|
+
tr.setSelection(Selection.near($end));
|
|
516
|
+
}
|
|
517
|
+
dispatch(tr.scrollIntoView());
|
|
518
|
+
return true;
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// --- the plugin --------------------------------------------------------------------
|
|
523
|
+
|
|
524
|
+
/** Plain keys only, and only while rows are showing: a chord, or Shift with
|
|
525
|
+
* an arrow, is still the editor's. */
|
|
526
|
+
function keyDown(view: EditorView, event: KeyboardEvent): boolean {
|
|
527
|
+
const active = suggestionState(view.state);
|
|
528
|
+
if (!active?.items?.length || !view.editable) return false;
|
|
529
|
+
const e = event as unknown as {
|
|
530
|
+
key: string;
|
|
531
|
+
shiftKey: boolean;
|
|
532
|
+
ctrlKey: boolean;
|
|
533
|
+
altKey: boolean;
|
|
534
|
+
metaKey: boolean;
|
|
535
|
+
};
|
|
536
|
+
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return false;
|
|
537
|
+
const move = (by: number): boolean =>
|
|
538
|
+
selectSuggestion(active.selected + by)(view.state, view.dispatch);
|
|
539
|
+
switch (e.key) {
|
|
540
|
+
case 'ArrowDown':
|
|
541
|
+
return move(1);
|
|
542
|
+
case 'ArrowUp':
|
|
543
|
+
return move(-1);
|
|
544
|
+
case 'PageDown':
|
|
545
|
+
return move(SUGGESTION_PAGE);
|
|
546
|
+
case 'PageUp':
|
|
547
|
+
return move(-SUGGESTION_PAGE);
|
|
548
|
+
case 'Enter':
|
|
549
|
+
case 'Tab':
|
|
550
|
+
return acceptSuggestion()(view.state, view.dispatch, view);
|
|
551
|
+
case 'Escape':
|
|
552
|
+
return dismissSuggestion(view.state, view.dispatch);
|
|
553
|
+
}
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* The suggestion plugin — one per editor, holding every trigger. It goes
|
|
559
|
+
* ahead of the default plugins, so its keys come before the keymap's:
|
|
560
|
+
* `<RichTextEditor suggestions>` puts it there itself, and an app that owns
|
|
561
|
+
* the state writes `plugins: [suggestions([…]), ...defaultPlugins(schema)]`.
|
|
562
|
+
*
|
|
563
|
+
* `suggesters` may be a function that returns them, read whenever they are
|
|
564
|
+
* needed — how the component hands over a prop that may be a new array on
|
|
565
|
+
* every render without the plugin, and the list that is open, being rebuilt.
|
|
566
|
+
* While a trigger's word is being typed its text carries the decoration
|
|
567
|
+
* class `suggestion`, which the editor draws in the accent colour.
|
|
568
|
+
*/
|
|
569
|
+
export function suggestions(
|
|
570
|
+
suggesters: readonly Suggester[] | (() => readonly Suggester[]),
|
|
571
|
+
): Plugin {
|
|
572
|
+
const get = typeof suggesters === 'function' ? suggesters : () => suggesters;
|
|
573
|
+
return new Plugin<PluginState>({
|
|
574
|
+
key,
|
|
575
|
+
// read back by `suggesterFor`, so a row is drawn by its suggester's
|
|
576
|
+
// `renderItem` in an app-owned state too
|
|
577
|
+
suggesters: get,
|
|
578
|
+
state: {
|
|
579
|
+
init: () => ({ active: null, closed: [] }),
|
|
580
|
+
apply: (tr, prev, _old, state) => nextState(tr, prev, state, get()),
|
|
581
|
+
},
|
|
582
|
+
props: {
|
|
583
|
+
handleKeyDown: keyDown,
|
|
584
|
+
decorations(state) {
|
|
585
|
+
const active = suggestionState(state);
|
|
586
|
+
return active
|
|
587
|
+
? DecorationSet.create(state.doc, [
|
|
588
|
+
Decoration.inline(active.from, active.to, {
|
|
589
|
+
class: 'suggestion',
|
|
590
|
+
}),
|
|
591
|
+
])
|
|
592
|
+
: null;
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
view: (view) => asker(view, get),
|
|
596
|
+
});
|
|
597
|
+
}
|