@milkdown/plugin-slash 4.14.2 → 5.1.1

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 (52) hide show
  1. package/README.md +42 -59
  2. package/lib/config.d.ts +20 -2
  3. package/lib/config.d.ts.map +1 -1
  4. package/lib/config.js +112 -81
  5. package/lib/config.js.map +1 -1
  6. package/lib/index.d.ts +22 -16
  7. package/lib/index.d.ts.map +1 -1
  8. package/lib/index.js +13 -18
  9. package/lib/index.js.map +1 -1
  10. package/lib/item.d.ts +2 -5
  11. package/lib/item.d.ts.map +1 -1
  12. package/lib/item.js +0 -2
  13. package/lib/item.js.map +1 -1
  14. package/lib/prose-plugin/dropdown.d.ts +6 -2
  15. package/lib/prose-plugin/dropdown.d.ts.map +1 -1
  16. package/lib/prose-plugin/dropdown.js +16 -23
  17. package/lib/prose-plugin/dropdown.js.map +1 -1
  18. package/lib/prose-plugin/index.d.ts +3 -4
  19. package/lib/prose-plugin/index.d.ts.map +1 -1
  20. package/lib/prose-plugin/index.js +4 -6
  21. package/lib/prose-plugin/index.js.map +1 -1
  22. package/lib/prose-plugin/input.d.ts +1 -2
  23. package/lib/prose-plugin/input.d.ts.map +1 -1
  24. package/lib/prose-plugin/input.js +18 -16
  25. package/lib/prose-plugin/input.js.map +1 -1
  26. package/lib/prose-plugin/props.d.ts +3 -3
  27. package/lib/prose-plugin/props.d.ts.map +1 -1
  28. package/lib/prose-plugin/props.js +21 -27
  29. package/lib/prose-plugin/props.js.map +1 -1
  30. package/lib/prose-plugin/status.d.ts +6 -12
  31. package/lib/prose-plugin/status.d.ts.map +1 -1
  32. package/lib/prose-plugin/status.js +16 -24
  33. package/lib/prose-plugin/status.js.map +1 -1
  34. package/lib/prose-plugin/view.d.ts +1 -2
  35. package/lib/prose-plugin/view.d.ts.map +1 -1
  36. package/lib/prose-plugin/view.js +6 -10
  37. package/lib/prose-plugin/view.js.map +1 -1
  38. package/lib/utility.d.ts +1 -2
  39. package/lib/utility.d.ts.map +1 -1
  40. package/lib/utility.js +0 -1
  41. package/lib/utility.js.map +1 -1
  42. package/package.json +3 -3
  43. package/src/config.ts +134 -82
  44. package/src/index.ts +17 -34
  45. package/src/item.ts +2 -7
  46. package/src/prose-plugin/dropdown.ts +22 -25
  47. package/src/prose-plugin/index.ts +7 -13
  48. package/src/prose-plugin/input.ts +19 -17
  49. package/src/prose-plugin/props.ts +26 -35
  50. package/src/prose-plugin/status.ts +19 -30
  51. package/src/prose-plugin/view.ts +6 -11
  52. package/src/utility.ts +1 -3
package/src/config.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2021, Milkdown by Mirone. */
2
- import { commandsCtx, themeToolCtx } from '@milkdown/core';
2
+ import { commandsCtx, schemaCtx, themeToolCtx } from '@milkdown/core';
3
+ import type { Ctx } from '@milkdown/ctx';
3
4
  import {
4
5
  InsertHr,
5
6
  InsertImage,
@@ -11,86 +12,137 @@ import {
11
12
  WrapInBulletList,
12
13
  WrapInOrderedList,
13
14
  } from '@milkdown/preset-gfm';
15
+ import { EditorState, Node } from '@milkdown/prose';
14
16
 
15
- import type { SlashConfig } from '.';
16
- import { createDropdownItem, nodeExists } from './utility';
17
+ import { WrappedAction } from './item';
18
+ import { createDropdownItem } from './utility';
17
19
 
18
- export const config: SlashConfig = ({ ctx }) => [
19
- {
20
- id: 'h1',
21
- dom: createDropdownItem(ctx.get(themeToolCtx), 'Large Heading', 'h1'),
22
- command: () => ctx.get(commandsCtx).call(TurnIntoHeading, 1),
23
- keyword: ['h1', 'large heading'],
24
- enable: nodeExists('heading'),
25
- },
26
- {
27
- id: 'h2',
28
- dom: createDropdownItem(ctx.get(themeToolCtx), 'Medium Heading', 'h2'),
29
- command: () => ctx.get(commandsCtx).call(TurnIntoHeading, 2),
30
- keyword: ['h2', 'medium heading'],
31
- enable: nodeExists('heading'),
32
- },
33
- {
34
- id: 'h3',
35
- dom: createDropdownItem(ctx.get(themeToolCtx), 'Small Heading', 'h3'),
36
- command: () => ctx.get(commandsCtx).call(TurnIntoHeading, 3),
37
- keyword: ['h3', 'small heading'],
38
- enable: nodeExists('heading'),
39
- },
40
- {
41
- id: 'bulletList',
42
- dom: createDropdownItem(ctx.get(themeToolCtx), 'Bullet List', 'bulletList'),
43
- command: () => ctx.get(commandsCtx).call(WrapInBulletList),
44
- keyword: ['bullet list', 'ul'],
45
- enable: nodeExists('bullet_list'),
46
- },
47
- {
48
- id: 'orderedList',
49
- dom: createDropdownItem(ctx.get(themeToolCtx), 'Ordered List', 'orderedList'),
50
- command: () => ctx.get(commandsCtx).call(WrapInOrderedList),
51
- keyword: ['ordered list', 'ol'],
52
- enable: nodeExists('ordered_list'),
53
- },
54
- {
55
- id: 'taskList',
56
- dom: createDropdownItem(ctx.get(themeToolCtx), 'Task List', 'taskList'),
57
- command: () => ctx.get(commandsCtx).call(TurnIntoTaskList),
58
- keyword: ['task list', 'task'],
59
- enable: nodeExists('task_list_item'),
60
- },
61
- {
62
- id: 'image',
63
- dom: createDropdownItem(ctx.get(themeToolCtx), 'Image', 'image'),
64
- command: () => ctx.get(commandsCtx).call(InsertImage),
65
- keyword: ['image'],
66
- enable: nodeExists('image'),
67
- },
68
- {
69
- id: 'blockquote',
70
- dom: createDropdownItem(ctx.get(themeToolCtx), 'Quote', 'quote'),
71
- command: () => ctx.get(commandsCtx).call(WrapInBlockquote),
72
- keyword: ['quote', 'blockquote'],
73
- enable: nodeExists('blockquote'),
74
- },
75
- {
76
- id: 'table',
77
- dom: createDropdownItem(ctx.get(themeToolCtx), 'Table', 'table'),
78
- command: () => ctx.get(commandsCtx).call(InsertTable),
79
- keyword: ['table'],
80
- enable: nodeExists('table'),
81
- },
82
- {
83
- id: 'code',
84
- dom: createDropdownItem(ctx.get(themeToolCtx), 'Code Fence', 'code'),
85
- command: () => ctx.get(commandsCtx).call(TurnIntoCodeFence),
86
- keyword: ['code'],
87
- enable: nodeExists('fence'),
88
- },
89
- {
90
- id: 'divider',
91
- dom: createDropdownItem(ctx.get(themeToolCtx), 'Divide Line', 'divider'),
92
- command: () => ctx.get(commandsCtx).call(InsertHr),
93
- keyword: ['divider', 'hr'],
94
- enable: nodeExists('hr'),
95
- },
96
- ];
20
+ type Nullable<T> = T | null | undefined;
21
+
22
+ export type StatusConfig = {
23
+ placeholder?: Nullable<string>;
24
+ actions?: Nullable<WrappedAction[]>;
25
+ };
26
+
27
+ export type StatusConfigBuilderParams = {
28
+ content: string;
29
+ isTopLevel: boolean;
30
+ parentNode: Node;
31
+ state: EditorState;
32
+ };
33
+
34
+ export type StatusConfigBuilder = (params: StatusConfigBuilderParams) => Nullable<StatusConfig>;
35
+
36
+ export type Config = (ctx: Ctx) => StatusConfigBuilder;
37
+
38
+ export const defaultActions = (ctx: Ctx, input = '/'): WrappedAction[] => {
39
+ const { nodes } = ctx.get(schemaCtx);
40
+ const actions: Array<WrappedAction & { keyword: string[]; typeName: string }> = [
41
+ {
42
+ id: 'h1',
43
+ dom: createDropdownItem(ctx.get(themeToolCtx), 'Large Heading', 'h1'),
44
+ command: () => ctx.get(commandsCtx).call(TurnIntoHeading, 1),
45
+ keyword: ['h1', 'large heading'],
46
+ typeName: 'heading',
47
+ },
48
+ {
49
+ id: 'h2',
50
+ dom: createDropdownItem(ctx.get(themeToolCtx), 'Medium Heading', 'h2'),
51
+ command: () => ctx.get(commandsCtx).call(TurnIntoHeading, 2),
52
+ keyword: ['h2', 'medium heading'],
53
+ typeName: 'heading',
54
+ },
55
+ {
56
+ id: 'h3',
57
+ dom: createDropdownItem(ctx.get(themeToolCtx), 'Small Heading', 'h3'),
58
+ command: () => ctx.get(commandsCtx).call(TurnIntoHeading, 3),
59
+ keyword: ['h3', 'small heading'],
60
+ typeName: 'heading',
61
+ },
62
+ {
63
+ id: 'bulletList',
64
+ dom: createDropdownItem(ctx.get(themeToolCtx), 'Bullet List', 'bulletList'),
65
+ command: () => ctx.get(commandsCtx).call(WrapInBulletList),
66
+ keyword: ['bullet list', 'ul'],
67
+ typeName: 'bullet_list',
68
+ },
69
+ {
70
+ id: 'orderedList',
71
+ dom: createDropdownItem(ctx.get(themeToolCtx), 'Ordered List', 'orderedList'),
72
+ command: () => ctx.get(commandsCtx).call(WrapInOrderedList),
73
+ keyword: ['ordered list', 'ol'],
74
+ typeName: 'ordered_list',
75
+ },
76
+ {
77
+ id: 'taskList',
78
+ dom: createDropdownItem(ctx.get(themeToolCtx), 'Task List', 'taskList'),
79
+ command: () => ctx.get(commandsCtx).call(TurnIntoTaskList),
80
+ keyword: ['task list', 'task'],
81
+ typeName: 'task_list_item',
82
+ },
83
+ {
84
+ id: 'image',
85
+ dom: createDropdownItem(ctx.get(themeToolCtx), 'Image', 'image'),
86
+ command: () => ctx.get(commandsCtx).call(InsertImage),
87
+ keyword: ['image'],
88
+ typeName: 'image',
89
+ },
90
+ {
91
+ id: 'blockquote',
92
+ dom: createDropdownItem(ctx.get(themeToolCtx), 'Quote', 'quote'),
93
+ command: () => ctx.get(commandsCtx).call(WrapInBlockquote),
94
+ keyword: ['quote', 'blockquote'],
95
+ typeName: 'blockquote',
96
+ },
97
+ {
98
+ id: 'table',
99
+ dom: createDropdownItem(ctx.get(themeToolCtx), 'Table', 'table'),
100
+ command: () => ctx.get(commandsCtx).call(InsertTable),
101
+ keyword: ['table'],
102
+ typeName: 'table',
103
+ },
104
+ {
105
+ id: 'code',
106
+ dom: createDropdownItem(ctx.get(themeToolCtx), 'Code Fence', 'code'),
107
+ command: () => ctx.get(commandsCtx).call(TurnIntoCodeFence),
108
+ keyword: ['code'],
109
+ typeName: 'fence',
110
+ },
111
+ {
112
+ id: 'divider',
113
+ dom: createDropdownItem(ctx.get(themeToolCtx), 'Divide Line', 'divider'),
114
+ command: () => ctx.get(commandsCtx).call(InsertHr),
115
+ keyword: ['divider', 'hr'],
116
+ typeName: 'hr',
117
+ },
118
+ ];
119
+
120
+ const userInput = input.slice(1).toLocaleLowerCase();
121
+
122
+ return actions
123
+ .filter((action) => !!nodes[action.typeName] && action.keyword.some((keyword) => keyword.includes(userInput)))
124
+ .map(({ keyword, typeName, ...action }) => action);
125
+ };
126
+
127
+ export const defaultConfig: Config = (ctx) => {
128
+ return ({ content, isTopLevel }) => {
129
+ if (!isTopLevel) return null;
130
+
131
+ if (!content) {
132
+ return { placeholder: 'Type / to use the slash commands...' };
133
+ }
134
+
135
+ if (content.startsWith('/')) {
136
+ return content === '/'
137
+ ? {
138
+ placeholder: 'Type to filter...',
139
+ actions: defaultActions(ctx),
140
+ }
141
+ : {
142
+ actions: defaultActions(ctx, content),
143
+ };
144
+ }
145
+
146
+ return null;
147
+ };
148
+ };
package/src/index.ts CHANGED
@@ -1,46 +1,29 @@
1
1
  /* Copyright 2021, Milkdown by Mirone. */
2
- import { EditorState, NodeWithPos } from '@milkdown/prose';
3
- import { AtomList, createProsePlugin, Utils } from '@milkdown/utils';
2
+ import { AtomList, createPlugin } from '@milkdown/utils';
4
3
 
5
- import { config } from './config';
6
- import { WrappedAction } from './item';
4
+ import type { Config } from './config';
5
+ import { defaultConfig } from './config';
7
6
  import { createSlashPlugin } from './prose-plugin';
8
- import { CursorStatus } from './prose-plugin/status';
9
7
 
10
- export { config } from './config';
11
- export { CursorStatus } from './prose-plugin/status';
12
- export { createDropdownItem, nodeExists } from './utility';
13
-
14
- export type SlashConfig = (utils: Utils) => WrappedAction[];
8
+ export type { Config, StatusConfig, StatusConfigBuilder, StatusConfigBuilderParams } from './config';
9
+ export { defaultActions, defaultConfig } from './config';
10
+ export { createDropdownItem } from './utility';
15
11
 
16
12
  export type Options = {
17
- shouldDisplay: (parent: NodeWithPos, state: EditorState) => boolean;
18
- config: SlashConfig;
19
- placeholder: {
20
- [CursorStatus.Empty]: string;
21
- [CursorStatus.Slash]: string;
22
- };
13
+ config: Config;
23
14
  };
24
15
 
25
- export const slashPlugin = createProsePlugin<Options>((options, utils) => {
26
- const slashConfig = options?.config ?? config;
27
- const placeholder = {
28
- [CursorStatus.Empty]: 'Type / to use the slash commands...',
29
- [CursorStatus.Slash]: 'Type to filter...',
30
- ...(options?.placeholder ?? {}),
31
- };
32
- const cfg = slashConfig(utils);
33
- const shouldDisplay =
34
- options?.shouldDisplay ??
35
- ((parent, state) => {
36
- const isTopLevel = state.selection.$from.depth === 1;
37
- return parent.node.childCount <= 1 && isTopLevel;
38
- });
39
-
40
- const plugin = createSlashPlugin(utils, cfg, placeholder, shouldDisplay);
16
+ export const slashPlugin = createPlugin<string, Options>((utils, options) => {
17
+ const slashConfig = options?.config ?? defaultConfig;
18
+
41
19
  return {
42
- id: 'slash',
43
- plugin,
20
+ prosePlugins: (_, ctx) => {
21
+ const config = slashConfig(ctx);
22
+
23
+ const plugin = createSlashPlugin(utils, config);
24
+
25
+ return [plugin];
26
+ },
44
27
  };
45
28
  });
46
29
 
package/src/item.ts CHANGED
@@ -1,26 +1,21 @@
1
1
  /* Copyright 2021, Milkdown by Mirone. */
2
- import type { Command, Schema } from '@milkdown/prose';
2
+ import type { Command } from '@milkdown/prose';
3
3
 
4
4
  import { cleanUpAndCreateNode } from './utility';
5
5
 
6
6
  export type Action = {
7
7
  id: string;
8
8
  $: HTMLElement;
9
- keyword: string[];
10
9
  command: Command;
11
- enable: (schema: Schema) => boolean;
12
10
  };
13
11
 
14
- export type WrappedAction = Pick<Action, 'keyword' | 'id'> & {
15
- enable: (schema: Schema) => boolean;
12
+ export type WrappedAction = Pick<Action, 'id'> & {
16
13
  command: () => void;
17
14
  dom: HTMLElement;
18
15
  };
19
16
 
20
17
  export const transformAction = (action: WrappedAction): Action => ({
21
18
  id: action.id,
22
- keyword: action.keyword,
23
19
  $: action.dom,
24
20
  command: cleanUpAndCreateNode(action.command),
25
- enable: action.enable,
26
21
  });
@@ -1,44 +1,41 @@
1
1
  /* Copyright 2021, Milkdown by Mirone. */
2
2
  import scrollIntoView from 'smooth-scroll-into-view-if-needed';
3
3
 
4
- import { Action } from '../item';
5
4
  import { Status } from './status';
6
5
 
7
- export const renderDropdown = (status: Status, dropdownElement: HTMLElement, items: Action[]): boolean => {
8
- const { filter } = status.get();
6
+ type Listeners = {
7
+ mouseEnter: EventListener;
8
+ mouseLeave: EventListener;
9
+ };
10
+
11
+ export const renderDropdown = (status: Status, dropdownElement: HTMLElement, listeners: Listeners): boolean => {
12
+ const { actions } = status.get();
9
13
 
10
- if (!status.isSlash()) {
14
+ if (!actions.length) {
11
15
  dropdownElement.classList.add('hide');
12
16
  return false;
13
17
  }
14
18
 
15
- const activeList = items
16
- .filter((item) => {
17
- item.$.classList.remove('active');
18
- const result = item.keyword.some((key) => key.includes(filter.toLocaleLowerCase()));
19
- if (result) {
20
- return true;
21
- }
22
- item.$.classList.add('hide');
23
- return false;
24
- })
25
- .map((item) => {
26
- item.$.classList.remove('hide');
27
- return item;
28
- });
19
+ dropdownElement.childNodes.forEach((child) => {
20
+ child.removeEventListener('mouseenter', listeners.mouseEnter);
21
+ child.removeEventListener('mouseleave', listeners.mouseLeave);
22
+ });
29
23
 
30
- status.setActions(activeList);
24
+ // Reset dropdownElement children
25
+ dropdownElement.textContent = '';
31
26
 
32
- if (activeList.length === 0) {
33
- dropdownElement.classList.add('hide');
34
- return false;
35
- }
27
+ actions.forEach(({ $ }) => {
28
+ $.classList.remove('active');
29
+ $.addEventListener('mouseenter', listeners.mouseEnter);
30
+ $.addEventListener('mouseleave', listeners.mouseLeave);
31
+ dropdownElement.appendChild($);
32
+ });
36
33
 
37
34
  dropdownElement.classList.remove('hide');
38
35
 
39
- activeList[0].$.classList.add('active');
36
+ actions[0].$.classList.add('active');
40
37
  requestAnimationFrame(() => {
41
- scrollIntoView(activeList[0].$, {
38
+ scrollIntoView(actions[0].$, {
42
39
  scrollMode: 'if-needed',
43
40
  block: 'nearest',
44
41
  inline: 'nearest',
@@ -1,26 +1,20 @@
1
1
  /* Copyright 2021, Milkdown by Mirone. */
2
- import { EditorState, NodeWithPos, Plugin, PluginKey } from '@milkdown/prose';
2
+ import { Plugin, PluginKey } from '@milkdown/prose';
3
3
  import { Utils } from '@milkdown/utils';
4
4
 
5
- import { transformAction, WrappedAction } from '../item';
5
+ import type { StatusConfigBuilder } from '..';
6
6
  import { createProps } from './props';
7
- import { createStatus, CursorStatus } from './status';
7
+ import { createStatus } from './status';
8
8
  import { createView } from './view';
9
9
 
10
10
  export const key = 'MILKDOWN_PLUGIN_SLASH';
11
11
 
12
- export const createSlashPlugin = (
13
- utils: Utils,
14
- items: WrappedAction[],
15
- placeholder: Record<CursorStatus, string>,
16
- shouldDisplay: (parent: NodeWithPos, state: EditorState) => boolean,
17
- ) => {
18
- const status = createStatus();
19
- const actions = items.map(transformAction);
12
+ export const createSlashPlugin = (utils: Utils, builder: StatusConfigBuilder) => {
13
+ const status = createStatus(builder);
20
14
 
21
15
  return new Plugin({
22
16
  key: new PluginKey(key),
23
- props: createProps(status, utils, placeholder, shouldDisplay),
24
- view: (view) => createView(status, actions, view, utils),
17
+ props: createProps(status, utils),
18
+ view: (view) => createView(status, view, utils),
25
19
  });
26
20
  };
@@ -3,7 +3,6 @@
3
3
  import { EditorView } from '@milkdown/prose';
4
4
  import scrollIntoView from 'smooth-scroll-into-view-if-needed';
5
5
 
6
- import { Action } from '../item';
7
6
  import { Status } from './status';
8
7
 
9
8
  export const createMouseManager = () => {
@@ -27,9 +26,10 @@ export const handleMouseMove = (mouseManager: MouseManager) => () => {
27
26
 
28
27
  export const handleMouseEnter = (status: Status, mouseManager: MouseManager) => (e: MouseEvent) => {
29
28
  if (mouseManager.isLock()) return;
30
- const active = status.get().activeActions.findIndex((x) => x.$.classList.contains('active'));
29
+ const { actions } = status.get();
30
+ const active = actions.findIndex((x) => x.$.classList.contains('active'));
31
31
  if (active >= 0) {
32
- status.get().activeActions[active].$.classList.remove('active');
32
+ actions[active].$.classList.remove('active');
33
33
  }
34
34
  const { target } = e;
35
35
  if (!(target instanceof HTMLElement)) return;
@@ -43,7 +43,7 @@ export const handleMouseLeave = () => (e: MouseEvent) => {
43
43
  };
44
44
 
45
45
  export const handleClick =
46
- (status: Status, items: Action[], view: EditorView, dropdownElement: HTMLElement) =>
46
+ (status: Status, view: EditorView, dropdownElement: HTMLElement) =>
47
47
  (e: Event): void => {
48
48
  const { target } = e;
49
49
  if (!(target instanceof HTMLElement)) return;
@@ -54,11 +54,13 @@ export const handleClick =
54
54
  e.preventDefault();
55
55
  };
56
56
 
57
- const el = Object.values(items).find(({ $ }) => $.contains(target));
57
+ const { actions } = status.get();
58
+
59
+ const el = Object.values(actions).find(({ $ }) => $.contains(target));
58
60
  if (!el) {
59
61
  if (status.isEmpty()) return;
60
62
 
61
- status.clearStatus();
63
+ status.clear();
62
64
  dropdownElement.classList.add('hide');
63
65
  stop();
64
66
 
@@ -75,18 +77,18 @@ export const handleKeydown =
75
77
  if (!mouseManager.isLock()) mouseManager.lock();
76
78
 
77
79
  const { key } = e;
78
- if (!status.isSlash()) return;
80
+ if (status.isEmpty()) return;
79
81
  if (!['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(key)) return;
80
82
 
81
- const { activeActions } = status.get();
83
+ const { actions } = status.get();
82
84
 
83
- let active = activeActions.findIndex(({ $ }) => $.classList.contains('active'));
85
+ let active = actions.findIndex(({ $ }) => $.classList.contains('active'));
84
86
  if (active < 0) active = 0;
85
87
 
86
88
  const moveActive = (next: number) => {
87
- activeActions[active].$.classList.remove('active');
88
- activeActions[next].$.classList.add('active');
89
- scrollIntoView(activeActions[next].$, {
89
+ actions[active].$.classList.remove('active');
90
+ actions[next].$.classList.add('active');
91
+ scrollIntoView(actions[next].$, {
90
92
  scrollMode: 'if-needed',
91
93
  block: 'nearest',
92
94
  inline: 'nearest',
@@ -94,14 +96,14 @@ export const handleKeydown =
94
96
  };
95
97
 
96
98
  if (key === 'ArrowDown') {
97
- const next = active === activeActions.length - 1 ? 0 : active + 1;
99
+ const next = active === actions.length - 1 ? 0 : active + 1;
98
100
 
99
101
  moveActive(next);
100
102
  return;
101
103
  }
102
104
 
103
105
  if (key === 'ArrowUp') {
104
- const next = active === 0 ? activeActions.length - 1 : active - 1;
106
+ const next = active === 0 ? actions.length - 1 : active - 1;
105
107
 
106
108
  moveActive(next);
107
109
  return;
@@ -110,11 +112,11 @@ export const handleKeydown =
110
112
  if (key === 'Escape') {
111
113
  if (status.isEmpty()) return;
112
114
 
113
- status.clearStatus();
115
+ status.clear();
114
116
  dropdownElement.classList.add('hide');
115
117
  return;
116
118
  }
117
119
 
118
- activeActions[active].command(view.state, view.dispatch, view);
119
- activeActions[active].$.classList.remove('active');
120
+ actions[active].command(view.state, view.dispatch, view);
121
+ actions[active].$.classList.remove('active');
120
122
  };
@@ -1,10 +1,10 @@
1
1
  /* Copyright 2021, Milkdown by Mirone. */
2
2
  import { css } from '@emotion/css';
3
3
  import { ThemeTool } from '@milkdown/core';
4
- import { Decoration, DecorationSet, EditorState, EditorView, findParentNode, NodeWithPos } from '@milkdown/prose';
4
+ import { Decoration, DecorationSet, EditorState, EditorView, findParentNode } from '@milkdown/prose';
5
5
  import { Utils } from '@milkdown/utils';
6
6
 
7
- import { CursorStatus, Status } from './status';
7
+ import type { Status } from './status';
8
8
 
9
9
  export type Props = ReturnType<typeof createProps>;
10
10
 
@@ -29,19 +29,13 @@ const createSlashStyle = () => css`
29
29
  }
30
30
  `;
31
31
 
32
- export const createProps = (
33
- status: Status,
34
- utils: Utils,
35
- placeholder: Record<CursorStatus, string>,
36
- shouldDisplay: (parent: NodeWithPos, state: EditorState) => boolean,
37
- ) => {
32
+ export const createProps = (status: Status, utils: Utils) => {
38
33
  const emptyStyle = utils.getStyle(createEmptyStyle);
39
34
  const slashStyle = utils.getStyle(createSlashStyle);
40
35
 
41
36
  return {
42
37
  handleKeyDown: (_: EditorView, event: Event) => {
43
- const { cursorStatus, activeActions } = status.get();
44
- if (cursorStatus !== CursorStatus.Slash || activeActions.length === 0) {
38
+ if (status.isEmpty()) {
45
39
  return false;
46
40
  }
47
41
  if (!(event instanceof KeyboardEvent)) {
@@ -55,46 +49,43 @@ export const createProps = (
55
49
  return true;
56
50
  },
57
51
  decorations: (state: EditorState) => {
58
- const parent = findParentNode(({ type }) => type.name === 'paragraph')(state.selection);
52
+ const paragraph = findParentNode(({ type }) => type.name === 'paragraph')(state.selection);
59
53
 
60
- if (!parent || !shouldDisplay(parent, state)) {
61
- status.clearStatus();
54
+ if (
55
+ !paragraph ||
56
+ paragraph.node.childCount > 1 ||
57
+ state.selection.$from.parentOffset !== paragraph.node.textContent.length
58
+ ) {
59
+ status.clear();
62
60
  return;
63
61
  }
64
62
 
65
- const isEmpty = parent.node.content.size === 0;
66
- const isSlash = parent.node.textContent === '/' && state.selection.$from.parentOffset > 0;
67
- const isSearch = parent.node.textContent.startsWith('/') && state.selection.$from.parentOffset > 1;
63
+ const { placeholder, actions } = status.update({
64
+ parentNode: state.selection.$from.node(state.selection.$from.depth - 1),
65
+ isTopLevel: state.selection.$from.depth === 1,
66
+ content: paragraph.node.textContent,
67
+ state,
68
+ });
69
+
70
+ if (!placeholder) {
71
+ return null;
72
+ }
68
73
 
69
74
  const createDecoration = (text: string, className: (string | undefined)[]) => {
70
- const pos = parent.pos;
75
+ const pos = paragraph.pos;
71
76
  return DecorationSet.create(state.doc, [
72
- Decoration.node(pos, pos + parent.node.nodeSize, {
77
+ Decoration.node(pos, pos + paragraph.node.nodeSize, {
73
78
  class: className.filter((x) => x).join(' '),
74
79
  'data-text': text,
75
80
  }),
76
81
  ]);
77
82
  };
78
83
 
79
- if (isEmpty) {
80
- status.clearStatus();
81
- const text = placeholder[CursorStatus.Empty];
82
- return createDecoration(text, [emptyStyle, 'empty-node']);
83
- }
84
-
85
- if (isSlash) {
86
- status.setSlash();
87
- const text = placeholder[CursorStatus.Slash];
88
- return createDecoration(text, [emptyStyle, slashStyle, 'empty-node', 'is-slash']);
89
- }
90
-
91
- if (isSearch) {
92
- status.setSlash(parent.node.textContent.slice(1));
93
- return null;
84
+ if (actions.length) {
85
+ return createDecoration(placeholder, [emptyStyle, slashStyle, 'empty-node', 'is-slash']);
94
86
  }
95
87
 
96
- status.clearStatus();
97
- return null;
88
+ return createDecoration(placeholder, [emptyStyle, 'empty-node']);
98
89
  },
99
90
  };
100
91
  };