@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.
- package/README.md +127 -0
- package/dist/document/normalize.d.ts +61 -0
- package/dist/document/normalize.d.ts.map +1 -0
- package/dist/document/normalize.js +373 -0
- package/dist/document/normalize.js.map +1 -0
- package/dist/document/types.d.ts +47 -0
- package/dist/document/types.d.ts.map +1 -0
- package/dist/document/types.js +9 -0
- package/dist/document/types.js.map +1 -0
- package/dist/editor/handle.d.ts +28 -0
- package/dist/editor/handle.d.ts.map +1 -0
- package/dist/editor/handle.js +34 -0
- package/dist/editor/handle.js.map +1 -0
- package/dist/editor/slashCommands.d.ts +21 -0
- package/dist/editor/slashCommands.d.ts.map +1 -0
- package/dist/editor/slashCommands.js +98 -0
- package/dist/editor/slashCommands.js.map +1 -0
- package/dist/editor/vocabulary.d.ts +110 -0
- package/dist/editor/vocabulary.d.ts.map +1 -0
- package/dist/editor/vocabulary.js +11 -0
- package/dist/editor/vocabulary.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/schema/contract.d.ts +14 -0
- package/dist/schema/contract.d.ts.map +1 -0
- package/dist/schema/contract.js +51 -0
- package/dist/schema/contract.js.map +1 -0
- package/dist/schema/extensions.d.ts +23 -0
- package/dist/schema/extensions.d.ts.map +1 -0
- package/dist/schema/extensions.js +81 -0
- package/dist/schema/extensions.js.map +1 -0
- package/dist/schema/image.d.ts +14 -0
- package/dist/schema/image.d.ts.map +1 -0
- package/dist/schema/image.js +28 -0
- package/dist/schema/image.js.map +1 -0
- package/dist/schema/index.d.ts +17 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +18 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/slashCommand.d.ts +19 -0
- package/dist/schema/slashCommand.d.ts.map +1 -0
- package/dist/schema/slashCommand.js +146 -0
- package/dist/schema/slashCommand.js.map +1 -0
- package/package.json +134 -0
- package/src/document/normalize.ts +456 -0
- package/src/document/types.ts +74 -0
- package/src/editor/handle.ts +65 -0
- package/src/editor/slashCommands.ts +120 -0
- package/src/editor/vocabulary.ts +126 -0
- package/src/index.ts +52 -0
- package/src/schema/contract.ts +76 -0
- package/src/schema/extensions.ts +120 -0
- package/src/schema/image.ts +44 -0
- package/src/schema/index.ts +29 -0
- 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
|
+
};
|