@medway-ui/notes-core 0.1.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.
Files changed (57) hide show
  1. package/README.md +127 -0
  2. package/dist/document/normalize.d.ts +61 -0
  3. package/dist/document/normalize.d.ts.map +1 -0
  4. package/dist/document/normalize.js +373 -0
  5. package/dist/document/normalize.js.map +1 -0
  6. package/dist/document/types.d.ts +47 -0
  7. package/dist/document/types.d.ts.map +1 -0
  8. package/dist/document/types.js +9 -0
  9. package/dist/document/types.js.map +1 -0
  10. package/dist/editor/handle.d.ts +28 -0
  11. package/dist/editor/handle.d.ts.map +1 -0
  12. package/dist/editor/handle.js +34 -0
  13. package/dist/editor/handle.js.map +1 -0
  14. package/dist/editor/slashCommands.d.ts +21 -0
  15. package/dist/editor/slashCommands.d.ts.map +1 -0
  16. package/dist/editor/slashCommands.js +98 -0
  17. package/dist/editor/slashCommands.js.map +1 -0
  18. package/dist/editor/vocabulary.d.ts +110 -0
  19. package/dist/editor/vocabulary.d.ts.map +1 -0
  20. package/dist/editor/vocabulary.js +11 -0
  21. package/dist/editor/vocabulary.js.map +1 -0
  22. package/dist/index.d.ts +15 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +11 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/schema/contract.d.ts +14 -0
  27. package/dist/schema/contract.d.ts.map +1 -0
  28. package/dist/schema/contract.js +51 -0
  29. package/dist/schema/contract.js.map +1 -0
  30. package/dist/schema/extensions.d.ts +23 -0
  31. package/dist/schema/extensions.d.ts.map +1 -0
  32. package/dist/schema/extensions.js +81 -0
  33. package/dist/schema/extensions.js.map +1 -0
  34. package/dist/schema/image.d.ts +14 -0
  35. package/dist/schema/image.d.ts.map +1 -0
  36. package/dist/schema/image.js +28 -0
  37. package/dist/schema/image.js.map +1 -0
  38. package/dist/schema/index.d.ts +17 -0
  39. package/dist/schema/index.d.ts.map +1 -0
  40. package/dist/schema/index.js +18 -0
  41. package/dist/schema/index.js.map +1 -0
  42. package/dist/schema/slashCommand.d.ts +19 -0
  43. package/dist/schema/slashCommand.d.ts.map +1 -0
  44. package/dist/schema/slashCommand.js +146 -0
  45. package/dist/schema/slashCommand.js.map +1 -0
  46. package/package.json +134 -0
  47. package/src/document/normalize.ts +456 -0
  48. package/src/document/types.ts +74 -0
  49. package/src/editor/handle.ts +65 -0
  50. package/src/editor/slashCommands.ts +120 -0
  51. package/src/editor/vocabulary.ts +126 -0
  52. package/src/index.ts +52 -0
  53. package/src/schema/contract.ts +76 -0
  54. package/src/schema/extensions.ts +120 -0
  55. package/src/schema/image.ts +44 -0
  56. package/src/schema/index.ts +29 -0
  57. package/src/schema/slashCommand.ts +181 -0
@@ -0,0 +1,181 @@
1
+ import { Extension, type Editor, type Range } from '@tiptap/core';
2
+ import { PluginKey } from '@tiptap/pm/state';
3
+ import Suggestion, {
4
+ exitSuggestion,
5
+ type SuggestionProps,
6
+ } from '@tiptap/suggestion';
7
+
8
+ import type {
9
+ EditorCaretRect,
10
+ SlashCommandId,
11
+ SlashMenuState,
12
+ } from '../editor/vocabulary.js';
13
+ import { filterSlashCommands } from '../editor/slashCommands.js';
14
+
15
+ const SlashPluginKey = new PluginKey('medwaySlashCommand');
16
+
17
+ type SlashProps = SuggestionProps<SlashCommandId, SlashCommandId>;
18
+
19
+ const EMPTY: SlashMenuState = {
20
+ active: false,
21
+ query: '',
22
+ commandIds: [],
23
+ caret: null,
24
+ };
25
+
26
+ const toCaretRect = (props: SlashProps): EditorCaretRect | null => {
27
+ const rect =
28
+ props.clientRect?.() ?? props.decorationNode?.getBoundingClientRect();
29
+ if (!rect) return null;
30
+
31
+ return { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
32
+ };
33
+
34
+ const applyCommand = (editor: Editor, range: Range, id: SlashCommandId) => {
35
+ // Deleting the typed range and applying the block happen in one chain so a
36
+ // single undo takes the student back to the text they typed.
37
+ const chain = editor.chain().focus().deleteRange(range);
38
+
39
+ switch (id) {
40
+ case 'paragraph':
41
+ chain.setParagraph().run();
42
+ return;
43
+ case 'heading1':
44
+ chain.setNode('heading', { level: 1 }).run();
45
+ return;
46
+ case 'heading2':
47
+ chain.setNode('heading', { level: 2 }).run();
48
+ return;
49
+ case 'heading3':
50
+ chain.setNode('heading', { level: 3 }).run();
51
+ return;
52
+ case 'bulletList':
53
+ chain.toggleBulletList().run();
54
+ return;
55
+ case 'orderedList':
56
+ chain.toggleOrderedList().run();
57
+ return;
58
+ case 'taskList':
59
+ chain.toggleTaskList().run();
60
+ return;
61
+ case 'blockquote':
62
+ chain.setBlockquote().run();
63
+ return;
64
+ case 'codeBlock':
65
+ chain.setCodeBlock().run();
66
+ return;
67
+ case 'horizontalRule':
68
+ chain.setHorizontalRule().run();
69
+ return;
70
+ case 'image':
71
+ // Only clears the typed `/query`: picking and uploading a file is host
72
+ // UI (`NoteEditor`'s `onImageUpload`), which inserts the node itself.
73
+ chain.run();
74
+ return;
75
+ default: {
76
+ // Exhaustiveness check: a new `SlashCommandId` without a case above stops
77
+ // this file compiling. Still a runtime no-op rather than a throw — `id`
78
+ // crosses the WebView bridge, so an out-of-contract value should degrade
79
+ // gracefully instead of crashing the editor.
80
+ const _exhaustive: never = id;
81
+ void _exhaustive;
82
+ chain.run();
83
+ }
84
+ }
85
+ };
86
+
87
+ export type SlashCommandController = {
88
+ /** Add this to the extension list — see `buildExtensions`. */
89
+ extension: Extension;
90
+ /** Runs the command the host picked, if the menu is still open. */
91
+ run: (commandId: SlashCommandId) => boolean;
92
+ dismiss: (editor: Editor) => void;
93
+ /** Re-publishes the caret position, e.g. after the document scrolls. */
94
+ refreshCaret: () => void;
95
+ };
96
+
97
+ /**
98
+ * The `/` menu's detection half: owns only *when* `/` is active, *what* was
99
+ * typed, *where* the caret is, and the range to replace. The host draws the
100
+ * menu and answers with `run`, which removes the typed `/query` in the same
101
+ * transaction as the command it applies.
102
+ */
103
+ export const createSlashCommand = (
104
+ onChange: (state: SlashMenuState) => void
105
+ ): SlashCommandController => {
106
+ let active: SlashProps | null = null;
107
+
108
+ const publish = (props: SlashProps | null) => {
109
+ if (!props) {
110
+ onChange(EMPTY);
111
+ return;
112
+ }
113
+
114
+ onChange({
115
+ active: true,
116
+ query: props.query,
117
+ commandIds: props.items,
118
+ caret: toCaretRect(props),
119
+ });
120
+ };
121
+
122
+ const extension = Extension.create({
123
+ name: 'medwaySlashCommand',
124
+
125
+ addProseMirrorPlugins() {
126
+ return [
127
+ Suggestion<SlashCommandId, SlashCommandId>({
128
+ editor: this.editor,
129
+ pluginKey: SlashPluginKey,
130
+ char: '/',
131
+ decorationClass: 'medway-slash',
132
+ // A `/` inside a code block is code, not a command.
133
+ allow: ({ state, range }) =>
134
+ state.doc.resolve(range.from).parent.type.name !== 'codeBlock',
135
+ items: ({ query }) => filterSlashCommands(query),
136
+ command: ({ editor, range, props }) =>
137
+ applyCommand(editor, range, props),
138
+ render: () => ({
139
+ onStart: (props) => {
140
+ active = props;
141
+ publish(props);
142
+ },
143
+ onUpdate: (props) => {
144
+ active = props;
145
+ publish(props);
146
+ },
147
+ onExit: () => {
148
+ active = null;
149
+ publish(null);
150
+ },
151
+ onKeyDown: ({ event }) => {
152
+ if (event.key !== 'Escape') return false;
153
+
154
+ active = null;
155
+ publish(null);
156
+ return true;
157
+ },
158
+ }),
159
+ }),
160
+ ];
161
+ },
162
+ });
163
+
164
+ return {
165
+ extension,
166
+ run: (commandId) => {
167
+ if (!active || !active.items.includes(commandId)) return false;
168
+
169
+ active.command(commandId);
170
+ return true;
171
+ },
172
+ dismiss: (editor) => {
173
+ if (!active) return;
174
+
175
+ exitSuggestion(editor.view, SlashPluginKey);
176
+ },
177
+ refreshCaret: () => {
178
+ if (active) publish(active);
179
+ },
180
+ };
181
+ };