ckeditor5-elements-core 49.0.0-alpha.0 → 49.0.0-alpha.4

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 +2 -2
  2. package/dist/elementOrPositionButtonView.d.ts +10 -4
  3. package/dist/elementOrPositionButtonView.d.ts.map +1 -1
  4. package/dist/elementOrPositionButtonView.js +8 -2
  5. package/dist/elementOrPositionButtonView.js.map +1 -1
  6. package/dist/index.d.ts +6 -9
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +6 -10
  9. package/dist/index.js.map +1 -1
  10. package/dist/matching.d.ts +24 -0
  11. package/dist/matching.d.ts.map +1 -0
  12. package/dist/matching.js +27 -0
  13. package/dist/matching.js.map +1 -0
  14. package/dist/singleCopyGuard.js +2 -2
  15. package/dist/utils.d.ts +17 -1
  16. package/dist/utils.d.ts.map +1 -1
  17. package/dist/utils.js +38 -0
  18. package/dist/utils.js.map +1 -1
  19. package/package.json +2 -4
  20. package/src/elementOrPositionButtonView.ts +22 -6
  21. package/src/index.ts +8 -18
  22. package/src/matching.ts +45 -0
  23. package/src/singleCopyGuard.ts +2 -2
  24. package/src/utils.ts +49 -1
  25. package/dist/augmentation.d.ts +0 -7
  26. package/dist/augmentation.d.ts.map +0 -1
  27. package/dist/augmentation.js +0 -2
  28. package/dist/augmentation.js.map +0 -1
  29. package/dist/elementMenu.d.ts +0 -93
  30. package/dist/elementMenu.d.ts.map +0 -1
  31. package/dist/elementMenu.js +0 -160
  32. package/dist/elementMenu.js.map +0 -1
  33. package/dist/elementMenuItemButtonView.d.ts +0 -24
  34. package/dist/elementMenuItemButtonView.d.ts.map +0 -1
  35. package/dist/elementMenuItemButtonView.js +0 -29
  36. package/dist/elementMenuItemButtonView.js.map +0 -1
  37. package/dist/elementMenuView.d.ts +0 -35
  38. package/dist/elementMenuView.d.ts.map +0 -1
  39. package/dist/elementMenuView.js +0 -118
  40. package/dist/elementMenuView.js.map +0 -1
  41. package/dist/menuSeparatorView.d.ts +0 -8
  42. package/dist/menuSeparatorView.d.ts.map +0 -1
  43. package/dist/menuSeparatorView.js +0 -16
  44. package/dist/menuSeparatorView.js.map +0 -1
  45. package/dist/submenuView.d.ts +0 -68
  46. package/dist/submenuView.d.ts.map +0 -1
  47. package/dist/submenuView.js +0 -159
  48. package/dist/submenuView.js.map +0 -1
  49. package/src/augmentation.ts +0 -7
  50. package/src/elementMenu.ts +0 -272
  51. package/src/elementMenuView.ts +0 -164
  52. package/theme/elementMenu.css +0 -14
@@ -1,159 +0,0 @@
1
- import { DropdownMenuButtonView, DropdownMenuNestedMenuPanelView, DropdownMenuPanelPositioningFunctions, KeystrokeHandler, View } from 'ckeditor5';
2
- import ElementMenuView, {} from './elementMenuView.js';
3
- /**
4
- * A menu item that opens a nested menu next to it, built of CKEditor's nested menu parts: a
5
- * `DropdownMenuButtonView` (with an arrow, `aria-haspopup` and `aria-expanded`) and a `DropdownMenuNestedMenuPanelView`.
6
- *
7
- * The panel is added to the editor's body collection while the nested menu is open, so it is not clipped by the menu.
8
- * It is placed next to the item, on the side where it fits (in right-to-left languages, preferably on the left).
9
- *
10
- * Clicking the item, hovering it, or pressing the right arrow (left in right-to-left languages) opens the nested menu;
11
- * the opposite arrow or Esc closes it. When one of the nested items fires `execute`, the submenu fires `execute` too,
12
- * so the whole menu can close.
13
- */
14
- export default class SubmenuView extends View {
15
- /**
16
- * The item that opens the nested menu.
17
- */
18
- buttonView;
19
- /**
20
- * The panel of the nested menu.
21
- */
22
- panelView;
23
- /**
24
- * The nested menu, in the {@link panelView}.
25
- */
26
- listView;
27
- /**
28
- * Handles the keystrokes within the submenu and its panel.
29
- */
30
- keystrokes = new KeystrokeHandler();
31
- _bodyCollection;
32
- constructor(locale, bodyCollection, label, items, { icon } = {}) {
33
- super(locale);
34
- this._bodyCollection = bodyCollection;
35
- this.set('isOpen', false);
36
- this.buttonView = new DropdownMenuButtonView(locale);
37
- this.buttonView.set({ label, icon });
38
- this.buttonView.bind('isOn').to(this, 'isOpen');
39
- this.buttonView.extendTemplate({ attributes: { class: 'ck-elements-submenu__button' } });
40
- // Only opens, as in CKEditor's menus: hovering the item has opened it already when it is clicked.
41
- this.buttonView.on('execute', () => {
42
- this.open();
43
- });
44
- this.listView = new ElementMenuView(locale, { label });
45
- this.listView.addItems(items);
46
- this.listView.on('cancel', () => {
47
- this.close();
48
- });
49
- this.panelView = new DropdownMenuNestedMenuPanelView(locale);
50
- this.panelView.isVisible = true;
51
- this.panelView.extendTemplate({ attributes: { class: 'ck-elements-submenu__panel' } });
52
- this.panelView.content.add(this.listView);
53
- for (const item of items) {
54
- item.delegate('execute').to(this);
55
- }
56
- this.on('change:isOpen', (evt, name, isOpen) => {
57
- if (isOpen) {
58
- this._addPanelToBody();
59
- }
60
- else {
61
- this._removePanelFromBody();
62
- }
63
- });
64
- this.setTemplate({
65
- tag: 'div',
66
- attributes: {
67
- class: ['ck', 'ck-elements-submenu', 'ck-dropdown-menu-list__nested-menu'],
68
- role: 'presentation'
69
- },
70
- children: [this.buttonView]
71
- });
72
- }
73
- /**
74
- * Renders the submenu and its panel, and sets up the keystrokes that open and close the nested menu.
75
- */
76
- render() {
77
- super.render();
78
- this.panelView.render();
79
- const isRtl = this.locale?.uiLanguageDirection === 'rtl';
80
- // The panel is not inside the submenu, so its keystrokes do not bubble up to it.
81
- for (const element of [this.element, this.panelView.element]) {
82
- if (element) {
83
- this.keystrokes.listenTo(element);
84
- }
85
- }
86
- this.keystrokes.set(isRtl ? 'arrowleft' : 'arrowright', (data, cancel) => {
87
- if (document.activeElement === this.buttonView.element) {
88
- this.open();
89
- cancel();
90
- }
91
- });
92
- this.keystrokes.set(isRtl ? 'arrowright' : 'arrowleft', (data, cancel) => {
93
- if (this.isOpen && this.panelView.element?.contains(document.activeElement)) {
94
- this.close();
95
- cancel();
96
- }
97
- });
98
- }
99
- /**
100
- * Opens the nested menu and focuses its first item.
101
- */
102
- open() {
103
- this.isOpen = true;
104
- this.listView.focus();
105
- }
106
- /**
107
- * Closes the nested menu and focuses the item that opens it.
108
- */
109
- close() {
110
- this.isOpen = false;
111
- this.buttonView.focus();
112
- }
113
- /**
114
- * Focuses the item that opens the nested menu.
115
- */
116
- focus() {
117
- this.buttonView.focus();
118
- }
119
- /**
120
- * Destroys the submenu, its panel and keystroke handler.
121
- */
122
- destroy() {
123
- this._removePanelFromBody();
124
- super.destroy();
125
- this.panelView.destroy();
126
- this.keystrokes.destroy();
127
- }
128
- /**
129
- * The positions of the panel, in the order they are tried: as in CKEditor's nested dropdown menus.
130
- */
131
- get _panelPositions() {
132
- const { eastSouth, eastNorth, westSouth, westNorth } = DropdownMenuPanelPositioningFunctions;
133
- return this.locale?.uiLanguageDirection === 'rtl'
134
- ? [westSouth, westNorth, eastSouth, eastNorth]
135
- : [eastSouth, eastNorth, westSouth, westNorth];
136
- }
137
- _addPanelToBody() {
138
- if (!this._bodyCollection.has(this.panelView)) {
139
- this._bodyCollection.add(this.panelView);
140
- }
141
- if (this.buttonView.element) {
142
- this.panelView.pin({
143
- target: this.buttonView.element,
144
- positions: this._panelPositions,
145
- limiter: document.body,
146
- fitInViewport: true
147
- });
148
- }
149
- }
150
- _removePanelFromBody() {
151
- // Nested submenus close with their parent.
152
- this.listView.closeSubmenus();
153
- if (this._bodyCollection.has(this.panelView)) {
154
- this.panelView.unpin();
155
- this._bodyCollection.remove(this.panelView);
156
- }
157
- }
158
- }
159
- //# sourceMappingURL=submenuView.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"submenuView.js","sourceRoot":"","sources":["../src/submenuView.ts"],"names":[],"mappings":"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"}
@@ -1,7 +0,0 @@
1
- import type { ElementMenu } from './index.js';
2
-
3
- declare module 'ckeditor5' {
4
- interface PluginsMap {
5
- [ElementMenu.pluginName]: ElementMenu;
6
- }
7
- }
@@ -1,272 +0,0 @@
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 ElementMenuView from './elementMenuView.js';
14
- import ElementOrPositionCommand from './elementOrPositionCommand.js';
15
- import { isInDocument } from './utils.js';
16
-
17
- /**
18
- * A submenu of an element menu. Missing items are skipped, as in the menu.
19
- */
20
- export interface ElementSubmenu {
21
- title: string;
22
-
23
- /**
24
- * The icon (SVG string) shown next to the title.
25
- */
26
- icon?: string;
27
-
28
- items: (ElementMenuItem | null | undefined)[];
29
- }
30
-
31
- /**
32
- * An item of an element menu:
33
- *
34
- * * a command, shown as a button with its `title` and `icon`, enabled and "on" according to its
35
- * {@link ElementOrPositionCommand#getState state} for the element,
36
- * * a submenu,
37
- * * `'|'`, a separator. Separators at the start or the end of a menu, and repeated ones, are not shown, so a group of
38
- * items can start with one.
39
- */
40
- export type ElementMenuItem = ElementOrPositionCommand | ElementSubmenu | '|';
41
-
42
- /**
43
- * Returns the items of the menu of the given element, e.g. as the `elementContextMenu` or `breadcrumbs.menu` editor
44
- * config option. Missing items (e.g. commands that are not registered) are skipped. No items means no menu.
45
- */
46
- export type ElementMenuConfig = (element: ModelElement, editor: Editor) => (ElementMenuItem | null | undefined)[];
47
-
48
- /**
49
- * The options of {@link ElementMenu#open} and {@link ElementMenu#show}.
50
- */
51
- export interface ElementMenuShowOptions {
52
- /**
53
- * The accessible name of the menu of an element (see {@link ElementMenu#open}). Defaults to "Element menu".
54
- */
55
- label?: string;
56
-
57
- /**
58
- * Called when the document data changes while the menu is open. When it returns `true`, the menu closes, e.g.
59
- * because the element it is for was removed.
60
- */
61
- isObsolete?: () => boolean;
62
-
63
- /**
64
- * What gets the focus when the menu is closed with Esc, e.g. the button that opened the menu. Defaults to the editor.
65
- */
66
- returnFocusTo?: { focus(): void };
67
-
68
- /**
69
- * Called when the menu closes, for any reason: Esc, a click outside, an executed item, or another menu replacing it.
70
- */
71
- onHide?: () => void;
72
- }
73
-
74
- /**
75
- * Shows menus in a balloon: the menu of an element, built from commands, submenus and separators (see {@link open}),
76
- * or any view, e.g. the quick insert search (see {@link show}).
77
- *
78
- * The element context menu, the breadcrumbs and quick insert show their menus with it. The menu of an element is
79
- * CKEditor's dropdown menu (see {@link ElementMenuView}).
80
- *
81
- * Executing an item closes the menu. Esc and clicking outside close it too; Esc returns the focus to the editor, or to
82
- * {@link ElementMenuShowOptions.returnFocusTo}.
83
- */
84
- export default class ElementMenu extends Plugin {
85
- /**
86
- * The view in the open menu, if any.
87
- */
88
- public view: FocusableView | null = null;
89
-
90
- private _keystrokes: KeystrokeHandler | null = null;
91
- private _isObsolete: (() => boolean) | null = null;
92
- private _onHide: (() => void) | null = null;
93
-
94
- public static get requires(): readonly [typeof ContextualBalloon, typeof Notification] {
95
- // The buttons of the menu report failed commands with notifications.
96
- return [ContextualBalloon, Notification] as const;
97
- }
98
-
99
- public static get pluginName(): 'ElementMenu' {
100
- return 'ElementMenu' as const;
101
- }
102
-
103
- public init(): void {
104
- this.listenTo(this.editor.model.document, 'change:data', () => {
105
- if (this._isObsolete?.()) {
106
- this.hide();
107
- }
108
- });
109
- }
110
-
111
- /**
112
- * Opens the menu of the element next to the target (e.g. a button, or a `DOMRect` in viewport coordinates), with
113
- * the items from the config callback. The menu closes when the element is removed.
114
- *
115
- * Returns `false` when there are no items.
116
- */
117
- public open(
118
- element: ModelElement,
119
- target: RectSource,
120
- config: ElementMenuConfig,
121
- { label = this.editor.t('Element menu'), isObsolete, ...options }: ElementMenuShowOptions = {}
122
- ): boolean {
123
- const items = cleanItems(config(element, this.editor));
124
-
125
- if (!items.length) {
126
- return false;
127
- }
128
-
129
- const view = new ElementMenuView(this.editor.locale, this.editor.ui.view.body, items, element);
130
-
131
- view.ariaLabel = label;
132
-
133
- this.show(view, target, {
134
- ...options,
135
- isObsolete: () => !isInDocument(element) || !!isObsolete?.()
136
- });
137
-
138
- return true;
139
- }
140
-
141
- /**
142
- * Shows the view (e.g. a menu) next to the target (e.g. a button, or a `DOMRect` in viewport coordinates), and
143
- * focuses it. The menu closes when the view, or an item of CKEditor's dropdown menu in it, fires `execute`.
144
- */
145
- public show(
146
- view: FocusableView,
147
- target: RectSource,
148
- { isObsolete, returnFocusTo, onHide }: ElementMenuShowOptions = {}
149
- ): void {
150
- this.hide();
151
-
152
- const close = (): void => {
153
- // The item may have opened another menu already (e.g. "Insert before" opens quick insert).
154
- if (this.view === view) {
155
- this.hide();
156
- }
157
- };
158
-
159
- this.listenTo(view, 'execute', close);
160
- this.listenTo(view, 'menu:execute', close);
161
-
162
- clickOutsideHandler({
163
- emitter: view,
164
- activator: () => this._balloon.visibleView === view,
165
- // The open nested menus are in panels outside the balloon.
166
- contextElements: () => [this._balloon.view.element, ...getOpenPanels(view)].filter(element => !!element),
167
- callback: () => {
168
- this.hide();
169
- }
170
- });
171
-
172
- this._balloon.add({
173
- view,
174
- position: { target },
175
- withArrow: false,
176
- singleViewMode: true
177
- });
178
-
179
- const keystrokes = new KeystrokeHandler();
180
-
181
- // Esc in a nested menu closes only that menu.
182
- keystrokes.set('Esc', (data, cancel) => {
183
- this.hide();
184
- (returnFocusTo ?? this.editor.editing.view).focus();
185
- cancel();
186
- });
187
-
188
- if (view.element) {
189
- keystrokes.listenTo(view.element);
190
- }
191
-
192
- this.view = view;
193
- this._keystrokes = keystrokes;
194
- this._isObsolete = isObsolete ?? null;
195
- this._onHide = onHide ?? null;
196
- view.focus();
197
- }
198
-
199
- /**
200
- * Hides the open menu, if any.
201
- */
202
- public hide(): void {
203
- const view = this.view;
204
- const onHide = this._onHide;
205
-
206
- if (!view) {
207
- return;
208
- }
209
-
210
- this._keystrokes?.destroy();
211
- this.view = null;
212
- this._keystrokes = null;
213
- this._isObsolete = null;
214
- this._onHide = null;
215
- this.stopListening(view);
216
-
217
- if (this._balloon.hasView(view)) {
218
- this._balloon.remove(view);
219
- }
220
-
221
- view.destroy();
222
- onHide?.();
223
- }
224
-
225
- public override destroy(): void {
226
- this.hide();
227
- super.destroy();
228
- }
229
-
230
- private get _balloon(): ContextualBalloon {
231
- return this.editor.plugins.get(ContextualBalloon);
232
- }
233
- }
234
-
235
- /**
236
- * Returns the items without missing ones, submenus without items, and separators at the start or the end, or
237
- * repeated ones.
238
- */
239
- function cleanItems(items: readonly (ElementMenuItem | null | undefined)[]): ElementMenuItem[] {
240
- const result: ElementMenuItem[] = [];
241
-
242
- for (const item of items) {
243
- const cleaned = item === '|' || item instanceof ElementOrPositionCommand || !item ? item : cleanSubmenu(item);
244
-
245
- if (!cleaned || (cleaned === '|' && (!result.length || result.at(-1) === '|'))) {
246
- continue;
247
- }
248
-
249
- result.push(cleaned);
250
- }
251
-
252
- if (result.at(-1) === '|') {
253
- result.pop();
254
- }
255
-
256
- return result;
257
- }
258
-
259
- function cleanSubmenu(submenu: ElementSubmenu): ElementSubmenu | null {
260
- const items = cleanItems(submenu.items);
261
-
262
- return items.length ? { ...submenu, items } : null;
263
- }
264
-
265
- /**
266
- * Returns the panels of the open nested menus of CKEditor's dropdown menu.
267
- */
268
- function getOpenPanels(view: FocusableView): (HTMLElement | null)[] {
269
- return view instanceof DropdownMenuRootListView
270
- ? view.menus.filter(menu => menu.isOpen).map(menu => menu.panelView.element)
271
- : [];
272
- }
@@ -1,164 +0,0 @@
1
- import {
2
- DropdownMenuRootListView,
3
- ListSeparatorView,
4
- type BodyCollection,
5
- type DropdownMenuDefinition,
6
- type ListView,
7
- type Locale
8
- } from 'ckeditor5';
9
- import { type ElementMenuItem, type ElementSubmenu } from './elementMenu.js';
10
- import { bindButtonToCommand } from './elementOrPositionButtonView.js';
11
- import ElementOrPositionCommand, { type ElementOrPosition } from './elementOrPositionCommand.js';
12
-
13
- /**
14
- * The menu of an element: CKEditor's dropdown menu (`DropdownMenuRootListView`), built from commands, submenus and
15
- * separators.
16
- *
17
- * CKEditor builds the menu and handles it: the nested menus (opened on click, hover, or the right arrow; placed where
18
- * they fit), the keyboard, and right-to-left languages. This view only connects the buttons to the commands: they
19
- * reflect the command title, icon and {@link ElementOrPositionCommand#getState state} for the target, and execute the
20
- * command for it. It also adds the separators, which CKEditor's menu definitions do not have.
21
- *
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
- private readonly _commands: Map<string, ElementOrPositionCommand>;
31
- private readonly _submenus: Map<string, ElementSubmenu>;
32
-
33
- /**
34
- * The positions of the separators in each list, by the id of the submenu (`''` for the menu itself).
35
- */
36
- private readonly _separators: Map<string, number[]>;
37
-
38
- constructor(
39
- locale: Locale,
40
- bodyCollection: BodyCollection,
41
- items: readonly (ElementMenuItem | null | undefined)[],
42
- target: ElementOrPosition
43
- ) {
44
- const maps: DefinitionMaps = { commands: new Map(), submenus: new Map(), separators: new Map() };
45
-
46
- super(locale, bodyCollection, createDefinitions(items, '', maps));
47
-
48
- this.target = target;
49
- this._commands = maps.commands;
50
- this._submenus = maps.submenus;
51
- this._separators = maps.separators;
52
- this.menuPanelClass = 'ck-elements-menu__panel';
53
-
54
- this.extendTemplate({
55
- attributes: { class: 'ck-elements-menu' },
56
- on: { contextmenu: this.bindTemplate.to(preventDefault) }
57
- });
58
- }
59
-
60
- /**
61
- * Renders the menu: CKEditor builds it from the definitions; then the buttons are connected to the commands, and the
62
- * separators are added.
63
- */
64
- public override render(): void {
65
- super.render();
66
-
67
- for (const button of this.buttons) {
68
- const command = this._commands.get(button.id);
69
-
70
- if (command) {
71
- bindButtonToCommand(button, command, { withText: true, target: this.target });
72
-
73
- // A toggleable button with a checkable role tells assistive technology whether it is "on" with `aria-checked`.
74
- button.role = command.isToggleable ? 'menuitemcheckbox' : 'menuitem';
75
- }
76
- }
77
-
78
- for (const menu of this.menus) {
79
- const submenu = this._submenus.get(menu.id);
80
-
81
- if (submenu) {
82
- menu.buttonView.icon = submenu.icon;
83
- menu.isEnabled = this._isEnabled(submenu);
84
- }
85
-
86
- // The panels of nested menus are not inside the menu.
87
- if (menu.panelView.element) {
88
- this.listenTo(menu.panelView.element, 'contextmenu', (evt, domEvent: Event) => {
89
- preventDefault(domEvent);
90
- });
91
- }
92
- }
93
-
94
- this._addSeparators('', this);
95
-
96
- for (const menu of this.menus) {
97
- this._addSeparators(menu.id, menu.listView);
98
- }
99
- }
100
-
101
- private _addSeparators(id: string, list: ListView): void {
102
- for (const position of this._separators.get(id) ?? []) {
103
- const separator = new ListSeparatorView(this.locale);
104
-
105
- separator.extendTemplate({ attributes: { role: 'separator' } });
106
- list.items.add(separator, position);
107
- }
108
- }
109
-
110
- private _isEnabled(item: ElementMenuItem): boolean {
111
- if (item === '|') {
112
- return false;
113
- }
114
-
115
- return item instanceof ElementOrPositionCommand
116
- ? item.getState(this.target).isEnabled
117
- : item.items.some(child => !!child && this._isEnabled(child));
118
- }
119
- }
120
-
121
- interface DefinitionMaps {
122
- commands: Map<string, ElementOrPositionCommand>;
123
- submenus: Map<string, ElementSubmenu>;
124
- separators: Map<string, number[]>;
125
- }
126
-
127
- /**
128
- * Creates CKEditor's menu definitions for the items. The ids are paths of positions (e.g. `'2.0'`). Collects the
129
- * commands and submenus by id, and the positions of the separators by the id of their submenu.
130
- */
131
- function createDefinitions(
132
- items: readonly (ElementMenuItem | null | undefined)[],
133
- parentId: string,
134
- maps: DefinitionMaps
135
- ): DropdownMenuDefinition {
136
- const definitions: DropdownMenuDefinition = [];
137
- let position = 0;
138
-
139
- for (const item of items) {
140
- const id = parentId ? `${parentId}.${String(position)}` : String(position);
141
-
142
- if (!item) {
143
- continue;
144
- }
145
-
146
- if (item === '|') {
147
- maps.separators.set(parentId, [...(maps.separators.get(parentId) ?? []), position]);
148
- } else if (item instanceof ElementOrPositionCommand) {
149
- maps.commands.set(id, item);
150
- definitions.push({ id, label: item.title });
151
- } else {
152
- maps.submenus.set(id, item);
153
- definitions.push({ id, menu: item.title, children: createDefinitions(item.items, id, maps) });
154
- }
155
-
156
- position++;
157
- }
158
-
159
- return definitions;
160
- }
161
-
162
- function preventDefault(domEvent: Event): void {
163
- domEvent.preventDefault();
164
- }
@@ -1,14 +0,0 @@
1
- /*
2
- * The menu of an element is CKEditor's dropdown menu, so CKEditor styles it. Only the alignment of labels is added.
3
- */
4
-
5
- /* In a menu with icons (or a nested menu of it), items without an icon get the space of one, so that all labels line up. */
6
- :is(.ck.ck-elements-menu, .ck.ck-elements-menu__panel > .ck-dropdown-menu-list):has(
7
- > .ck-list__item > :is(.ck-button, .ck-dropdown-menu-list__nested-menu > .ck-button) > .ck-button__icon
8
- )
9
- > .ck-list__item
10
- > :is(.ck-button, .ck-dropdown-menu-list__nested-menu > .ck-button):not(:has(> .ck-button__icon))
11
- > .ck-button__label {
12
- /* The width of an icon, and the gap after it. */
13
- margin-inline-start: calc(var(--ck-icon-size) + var(--ck-button-icon-gap));
14
- }