ckeditor5-elements-menu 0.0.0-stage → 49.0.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 (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -2
  3. package/dist/augmentation.d.ts +30 -0
  4. package/dist/augmentation.d.ts.map +1 -0
  5. package/dist/augmentation.js +2 -0
  6. package/dist/augmentation.js.map +1 -0
  7. package/dist/breadcrumbs/breadcrumbItemView.d.ts +47 -0
  8. package/dist/breadcrumbs/breadcrumbItemView.d.ts.map +1 -0
  9. package/dist/breadcrumbs/breadcrumbItemView.js +30 -0
  10. package/dist/breadcrumbs/breadcrumbItemView.js.map +1 -0
  11. package/dist/breadcrumbs/breadcrumbLabelView.d.ts +29 -0
  12. package/dist/breadcrumbs/breadcrumbLabelView.d.ts.map +1 -0
  13. package/dist/breadcrumbs/breadcrumbLabelView.js +39 -0
  14. package/dist/breadcrumbs/breadcrumbLabelView.js.map +1 -0
  15. package/dist/breadcrumbs/breadcrumbsView.d.ts +68 -0
  16. package/dist/breadcrumbs/breadcrumbsView.d.ts.map +1 -0
  17. package/dist/breadcrumbs/breadcrumbsView.js +134 -0
  18. package/dist/breadcrumbs/breadcrumbsView.js.map +1 -0
  19. package/dist/breadcrumbs/elementBreadcrumbs.d.ts +73 -0
  20. package/dist/breadcrumbs/elementBreadcrumbs.d.ts.map +1 -0
  21. package/dist/breadcrumbs/elementBreadcrumbs.js +225 -0
  22. package/dist/breadcrumbs/elementBreadcrumbs.js.map +1 -0
  23. package/dist/contextMenu/contextMenuObserver.d.ts +19 -0
  24. package/dist/contextMenu/contextMenuObserver.d.ts.map +1 -0
  25. package/dist/contextMenu/contextMenuObserver.js +18 -0
  26. package/dist/contextMenu/contextMenuObserver.js.map +1 -0
  27. package/dist/contextMenu/elementContextMenu.d.ts +34 -0
  28. package/dist/contextMenu/elementContextMenu.d.ts.map +1 -0
  29. package/dist/contextMenu/elementContextMenu.js +88 -0
  30. package/dist/contextMenu/elementContextMenu.js.map +1 -0
  31. package/dist/domTarget.d.ts +8 -0
  32. package/dist/domTarget.d.ts.map +1 -0
  33. package/dist/domTarget.js +20 -0
  34. package/dist/domTarget.js.map +1 -0
  35. package/dist/elementMenu.d.ts +93 -0
  36. package/dist/elementMenu.d.ts.map +1 -0
  37. package/dist/elementMenu.js +140 -0
  38. package/dist/elementMenu.js.map +1 -0
  39. package/dist/elementMenuView.d.ts +40 -0
  40. package/dist/elementMenuView.d.ts.map +1 -0
  41. package/dist/elementMenuView.js +143 -0
  42. package/dist/elementMenuView.js.map +1 -0
  43. package/dist/index.d.ts +24 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +29 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/quickInsert/elementQuickInsert.d.ts +57 -0
  48. package/dist/quickInsert/elementQuickInsert.d.ts.map +1 -0
  49. package/dist/quickInsert/elementQuickInsert.js +96 -0
  50. package/dist/quickInsert/elementQuickInsert.js.map +1 -0
  51. package/dist/quickInsert/icons.d.ts +16 -0
  52. package/dist/quickInsert/icons.d.ts.map +1 -0
  53. package/dist/quickInsert/icons.js +21 -0
  54. package/dist/quickInsert/icons.js.map +1 -0
  55. package/dist/quickInsert/quickInsertCommand.d.ts +47 -0
  56. package/dist/quickInsert/quickInsertCommand.d.ts.map +1 -0
  57. package/dist/quickInsert/quickInsertCommand.js +81 -0
  58. package/dist/quickInsert/quickInsertCommand.js.map +1 -0
  59. package/dist/quickInsert/quickInsertListView.d.ts +29 -0
  60. package/dist/quickInsert/quickInsertListView.d.ts.map +1 -0
  61. package/dist/quickInsert/quickInsertListView.js +44 -0
  62. package/dist/quickInsert/quickInsertListView.js.map +1 -0
  63. package/dist/quickInsert/quickInsertPositionCommand.d.ts +33 -0
  64. package/dist/quickInsert/quickInsertPositionCommand.d.ts.map +1 -0
  65. package/dist/quickInsert/quickInsertPositionCommand.js +52 -0
  66. package/dist/quickInsert/quickInsertPositionCommand.js.map +1 -0
  67. package/dist/quickInsert/quickInsertView.d.ts +20 -0
  68. package/dist/quickInsert/quickInsertView.d.ts.map +1 -0
  69. package/dist/quickInsert/quickInsertView.js +52 -0
  70. package/dist/quickInsert/quickInsertView.js.map +1 -0
  71. package/package.json +52 -3
  72. package/src/augmentation.ts +44 -0
  73. package/src/breadcrumbs/breadcrumbItemView.ts +74 -0
  74. package/src/breadcrumbs/breadcrumbLabelView.ts +55 -0
  75. package/src/breadcrumbs/breadcrumbsView.ts +176 -0
  76. package/src/breadcrumbs/elementBreadcrumbs.ts +323 -0
  77. package/src/contextMenu/contextMenuObserver.ts +27 -0
  78. package/src/contextMenu/elementContextMenu.ts +104 -0
  79. package/src/domTarget.ts +27 -0
  80. package/src/elementMenu.ts +249 -0
  81. package/src/elementMenuView.ts +202 -0
  82. package/src/index.ts +54 -0
  83. package/src/quickInsert/elementQuickInsert.ts +135 -0
  84. package/src/quickInsert/icons.ts +26 -0
  85. package/src/quickInsert/quickInsertCommand.ts +96 -0
  86. package/src/quickInsert/quickInsertListView.ts +71 -0
  87. package/src/quickInsert/quickInsertPositionCommand.ts +68 -0
  88. package/src/quickInsert/quickInsertView.ts +64 -0
  89. package/theme/menu.css +131 -0
@@ -0,0 +1,249 @@
1
+ import {
2
+ ContextualBalloon,
3
+ DropdownMenuRootListView,
4
+ KeystrokeHandler,
5
+ Notification,
6
+ Plugin,
7
+ clickOutsideHandler,
8
+ type Editor,
9
+ type FocusableView,
10
+ type ModelElement,
11
+ type RectSource
12
+ } from 'ckeditor5';
13
+ import { isInDocument, type ElementOrPositionCommand } from 'ckeditor5-elements-core';
14
+ import ElementMenuView from './elementMenuView.js';
15
+
16
+ /**
17
+ * A submenu of an element menu. Missing items are skipped, as in the menu.
18
+ */
19
+ export interface ElementSubmenu {
20
+ title: string;
21
+
22
+ /**
23
+ * The icon (SVG string) shown next to the title.
24
+ */
25
+ icon?: string;
26
+
27
+ items: (ElementMenuItem | null | undefined)[];
28
+ }
29
+
30
+ /**
31
+ * An item of an element menu:
32
+ *
33
+ * * a command, shown as a button with its `title` and `icon`, enabled and "on" according to its
34
+ * {@link ElementOrPositionCommand#getState state} for the element,
35
+ * * a submenu,
36
+ * * `'|'`, a separator. Separators at the start or the end of a menu, and repeated ones, are not shown, so a group of
37
+ * items can start with one.
38
+ */
39
+ export type ElementMenuItem = ElementOrPositionCommand | ElementSubmenu | '|';
40
+
41
+ /**
42
+ * Returns the items of the menu of the given element, e.g. as the `elementContextMenu` or `elementBreadcrumbs.menu` editor
43
+ * config option. Missing items (e.g. commands that are not registered) are skipped. No items means no menu.
44
+ */
45
+ export type ElementMenuConfig = (element: ModelElement, editor: Editor) => (ElementMenuItem | null | undefined)[];
46
+
47
+ /**
48
+ * The options of {@link ElementMenu#open} and {@link ElementMenu#show}.
49
+ */
50
+ export interface ElementMenuShowOptions {
51
+ /**
52
+ * The accessible name of the menu of an element (see {@link ElementMenu#open}). Defaults to "Element menu".
53
+ */
54
+ label?: string;
55
+
56
+ /**
57
+ * Called when the document data changes while the menu is open. When it returns `true`, the menu closes, e.g.
58
+ * because the element it is for was removed.
59
+ */
60
+ isObsolete?: () => boolean;
61
+
62
+ /**
63
+ * What gets the focus when the menu is closed with Esc, e.g. the button that opened the menu. Defaults to the editor.
64
+ */
65
+ returnFocusTo?: { focus(): void };
66
+
67
+ /**
68
+ * Called when the menu closes, for any reason: Esc, a click outside, an executed item, or another menu replacing it.
69
+ */
70
+ onHide?: () => void;
71
+ }
72
+
73
+ /**
74
+ * Shows menus in a balloon: the menu of an element, built from commands, submenus and separators (see {@link open}),
75
+ * or any view, e.g. the quick insert search (see {@link show}).
76
+ *
77
+ * The element context menu, the breadcrumbs and quick insert show their menus with it. The menu of an element is
78
+ * CKEditor's dropdown menu (see {@link ElementMenuView}).
79
+ *
80
+ * Executing an item closes the menu. Esc and clicking outside close it too; Esc returns the focus to the editor, or to
81
+ * {@link ElementMenuShowOptions.returnFocusTo}.
82
+ */
83
+ export default class ElementMenu extends Plugin {
84
+ /**
85
+ * The view in the open menu, if any.
86
+ */
87
+ public view: FocusableView | null = null;
88
+
89
+ private _keystrokes: KeystrokeHandler | null = null;
90
+ private _isObsolete: (() => boolean) | null = null;
91
+ private _onHide: (() => void) | null = null;
92
+
93
+ public static get requires(): readonly [typeof ContextualBalloon, typeof Notification] {
94
+ // The buttons of the menu report failed commands with notifications.
95
+ return [ContextualBalloon, Notification] as const;
96
+ }
97
+
98
+ public static get pluginName(): 'ElementMenu' {
99
+ return 'ElementMenu' as const;
100
+ }
101
+
102
+ public init(): void {
103
+ this.listenTo(this.editor.model.document, 'change:data', () => {
104
+ if (this._isObsolete?.()) {
105
+ this.hide();
106
+ }
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Opens the menu of the element next to the target (e.g. a button, or a `DOMRect` in viewport coordinates), with
112
+ * the items from the config callback. The menu closes when the element is removed.
113
+ *
114
+ * Returns `false` when there are no items.
115
+ */
116
+ public open(
117
+ element: ModelElement,
118
+ target: RectSource,
119
+ config: ElementMenuConfig,
120
+ { label = this.editor.t('Element menu'), isObsolete, ...options }: ElementMenuShowOptions = {}
121
+ ): boolean {
122
+ const view = new ElementMenuView(
123
+ this.editor.locale,
124
+ this.editor.ui.view.body,
125
+ config(element, this.editor),
126
+ element
127
+ );
128
+
129
+ if (view.isEmpty) {
130
+ view.destroy();
131
+
132
+ return false;
133
+ }
134
+
135
+ view.ariaLabel = label;
136
+
137
+ this.show(view, target, {
138
+ ...options,
139
+ isObsolete: () => !isInDocument(element) || !!isObsolete?.()
140
+ });
141
+
142
+ return true;
143
+ }
144
+
145
+ /**
146
+ * Shows the view (e.g. a menu) next to the target (e.g. a button, or a `DOMRect` in viewport coordinates), and
147
+ * focuses it. The menu closes when the view, or an item of CKEditor's dropdown menu in it, fires `execute`.
148
+ */
149
+ public show(
150
+ view: FocusableView,
151
+ target: RectSource,
152
+ { isObsolete, returnFocusTo, onHide }: ElementMenuShowOptions = {}
153
+ ): void {
154
+ this.hide();
155
+
156
+ const close = (): void => {
157
+ // The item may have opened another menu already (e.g. "Insert before" opens quick insert).
158
+ if (this.view === view) {
159
+ this.hide();
160
+ }
161
+ };
162
+
163
+ this.listenTo(view, 'execute', close);
164
+ this.listenTo(view, 'menu:execute', close);
165
+
166
+ clickOutsideHandler({
167
+ emitter: view,
168
+ activator: () => this._balloon.visibleView === view,
169
+ // The open nested menus are in panels outside the balloon.
170
+ contextElements: () => [this._balloon.view.element, ...getOpenPanels(view)].filter(element => !!element),
171
+ callback: () => {
172
+ this.hide();
173
+ }
174
+ });
175
+
176
+ this._balloon.add({
177
+ view,
178
+ position: { target },
179
+ withArrow: false,
180
+ singleViewMode: true,
181
+ // Views that other plugins add to the balloon meanwhile (e.g. an element toolbar, when the selection moves
182
+ // into its element) go to their own stack, so they do not cover the menu.
183
+ stackId: 'elementMenu'
184
+ });
185
+
186
+ const keystrokes = new KeystrokeHandler();
187
+
188
+ // Esc in a nested menu closes only that menu.
189
+ keystrokes.set('Esc', (data, cancel) => {
190
+ this.hide();
191
+ (returnFocusTo ?? this.editor.editing.view).focus();
192
+ cancel();
193
+ });
194
+
195
+ if (view.element) {
196
+ keystrokes.listenTo(view.element);
197
+ }
198
+
199
+ this.view = view;
200
+ this._keystrokes = keystrokes;
201
+ this._isObsolete = isObsolete ?? null;
202
+ this._onHide = onHide ?? null;
203
+ view.focus();
204
+ }
205
+
206
+ /**
207
+ * Hides the open menu, if any.
208
+ */
209
+ public hide(): void {
210
+ const view = this.view;
211
+ const onHide = this._onHide;
212
+
213
+ if (!view) {
214
+ return;
215
+ }
216
+
217
+ this._keystrokes?.destroy();
218
+ this.view = null;
219
+ this._keystrokes = null;
220
+ this._isObsolete = null;
221
+ this._onHide = null;
222
+ this.stopListening(view);
223
+
224
+ if (this._balloon.hasView(view)) {
225
+ this._balloon.remove(view);
226
+ }
227
+
228
+ view.destroy();
229
+ onHide?.();
230
+ }
231
+
232
+ public override destroy(): void {
233
+ this.hide();
234
+ super.destroy();
235
+ }
236
+
237
+ private get _balloon(): ContextualBalloon {
238
+ return this.editor.plugins.get(ContextualBalloon);
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Returns the panels of the open nested menus of CKEditor's dropdown menu.
244
+ */
245
+ function getOpenPanels(view: FocusableView): (HTMLElement | null)[] {
246
+ return view instanceof DropdownMenuRootListView
247
+ ? view.menus.filter(menu => menu.isOpen).map(menu => menu.panelView.element)
248
+ : [];
249
+ }
@@ -0,0 +1,202 @@
1
+ import {
2
+ DropdownMenuRootListView,
3
+ ListSeparatorView,
4
+ type BodyCollection,
5
+ type DropdownMenuDefinition,
6
+ type ListView,
7
+ type Locale
8
+ } from 'ckeditor5';
9
+ import { ElementOrPositionCommand, bindButtonToCommand, type ElementOrPosition } from 'ckeditor5-elements-core';
10
+ import { type ElementMenuItem, type ElementSubmenu } from './elementMenu.js';
11
+
12
+ /**
13
+ * The menu of an element: CKEditor's dropdown menu (`DropdownMenuRootListView`), built from commands, submenus and
14
+ * separators.
15
+ *
16
+ * CKEditor builds the menu and handles it: the nested menus (opened on click, hover, or the right arrow; placed where
17
+ * they fit), the keyboard, and right-to-left languages. This view only connects the buttons to the commands: they
18
+ * reflect the command title, icon and {@link ElementOrPositionCommand#getState state} for the target, and execute the
19
+ * command for it. It also adds the separators, which CKEditor's menu definitions do not have.
20
+ *
21
+ * Missing items are skipped, and submenus without items, and separators at the start or the end, or repeated ones.
22
+ * Submenus are enabled when one of their items is. Right-clicking the menu does not open the native context menu.
23
+ */
24
+ export default class ElementMenuView extends DropdownMenuRootListView {
25
+ /**
26
+ * The element or position the commands are executed for.
27
+ */
28
+ public readonly target: ElementOrPosition;
29
+
30
+ /**
31
+ * Whether the menu has no items: all were missing.
32
+ */
33
+ public readonly isEmpty: boolean;
34
+
35
+ private readonly _commands: Map<string, ElementOrPositionCommand>;
36
+ private readonly _submenus: Map<string, MenuSubmenu>;
37
+
38
+ /**
39
+ * The positions of the separators in each list, by the id of the submenu (`''` for the menu itself).
40
+ */
41
+ private readonly _separators: Map<string, number[]>;
42
+
43
+ constructor(
44
+ locale: Locale,
45
+ bodyCollection: BodyCollection,
46
+ items: readonly (ElementMenuItem | null | undefined)[],
47
+ target: ElementOrPosition
48
+ ) {
49
+ const maps: DefinitionMaps = { commands: new Map(), submenus: new Map(), separators: new Map() };
50
+ const menuItems = cleanItems(items);
51
+
52
+ super(locale, bodyCollection, createDefinitions(menuItems, '', maps));
53
+
54
+ this.target = target;
55
+ this.isEmpty = !menuItems.length;
56
+ this._commands = maps.commands;
57
+ this._submenus = maps.submenus;
58
+ this._separators = maps.separators;
59
+ this.menuPanelClass = 'ck-elements-menu__panel';
60
+
61
+ this.extendTemplate({
62
+ attributes: { class: 'ck-elements-menu' },
63
+ on: { contextmenu: this.bindTemplate.to(preventDefault) }
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Renders the menu: CKEditor builds it from the definitions; then the buttons are connected to the commands, and the
69
+ * separators are added.
70
+ */
71
+ public override render(): void {
72
+ super.render();
73
+
74
+ for (const button of this.buttons) {
75
+ const command = this._commands.get(button.id);
76
+
77
+ if (command) {
78
+ bindButtonToCommand(button, command, { withText: true, target: this.target });
79
+
80
+ // A toggleable button with a checkable role tells assistive technology whether it is "on" with `aria-checked`.
81
+ button.role = command.isToggleable ? 'menuitemcheckbox' : 'menuitem';
82
+ }
83
+ }
84
+
85
+ for (const menu of this.menus) {
86
+ const submenu = this._submenus.get(menu.id);
87
+
88
+ if (submenu) {
89
+ menu.buttonView.icon = submenu.icon;
90
+ menu.isEnabled = this._isEnabled(submenu);
91
+ }
92
+
93
+ // The panels of nested menus are not inside the menu.
94
+ if (menu.panelView.element) {
95
+ this.listenTo(menu.panelView.element, 'contextmenu', (evt, domEvent: Event) => {
96
+ preventDefault(domEvent);
97
+ });
98
+ }
99
+ }
100
+
101
+ this._addSeparators('', this);
102
+
103
+ for (const menu of this.menus) {
104
+ this._addSeparators(menu.id, menu.listView);
105
+ }
106
+ }
107
+
108
+ private _addSeparators(id: string, list: ListView): void {
109
+ for (const position of this._separators.get(id) ?? []) {
110
+ const separator = new ListSeparatorView(this.locale);
111
+
112
+ separator.extendTemplate({ attributes: { role: 'separator' } });
113
+ list.items.add(separator, position);
114
+ }
115
+ }
116
+
117
+ private _isEnabled(item: MenuItem): boolean {
118
+ if (item === '|') {
119
+ return false;
120
+ }
121
+
122
+ return item instanceof ElementOrPositionCommand
123
+ ? item.getState(this.target).isEnabled
124
+ : item.items.some(child => this._isEnabled(child));
125
+ }
126
+ }
127
+
128
+ /**
129
+ * An item of the menu, as it is shown: none is missing.
130
+ */
131
+ type MenuItem = ElementOrPositionCommand | MenuSubmenu | '|';
132
+
133
+ interface MenuSubmenu extends Omit<ElementSubmenu, 'items'> {
134
+ items: MenuItem[];
135
+ }
136
+
137
+ interface DefinitionMaps {
138
+ commands: Map<string, ElementOrPositionCommand>;
139
+ submenus: Map<string, MenuSubmenu>;
140
+ separators: Map<string, number[]>;
141
+ }
142
+
143
+ /**
144
+ * Returns the items without missing ones, submenus without items, and separators at the start or the end, or
145
+ * repeated ones.
146
+ */
147
+ function cleanItems(items: readonly (ElementMenuItem | null | undefined)[]): MenuItem[] {
148
+ const result: MenuItem[] = [];
149
+
150
+ for (const item of items) {
151
+ const cleaned = item === '|' || item instanceof ElementOrPositionCommand || !item ? item : cleanSubmenu(item);
152
+
153
+ if (!cleaned || (cleaned === '|' && (!result.length || result.at(-1) === '|'))) {
154
+ continue;
155
+ }
156
+
157
+ result.push(cleaned);
158
+ }
159
+
160
+ if (result.at(-1) === '|') {
161
+ result.pop();
162
+ }
163
+
164
+ return result;
165
+ }
166
+
167
+ function cleanSubmenu(submenu: ElementSubmenu): MenuSubmenu | null {
168
+ const items = cleanItems(submenu.items);
169
+
170
+ return items.length ? { ...submenu, items } : null;
171
+ }
172
+
173
+ /**
174
+ * Creates CKEditor's menu definitions for the items. The ids are paths of positions (e.g. `'2.0'`). Collects the
175
+ * commands and submenus by id, and the positions of the separators by the id of their submenu.
176
+ */
177
+ function createDefinitions(items: readonly MenuItem[], parentId: string, maps: DefinitionMaps): DropdownMenuDefinition {
178
+ const definitions: DropdownMenuDefinition = [];
179
+ let position = 0;
180
+
181
+ for (const item of items) {
182
+ const id = parentId ? `${parentId}.${String(position)}` : String(position);
183
+
184
+ if (item === '|') {
185
+ maps.separators.set(parentId, [...(maps.separators.get(parentId) ?? []), position]);
186
+ } else if (item instanceof ElementOrPositionCommand) {
187
+ maps.commands.set(id, item);
188
+ definitions.push({ id, label: item.title });
189
+ } else {
190
+ maps.submenus.set(id, item);
191
+ definitions.push({ id, menu: item.title, children: createDefinitions(item.items, id, maps) });
192
+ }
193
+
194
+ position++;
195
+ }
196
+
197
+ return definitions;
198
+ }
199
+
200
+ function preventDefault(domEvent: Event): void {
201
+ domEvent.preventDefault();
202
+ }
package/src/index.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Element menus: `ElementMenu` shows the menu of an element, built from commands, submenus and separators, and the
3
+ * features that open it: the element context menu (a right-click, Shift+F10 or the menu key), the breadcrumbs (the
4
+ * element path at the selection, clickable with a menu) and quick insert (search and insert the elements allowed at a
5
+ * position).
6
+ *
7
+ * @module ckeditor5-elements-menu
8
+ */
9
+
10
+ // Plugins.
11
+ export {
12
+ default as ElementMenu,
13
+ type ElementMenuConfig,
14
+ type ElementMenuItem,
15
+ type ElementMenuShowOptions,
16
+ type ElementSubmenu
17
+ } from './elementMenu.js';
18
+ export { default as ElementContextMenu } from './contextMenu/elementContextMenu.js';
19
+ export { default as ElementBreadcrumbs, type ElementBreadcrumbsConfig } from './breadcrumbs/elementBreadcrumbs.js';
20
+ export { default as ElementQuickInsert, type ElementQuickInsertConfig } from './quickInsert/elementQuickInsert.js';
21
+
22
+ // Commands.
23
+ export { default as QuickInsertCommand } from './quickInsert/quickInsertCommand.js';
24
+ export {
25
+ default as QuickInsertPositionCommand,
26
+ type QuickInsertPlacement
27
+ } from './quickInsert/quickInsertPositionCommand.js';
28
+
29
+ // Views.
30
+ export { default as ElementMenuView } from './elementMenuView.js';
31
+ export {
32
+ default as BreadcrumbsView,
33
+ type BreadcrumbsViewExecuteEvent,
34
+ type BreadcrumbsViewOptions
35
+ } from './breadcrumbs/breadcrumbsView.js';
36
+ export {
37
+ default as BreadcrumbItemView,
38
+ type BreadcrumbItem,
39
+ type BreadcrumbItemViewOptions
40
+ } from './breadcrumbs/breadcrumbItemView.js';
41
+ export { default as BreadcrumbLabelView } from './breadcrumbs/breadcrumbLabelView.js';
42
+ export { default as QuickInsertView } from './quickInsert/quickInsertView.js';
43
+ export { default as QuickInsertListView, type QuickInsertItem } from './quickInsert/quickInsertListView.js';
44
+
45
+ // Observers.
46
+ export {
47
+ default as ContextMenuObserver,
48
+ type ViewDocumentContextMenuEvent
49
+ } from './contextMenu/contextMenuObserver.js';
50
+
51
+ // Icons.
52
+ export { insertAfterIcon, insertBeforeIcon, insertIcon } from './quickInsert/icons.js';
53
+
54
+ import './augmentation.js';
@@ -0,0 +1,135 @@
1
+ import { Plugin, type Editor, type FilteredViewExecuteEvent, type RectSource } from 'ckeditor5';
2
+ import type { ElementOrPosition } from 'ckeditor5-elements-core';
3
+ import { getDomTarget } from '../domTarget.js';
4
+ import ElementMenu from '../elementMenu.js';
5
+ import QuickInsertCommand from './quickInsertCommand.js';
6
+ import QuickInsertPositionCommand from './quickInsertPositionCommand.js';
7
+ import { type QuickInsertItem } from './quickInsertListView.js';
8
+ import QuickInsertView from './quickInsertView.js';
9
+
10
+ /**
11
+ * The configuration of the {@link ElementQuickInsert} plugin.
12
+ */
13
+ export interface ElementQuickInsertConfig {
14
+ /**
15
+ * Labels of the elements, by model name. Elements without a label are listed by their model name.
16
+ */
17
+ labels?: Record<string, string>;
18
+
19
+ /**
20
+ * Model names of the elements that are never listed.
21
+ */
22
+ exclude?: string[];
23
+
24
+ /**
25
+ * The keystroke that opens the menu at the selection, e.g. `'Ctrl+Space'`. Defaults to `'Alt+Enter'`
26
+ * (Option+Enter on macOS). `false` registers no keystroke.
27
+ */
28
+ keystroke?: string | false;
29
+ }
30
+
31
+ /**
32
+ * Opens a menu with a search field and the elements allowed at the selection, on Alt+Enter (Option+Enter on macOS; see
33
+ * {@link ElementQuickInsertConfig.keystroke}). Choosing an element inserts it with the `quickInsert` command.
34
+ *
35
+ * The menu is shown by the `ElementMenu` plugin. The `quickInsertBefore` and `quickInsertAfter` commands (see
36
+ * {@link QuickInsertPositionCommand}) open it for the position before or after an element; list them in the
37
+ * `elementContextMenu` config, e.g. as an "Insert" submenu.
38
+ *
39
+ * The menu closes when the document changes while it is open, so it never inserts at an outdated position.
40
+ */
41
+ export default class ElementQuickInsert extends Plugin {
42
+ public static get requires(): readonly [typeof ElementMenu] {
43
+ return [ElementMenu] as const;
44
+ }
45
+
46
+ public static get pluginName(): 'ElementQuickInsert' {
47
+ return 'ElementQuickInsert' as const;
48
+ }
49
+
50
+ constructor(editor: Editor) {
51
+ super(editor);
52
+
53
+ editor.config.define('elementQuickInsert', { labels: {}, exclude: [], keystroke: 'Alt+Enter' });
54
+ }
55
+
56
+ public init(): void {
57
+ const editor = this.editor;
58
+ const keystroke = editor.config.get('elementQuickInsert')?.keystroke;
59
+
60
+ editor.commands.add('quickInsert', new QuickInsertCommand(editor));
61
+ editor.commands.add('quickInsertBefore', new QuickInsertPositionCommand(editor, 'before', this));
62
+ editor.commands.add('quickInsertAfter', new QuickInsertPositionCommand(editor, 'after', this));
63
+
64
+ if (keystroke) {
65
+ editor.keystrokes.set(keystroke, (data, cancel) => {
66
+ if (this.open()) {
67
+ cancel();
68
+ }
69
+ });
70
+
71
+ editor.accessibility.addKeystrokeInfos({
72
+ keystrokes: [{ label: editor.t('Insert an element at the cursor'), keystroke }]
73
+ });
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Returns the elements that can be inserted at the target (a position, the position before an element, or the
79
+ * selection), with their labels.
80
+ */
81
+ public getItems(elementOrPosition?: ElementOrPosition): QuickInsertItem[] {
82
+ const config = this.editor.config.get('elementQuickInsert');
83
+ const labels = config?.labels ?? {};
84
+ const command = this.editor.commands.get('quickInsert');
85
+
86
+ return (command?.getAllowedElements(elementOrPosition, config?.exclude) ?? []).map(name => ({
87
+ name,
88
+ label: labels[name] ?? name
89
+ }));
90
+ }
91
+
92
+ /**
93
+ * Whether an element can be inserted at the target (a position, the position before an element, or the selection),
94
+ * as {@link getItems} lists them: it stops at the first one, e.g. for whether a command is enabled.
95
+ */
96
+ public hasItems(elementOrPosition?: ElementOrPosition): boolean {
97
+ const command = this.editor.commands.get('quickInsert');
98
+
99
+ return !!command?.hasAllowedElements(elementOrPosition, this.editor.config.get('elementQuickInsert')?.exclude);
100
+ }
101
+
102
+ /**
103
+ * Opens the menu for inserting at the target (a position, the position before an element, or the selection), next
104
+ * to the given DOM target. By default, the menu is next to the element, or the selection.
105
+ *
106
+ * Returns `false` when nothing can be inserted at the target because it cannot be edited, or there is nothing to
107
+ * show the menu next to.
108
+ */
109
+ public open(elementOrPosition?: ElementOrPosition, domTarget?: RectSource): boolean {
110
+ const editor = this.editor;
111
+ const command = editor.commands.get('quickInsert');
112
+ const position = command?.getInsertPosition(elementOrPosition);
113
+ const menuTarget = domTarget ?? getDomTarget(editor, elementOrPosition);
114
+
115
+ if (!command || !position || !menuTarget || !command.getState(position).isEnabled) {
116
+ return false;
117
+ }
118
+
119
+ const view = new QuickInsertView(editor.locale, this.getItems(position));
120
+
121
+ view.on<FilteredViewExecuteEvent>('execute', (evt, { value }) => {
122
+ // Focus first: focusing reads the DOM selection, which would override the selection set by the command.
123
+ editor.editing.view.focus();
124
+ command.executeFor(position, value);
125
+ });
126
+
127
+ // The search field is labelled "Insert element".
128
+ editor.plugins.get(ElementMenu).show(view, menuTarget, {
129
+ // The position would be outdated after any change, e.g. by another user.
130
+ isObsolete: () => true
131
+ });
132
+
133
+ return true;
134
+ }
135
+ }
@@ -0,0 +1,26 @@
1
+ import { IconPlus } from 'ckeditor5';
2
+
3
+ /**
4
+ * The icons of the quick insert commands, in the style of the CKEditor 5 icons (20×20).
5
+ */
6
+
7
+ /**
8
+ * Insert element: CKEditor's own plus icon.
9
+ */
10
+ export const insertIcon = IconPlus;
11
+
12
+ /**
13
+ * Insert before: a plus above a block.
14
+ */
15
+ export const insertBeforeIcon =
16
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill-rule="evenodd" d="' +
17
+ 'M9.25 1.5h1.5v2.75h2.75v1.5h-2.75V8.5h-1.5V5.75H6.5v-1.5h2.75zM3.5 11h13a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-13a1 1 0 0 ' +
18
+ '1-1-1v-5a1 1 0 0 1 1-1zm.5 1.5v4h12v-4z"/></svg>';
19
+
20
+ /**
21
+ * Insert after: a block above a plus.
22
+ */
23
+ export const insertAfterIcon =
24
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill-rule="evenodd" d="' +
25
+ 'M3.5 2h13a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-13a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1zm.5 1.5v4h12v-4zM9.25 11h1.5v2.75h2.75v1.5' +
26
+ 'h-2.75V18h-1.5v-2.75H6.5v-1.5h2.75z"/></svg>';