@use-kona/editor 0.1.16 → 0.1.17
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/plugins/CommandsPlugin/CommandsPlugin.d.ts +2 -3
- package/dist/plugins/CommandsPlugin/CommandsPlugin.js +5 -15
- package/dist/plugins/CommandsPlugin/Menu.d.ts +3 -3
- package/dist/plugins/CommandsPlugin/Menu.js +240 -69
- package/dist/plugins/CommandsPlugin/index.d.ts +1 -1
- package/dist/plugins/CommandsPlugin/resolveCommands.d.ts +34 -0
- package/dist/plugins/CommandsPlugin/resolveCommands.js +150 -0
- package/dist/plugins/CommandsPlugin/resolveCommands.spec.d.ts +1 -0
- package/dist/plugins/CommandsPlugin/resolveCommands.spec.js +277 -0
- package/dist/plugins/CommandsPlugin/styles.module.js +5 -0
- package/dist/plugins/CommandsPlugin/styles_module.css +47 -7
- package/dist/plugins/CommandsPlugin/types.d.ts +14 -2
- package/dist/plugins/CommandsPlugin/useResolvedCommands.d.ts +12 -0
- package/dist/plugins/CommandsPlugin/useResolvedCommands.js +46 -0
- package/dist/plugins/DnDPlugin/DnDPlugin.d.ts +7 -0
- package/dist/plugins/DnDPlugin/DnDPlugin.js +34 -7
- package/dist/plugins/NodeIdPlugin/NodeIdPlugin.js +6 -1
- package/dist/plugins/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/plugins/CommandsPlugin/CommandsPlugin.tsx +5 -25
- package/src/plugins/CommandsPlugin/Menu.tsx +260 -86
- package/src/plugins/CommandsPlugin/index.ts +1 -1
- package/src/plugins/CommandsPlugin/resolveCommands.spec.ts +261 -0
- package/src/plugins/CommandsPlugin/resolveCommands.ts +275 -0
- package/src/plugins/CommandsPlugin/styles.module.css +49 -7
- package/src/plugins/CommandsPlugin/types.ts +17 -3
- package/src/plugins/CommandsPlugin/useResolvedCommands.ts +67 -0
- package/src/plugins/DnDPlugin/DnDPlugin.tsx +64 -9
- package/src/plugins/NodeIdPlugin/NodeIdPlugin.ts +10 -2
- package/src/plugins/index.ts +6 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useStore } from '@nanostores/react';
|
|
2
2
|
import cn from 'clsx';
|
|
3
3
|
import type { MapStore } from 'nanostores';
|
|
4
|
-
import
|
|
4
|
+
import {
|
|
5
5
|
type CSSProperties,
|
|
6
6
|
type ReactNode,
|
|
7
7
|
useEffect,
|
|
@@ -16,31 +16,56 @@ import { useFocused, useSlate, useSlateSelection } from 'slate-react';
|
|
|
16
16
|
import type { CustomElement } from '../../../types';
|
|
17
17
|
import { insert, insertText, removeCommand, set, wrap } from './actions';
|
|
18
18
|
import styles from './styles.module.css';
|
|
19
|
-
import type { Command, CommandsStore } from './types';
|
|
19
|
+
import type { Command, CommandPathEntry, CommandsStore } from './types';
|
|
20
|
+
import { useResolvedCommands } from './useResolvedCommands';
|
|
20
21
|
|
|
21
22
|
type Props = {
|
|
22
23
|
$store: MapStore<CommandsStore>;
|
|
23
|
-
|
|
24
|
+
rootCommands: Command[];
|
|
24
25
|
ignoreNodes?: string[];
|
|
25
26
|
renderMenu: (children: ReactNode) => ReactNode;
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
export const Menu = (props: Props) => {
|
|
29
|
-
const {
|
|
30
|
+
const { rootCommands, $store, renderMenu, ignoreNodes = [] } = props;
|
|
30
31
|
const store = useStore($store);
|
|
31
32
|
const [style, setStyle] = useState<CSSProperties | undefined>({});
|
|
32
33
|
const [active, setActive] = useState(0);
|
|
33
|
-
const
|
|
34
|
+
const [path, setPath] = useState<CommandPathEntry[]>([]);
|
|
35
|
+
const refs = useRef<Record<number, HTMLButtonElement | null>>({});
|
|
34
36
|
|
|
35
37
|
const selection = useSlateSelection();
|
|
36
38
|
const editor = useSlate() as Editor;
|
|
37
39
|
const isFocused = useFocused();
|
|
38
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
40
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
39
41
|
|
|
40
42
|
const entry = Editor.above<CustomElement>(editor, {
|
|
41
43
|
match: (n) => Editor.isBlock(editor, n as CustomElement),
|
|
42
44
|
});
|
|
43
45
|
|
|
46
|
+
const isBrowseMode = typeof store.filter === 'string' && store.filter === '';
|
|
47
|
+
const isSearchMode = typeof store.filter === 'string' && store.filter !== '';
|
|
48
|
+
|
|
49
|
+
const { commands, isLoading, isError } = useResolvedCommands({
|
|
50
|
+
rootCommands,
|
|
51
|
+
filter: store.filter,
|
|
52
|
+
path,
|
|
53
|
+
editor,
|
|
54
|
+
isOpen: store.isOpen,
|
|
55
|
+
});
|
|
56
|
+
const entries = useMemo(() => {
|
|
57
|
+
const commandEntries = commands.map((command) => ({
|
|
58
|
+
type: 'command' as const,
|
|
59
|
+
command,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
if (isBrowseMode && path.length > 0) {
|
|
63
|
+
return [{ type: 'back' as const }, ...commandEntries];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return commandEntries;
|
|
67
|
+
}, [commands, isBrowseMode, path.length]);
|
|
68
|
+
|
|
44
69
|
// biome-ignore lint/correctness/useExhaustiveDependencies: we care only about those deps
|
|
45
70
|
const actions = useMemo(() => {
|
|
46
71
|
return {
|
|
@@ -50,7 +75,33 @@ export const Menu = (props: Props) => {
|
|
|
50
75
|
wrap: wrap(editor, selection, store.filter),
|
|
51
76
|
insertText: insertText(editor),
|
|
52
77
|
};
|
|
53
|
-
}, [
|
|
78
|
+
}, [selection, store.filter]);
|
|
79
|
+
|
|
80
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: reset on open session changes
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!store.isOpen) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
setPath([]);
|
|
86
|
+
setActive(0);
|
|
87
|
+
}, [store.isOpen, store.openId]);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (store.filter === false || isSearchMode) {
|
|
91
|
+
setActive(0);
|
|
92
|
+
}
|
|
93
|
+
}, [isSearchMode, store.filter]);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!entries.length) {
|
|
97
|
+
setActive(0);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (active > entries.length - 1) {
|
|
102
|
+
setActive(entries.length - 1);
|
|
103
|
+
}
|
|
104
|
+
}, [active, entries]);
|
|
54
105
|
|
|
55
106
|
// biome-ignore lint/correctness/useExhaustiveDependencies: we care only about those deps
|
|
56
107
|
useLayoutEffect(() => {
|
|
@@ -61,55 +112,129 @@ export const Menu = (props: Props) => {
|
|
|
61
112
|
return;
|
|
62
113
|
}
|
|
63
114
|
|
|
64
|
-
|
|
115
|
+
const frame = window.requestAnimationFrame(() => {
|
|
65
116
|
const domSelection = window.getSelection();
|
|
66
|
-
const domRange =
|
|
117
|
+
const domRange =
|
|
118
|
+
domSelection && domSelection.rangeCount > 0
|
|
119
|
+
? domSelection.getRangeAt(0)
|
|
120
|
+
: null;
|
|
67
121
|
const rect = domRange?.getBoundingClientRect();
|
|
122
|
+
const x = (rect?.left || 0) + (rect?.width || 0) / 2;
|
|
123
|
+
const menuHeight = ref.current?.offsetHeight || 0;
|
|
124
|
+
let y = (rect?.top || 0) + (rect?.height || 0) + 2;
|
|
68
125
|
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
opacity: 1,
|
|
72
|
-
transform: 'scale(1)',
|
|
73
|
-
top: `${(rect?.top || 0) + window.scrollY + (rect?.height || 0) + 2}px`,
|
|
74
|
-
left: `${(rect?.left || 0) + window.scrollX + (rect?.width || 0) / 2}px`,
|
|
75
|
-
});
|
|
76
|
-
} else {
|
|
77
|
-
setStyle({
|
|
78
|
-
opacity: 0,
|
|
79
|
-
transform: 'scale(0.9)',
|
|
80
|
-
});
|
|
126
|
+
if (menuHeight > 0 && y + menuHeight >= window.innerHeight) {
|
|
127
|
+
y = Math.max(8, (rect?.top || 0) - menuHeight - 2);
|
|
81
128
|
}
|
|
82
|
-
}, 0);
|
|
83
129
|
|
|
130
|
+
const transform = `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0) ${store.isOpen ? 'scale(1)' : 'scale(0.95)'}`;
|
|
131
|
+
|
|
132
|
+
setStyle({
|
|
133
|
+
opacity: store.isOpen ? 1 : 0,
|
|
134
|
+
transform,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return () => {
|
|
139
|
+
window.cancelAnimationFrame(frame);
|
|
140
|
+
};
|
|
141
|
+
}, [selection, commands.length, isLoading, isError, store.isOpen, isFocused]);
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
84
144
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
145
|
+
if (!store.isOpen) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
85
149
|
switch (event.key) {
|
|
86
150
|
case 'ArrowDown': {
|
|
151
|
+
if (!entries.length) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
87
155
|
event.preventDefault();
|
|
156
|
+
event.stopPropagation();
|
|
88
157
|
setActive((active) => {
|
|
89
|
-
const
|
|
90
|
-
refs.current[
|
|
91
|
-
|
|
92
|
-
return newActive;
|
|
158
|
+
const nextActive = active >= entries.length - 1 ? 0 : active + 1;
|
|
159
|
+
refs.current[nextActive]?.scrollIntoView({ block: 'nearest' });
|
|
160
|
+
return nextActive;
|
|
93
161
|
});
|
|
94
162
|
break;
|
|
95
163
|
}
|
|
96
164
|
case 'ArrowUp': {
|
|
165
|
+
if (!entries.length) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
97
169
|
event.preventDefault();
|
|
170
|
+
event.stopPropagation();
|
|
98
171
|
setActive((active) => {
|
|
99
|
-
const
|
|
100
|
-
refs.current[
|
|
101
|
-
|
|
102
|
-
return newActive;
|
|
172
|
+
const nextActive = active <= 0 ? entries.length - 1 : active - 1;
|
|
173
|
+
refs.current[nextActive]?.scrollIntoView({ block: 'nearest' });
|
|
174
|
+
return nextActive;
|
|
103
175
|
});
|
|
104
176
|
break;
|
|
105
177
|
}
|
|
178
|
+
case 'ArrowRight': {
|
|
179
|
+
const entry = entries[active];
|
|
180
|
+
if (!entry || entry.type !== 'command' || !entry.command.isSubmenu) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
event.stopPropagation();
|
|
186
|
+
setPath(entry.command.path);
|
|
187
|
+
if (!isBrowseMode) {
|
|
188
|
+
$store.setKey('filter', '');
|
|
189
|
+
}
|
|
190
|
+
setActive(0);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case 'ArrowLeft': {
|
|
194
|
+
if (!isBrowseMode || !path.length) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
event.stopPropagation();
|
|
200
|
+
setPath((path) => path.slice(0, path.length - 1));
|
|
201
|
+
setActive(0);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
106
204
|
case 'Enter': {
|
|
205
|
+
const entry = entries[active];
|
|
206
|
+
if (!entry) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
107
210
|
event.preventDefault();
|
|
108
|
-
|
|
211
|
+
event.stopPropagation();
|
|
212
|
+
|
|
213
|
+
if (entry.type === 'back' && isBrowseMode) {
|
|
214
|
+
setPath((path) => path.slice(0, path.length - 1));
|
|
215
|
+
setActive(0);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (entry.type !== 'command') {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (entry.command.isSubmenu) {
|
|
224
|
+
setPath(entry.command.path);
|
|
225
|
+
if (!isBrowseMode) {
|
|
226
|
+
$store.setKey('filter', '');
|
|
227
|
+
}
|
|
228
|
+
setActive(0);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
entry.command.command.action?.(actions, editor);
|
|
109
233
|
$store.setKey('isOpen', false);
|
|
110
234
|
break;
|
|
111
235
|
}
|
|
112
236
|
case 'Escape': {
|
|
237
|
+
event.preventDefault();
|
|
113
238
|
event.stopPropagation();
|
|
114
239
|
$store.setKey('isOpen', false);
|
|
115
240
|
break;
|
|
@@ -117,46 +242,25 @@ export const Menu = (props: Props) => {
|
|
|
117
242
|
}
|
|
118
243
|
};
|
|
119
244
|
|
|
120
|
-
|
|
121
|
-
document.addEventListener('keydown', handleKeyDown);
|
|
122
|
-
}
|
|
245
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
123
246
|
|
|
124
247
|
return () => {
|
|
125
248
|
document.removeEventListener('keydown', handleKeyDown);
|
|
126
249
|
};
|
|
127
|
-
}, [
|
|
250
|
+
}, [
|
|
251
|
+
actions,
|
|
252
|
+
active,
|
|
253
|
+
entries,
|
|
254
|
+
editor,
|
|
255
|
+
isBrowseMode,
|
|
256
|
+
path.length,
|
|
257
|
+
store.isOpen,
|
|
258
|
+
$store,
|
|
259
|
+
]);
|
|
128
260
|
|
|
129
|
-
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
if (active < 0) {
|
|
132
|
-
setActive(0);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
261
|
+
const hasRows = entries.length > 0 || isLoading || isError;
|
|
135
262
|
|
|
136
|
-
|
|
137
|
-
setActive(commands.length - 1);
|
|
138
|
-
}
|
|
139
|
-
}, [store.filter]);
|
|
140
|
-
|
|
141
|
-
useLayoutEffect(() => {
|
|
142
|
-
const element = ref.current;
|
|
143
|
-
if (element) {
|
|
144
|
-
const { height, top } = element.getBoundingClientRect();
|
|
145
|
-
|
|
146
|
-
const domSelection = window.getSelection();
|
|
147
|
-
const domRange = domSelection?.getRangeAt(0);
|
|
148
|
-
const rect = domRange?.getBoundingClientRect();
|
|
149
|
-
|
|
150
|
-
if (top + height >= window.innerHeight) {
|
|
151
|
-
setStyle((style) => ({
|
|
152
|
-
...style,
|
|
153
|
-
top: `${top - height - (rect?.height ?? 22)}px`,
|
|
154
|
-
}));
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}, []);
|
|
158
|
-
|
|
159
|
-
if (!commands.length) {
|
|
263
|
+
if (store.filter === false || !hasRows) {
|
|
160
264
|
return null;
|
|
161
265
|
}
|
|
162
266
|
|
|
@@ -164,6 +268,8 @@ export const Menu = (props: Props) => {
|
|
|
164
268
|
return null;
|
|
165
269
|
}
|
|
166
270
|
|
|
271
|
+
const pathLabel = path.map((item) => item.title).join(' / ');
|
|
272
|
+
|
|
167
273
|
return createPortal(
|
|
168
274
|
renderMenu(
|
|
169
275
|
<>
|
|
@@ -183,28 +289,96 @@ export const Menu = (props: Props) => {
|
|
|
183
289
|
event.preventDefault();
|
|
184
290
|
}}
|
|
185
291
|
>
|
|
186
|
-
{
|
|
187
|
-
<
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
292
|
+
{isBrowseMode && pathLabel && (
|
|
293
|
+
<div className={styles.path}>{pathLabel}</div>
|
|
294
|
+
)}
|
|
295
|
+
{entries.map((entry, index) => {
|
|
296
|
+
if (entry.type === 'back') {
|
|
297
|
+
return (
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
ref={(element) => {
|
|
301
|
+
refs.current[index] = element;
|
|
302
|
+
}}
|
|
303
|
+
key="back"
|
|
304
|
+
className={cn(styles.button, {
|
|
305
|
+
[styles.active]: index === active,
|
|
306
|
+
})}
|
|
307
|
+
onMouseDown={(event) => {
|
|
308
|
+
event.preventDefault();
|
|
309
|
+
setPath((path) => path.slice(0, path.length - 1));
|
|
310
|
+
setActive(0);
|
|
311
|
+
}}
|
|
312
|
+
>
|
|
313
|
+
<span className={styles.icon}>...</span>
|
|
314
|
+
<span className={styles.content}>
|
|
315
|
+
<span>...</span>
|
|
316
|
+
</span>
|
|
317
|
+
</button>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<button
|
|
323
|
+
type="button"
|
|
324
|
+
ref={(element) => {
|
|
191
325
|
refs.current[index] = element;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
326
|
+
}}
|
|
327
|
+
key={entry.command.key}
|
|
328
|
+
className={cn(styles.button, {
|
|
329
|
+
[styles.active]: index === active,
|
|
330
|
+
})}
|
|
331
|
+
onMouseDown={(event) => {
|
|
332
|
+
event.preventDefault();
|
|
333
|
+
if (entry.command.isSubmenu) {
|
|
334
|
+
setPath(entry.command.path);
|
|
335
|
+
if (!isBrowseMode) {
|
|
336
|
+
$store.setKey('filter', '');
|
|
337
|
+
}
|
|
338
|
+
setActive(0);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
entry.command.command.action?.(actions, editor);
|
|
343
|
+
$store.setKey('isOpen', false);
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
346
|
+
<span className={styles.icon}>
|
|
347
|
+
{entry.command.command.icon}
|
|
348
|
+
</span>
|
|
349
|
+
<span className={styles.content}>
|
|
350
|
+
<span>{entry.command.command.title}</span>
|
|
351
|
+
{isSearchMode && entry.command.breadcrumb && (
|
|
352
|
+
<span className={styles.breadcrumb}>
|
|
353
|
+
{entry.command.breadcrumb}
|
|
354
|
+
</span>
|
|
355
|
+
)}
|
|
356
|
+
</span>
|
|
357
|
+
{entry.command.isSubmenu && isBrowseMode && (
|
|
358
|
+
<span className={styles.submenu} aria-hidden="true">
|
|
359
|
+
<svg
|
|
360
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
361
|
+
width="12"
|
|
362
|
+
height="12"
|
|
363
|
+
viewBox="0 0 24 24"
|
|
364
|
+
fill="none"
|
|
365
|
+
stroke="currentColor"
|
|
366
|
+
strokeWidth="2"
|
|
367
|
+
strokeLinecap="round"
|
|
368
|
+
strokeLinejoin="round"
|
|
369
|
+
>
|
|
370
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
371
|
+
<path d="M9 6l6 6l-6 6" />
|
|
372
|
+
</svg>
|
|
373
|
+
</span>
|
|
374
|
+
)}
|
|
375
|
+
</button>
|
|
376
|
+
);
|
|
377
|
+
})}
|
|
378
|
+
{isLoading && <div className={styles.systemRow}>Loading...</div>}
|
|
379
|
+
{isError && (
|
|
380
|
+
<div className={styles.systemRow}>Could not load commands</div>
|
|
381
|
+
)}
|
|
208
382
|
</div>
|
|
209
383
|
</>,
|
|
210
384
|
),
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { CommandsPlugin } from './CommandsPlugin';
|
|
2
|
-
export type { Command } from './types';
|
|
2
|
+
export type { Command, CommandPathEntry, GetCommandsContext } from './types';
|