@kubex/zinc 1.1.86 → 1.1.88
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/custom-elements.json +368 -358
- package/dist/vscode.html-custom-data.json +15 -15
- package/dist/web-types.json +39 -39
- package/dist/zn.d.ts +8 -32
- package/dist/zn.min.js +230 -247
- package/package.json +1 -1
- package/src/components/editor/modules/context-menu/context-menu-tool.test.ts +36 -7
- package/src/components/editor/modules/context-menu/context-menu.ts +108 -118
- package/src/components/editor/modules/toolbar/toolbar.ts +4 -1
- package/src/components/slash-menu/slash-menu.component.ts +24 -5
- package/src/components/slash-menu/slash-menu.scss +2 -2
- package/src/components/slash-menu/slash-menu.test.ts +13 -1
- package/src/components/editor/modules/context-menu/context-menu-component.ts +0 -116
- package/src/components/editor/modules/context-menu/context-menu.scss +0 -49
package/package.json
CHANGED
|
@@ -9,9 +9,9 @@ interface QuillLike {
|
|
|
9
9
|
root: HTMLElement;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
interface
|
|
12
|
+
interface SlashMenuLike extends HTMLElement {
|
|
13
13
|
open: boolean;
|
|
14
|
-
|
|
14
|
+
items: { label: string }[];
|
|
15
15
|
setActiveIndex: (index: number) => void;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -19,9 +19,13 @@ interface EditorDialogLike extends HTMLElement {
|
|
|
19
19
|
dialogEl: HTMLDialogElement;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
interface DropdownLike extends HTMLElement {
|
|
23
|
+
open: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
describe('<zn-editor> context menu tools', () => {
|
|
23
27
|
afterEach(() => {
|
|
24
|
-
document.querySelectorAll('zn-
|
|
28
|
+
document.querySelectorAll('zn-slash-menu, zn-editor-dialog').forEach(el => el.remove());
|
|
25
29
|
});
|
|
26
30
|
|
|
27
31
|
const editorWithTool = () => fixture<ZnEditor>(html`
|
|
@@ -43,8 +47,8 @@ describe('<zn-editor> context menu tools', () => {
|
|
|
43
47
|
return quill;
|
|
44
48
|
};
|
|
45
49
|
|
|
46
|
-
const contextMenu = ():
|
|
47
|
-
document.querySelector('zn-
|
|
50
|
+
const contextMenu = (): SlashMenuLike =>
|
|
51
|
+
document.querySelector('zn-slash-menu') as unknown as SlashMenuLike;
|
|
48
52
|
|
|
49
53
|
it('lists a context-menu flagged tool in the slash menu', async () => {
|
|
50
54
|
const el = await editorWithTool();
|
|
@@ -54,7 +58,7 @@ describe('<zn-editor> context menu tools', () => {
|
|
|
54
58
|
expect(menu, 'context menu should exist').to.exist;
|
|
55
59
|
expect(menu.open, 'context menu should be open').to.be.true;
|
|
56
60
|
|
|
57
|
-
const labels = menu.
|
|
61
|
+
const labels = menu.items.map(item => item.label);
|
|
58
62
|
expect(labels).to.include('Canned Responses');
|
|
59
63
|
});
|
|
60
64
|
|
|
@@ -63,7 +67,7 @@ describe('<zn-editor> context menu tools', () => {
|
|
|
63
67
|
const quill = await openContextMenu(el);
|
|
64
68
|
|
|
65
69
|
const menu = contextMenu();
|
|
66
|
-
const index = menu.
|
|
70
|
+
const index = menu.items.findIndex(item => item.label === 'Canned Responses');
|
|
67
71
|
expect(index).to.be.greaterThan(-1);
|
|
68
72
|
menu.setActiveIndex(index);
|
|
69
73
|
|
|
@@ -80,4 +84,29 @@ describe('<zn-editor> context menu tools', () => {
|
|
|
80
84
|
expect(dialog.dialogEl.open, 'dialog should be open').to.be.true;
|
|
81
85
|
expect(dialog.innerHTML).to.contain('/canned');
|
|
82
86
|
});
|
|
87
|
+
|
|
88
|
+
it('keeps the date picker open when Date is chosen with the mouse', async () => {
|
|
89
|
+
const el = await fixture<ZnEditor>(html`
|
|
90
|
+
<zn-editor id="test-editor" dates></zn-editor>`);
|
|
91
|
+
const quill = await openContextMenu(el);
|
|
92
|
+
quill.insertText(1, 'date', 'user');
|
|
93
|
+
quill.setSelection(5, 0, 'user');
|
|
94
|
+
await aTimeout(50);
|
|
95
|
+
|
|
96
|
+
const menu = contextMenu();
|
|
97
|
+
const index = menu.items.findIndex(item => item.label === 'Date');
|
|
98
|
+
expect(index, 'Date should be listed').to.be.greaterThan(-1);
|
|
99
|
+
|
|
100
|
+
const button = menu.shadowRoot!.querySelectorAll<HTMLButtonElement>('[data-slash-item]')[index];
|
|
101
|
+
expect(button, 'Date item should be rendered').to.exist;
|
|
102
|
+
|
|
103
|
+
const toolbar = el.shadowRoot!.getElementById('toolbar');
|
|
104
|
+
const dateDropdown = toolbar?.shadowRoot?.querySelector('zn-dropdown.toolbar__date-dropdown') as DropdownLike | null;
|
|
105
|
+
const overflowDropdown = toolbar?.shadowRoot?.querySelector('zn-dropdown.toolbar__overflow') as DropdownLike | null;
|
|
106
|
+
|
|
107
|
+
button.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, cancelable: true, composed: true}));
|
|
108
|
+
await aTimeout(100);
|
|
109
|
+
|
|
110
|
+
expect(dateDropdown?.open || overflowDropdown?.open, 'a dropdown holding the date picker should be open').to.be.true;
|
|
111
|
+
});
|
|
83
112
|
});
|
|
@@ -1,23 +1,38 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {html} from "lit";
|
|
3
|
-
import {litToHTML} from "../../../../utilities/lit-to-html";
|
|
4
|
-
import {type ResultItem} from "./context-menu-component";
|
|
1
|
+
import {filterSlashItems, SLASH_ITEM_SELECT} from "../../../slash-menu";
|
|
5
2
|
import Delta from "quill-delta";
|
|
6
3
|
import Quill from "quill";
|
|
7
4
|
import ZnEditorQuickAction from "./quick-action";
|
|
8
5
|
import ZnEditorTool from "../toolbar/tool";
|
|
9
6
|
import type {EditorFeatureConfig} from "../../editor.component";
|
|
10
|
-
import type
|
|
7
|
+
import type {SlashMenuItem} from "../../../slash-menu";
|
|
8
|
+
import type {VirtualElement} from "@floating-ui/dom";
|
|
11
9
|
import type Toolbar from "../toolbar/toolbar";
|
|
10
|
+
import type ZnSlashMenu from "../../../slash-menu";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A slash menu entry plus what choosing it does in the editor: trigger a toolbar tool by `key`,
|
|
14
|
+
* or apply `format` with `formatValue`. `SlashMenuItem.value` is left unset so the menu never
|
|
15
|
+
* renders format values as insertion tokens.
|
|
16
|
+
*/
|
|
17
|
+
interface ContextMenuItem extends SlashMenuItem {
|
|
18
|
+
format?: string;
|
|
19
|
+
key?: string;
|
|
20
|
+
formatValue?: string | boolean;
|
|
21
|
+
}
|
|
12
22
|
|
|
13
23
|
class ContextMenu {
|
|
14
24
|
private _quill: Quill;
|
|
15
25
|
private readonly _toolbarModule: Toolbar;
|
|
16
|
-
private
|
|
26
|
+
private _menu: ZnSlashMenu;
|
|
17
27
|
private _startIndex = -1;
|
|
28
|
+
/** Trigger position the user dismissed with Escape; the menu stays shut until they move off it. */
|
|
29
|
+
private _dismissedIndex = -1;
|
|
18
30
|
private _keydownHandler = (e: KeyboardEvent) => this.onKeydown(e);
|
|
19
31
|
private _docClickHandler = (e: MouseEvent) => this.onDocumentClick(e);
|
|
20
32
|
private _featureConfig: EditorFeatureConfig = {};
|
|
33
|
+
private readonly _caretAnchor: VirtualElement = {
|
|
34
|
+
getBoundingClientRect: () => this.caretRect()
|
|
35
|
+
};
|
|
21
36
|
|
|
22
37
|
constructor(quill: Quill, options: { config: EditorFeatureConfig }) {
|
|
23
38
|
this._quill = quill;
|
|
@@ -29,69 +44,56 @@ class ContextMenu {
|
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
private initComponent() {
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
const doc = this._quill.container.ownerDocument;
|
|
48
|
+
// The named import above pulls in the slash-menu module, which registers the element
|
|
49
|
+
this._menu = doc.createElement('zn-slash-menu');
|
|
50
|
+
this._menu.heading = 'Options';
|
|
51
|
+
this._caretAnchor.contextElement = this._quill.root;
|
|
52
|
+
this._menu.anchor = this._caretAnchor;
|
|
53
|
+
doc.body.appendChild(this._menu);
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
private attachEvents() {
|
|
37
|
-
this._quill.on(Quill.events.TEXT_CHANGE, () => this.updateFromEditor());
|
|
38
57
|
this._quill.on(Quill.events.EDITOR_CHANGE, () => this.updateFromEditor());
|
|
39
58
|
this._quill.root.addEventListener('keydown', this._keydownHandler);
|
|
40
|
-
this.
|
|
41
|
-
this._quill.on('editor-change', () => this.positionComponent());
|
|
59
|
+
this._menu.addEventListener(SLASH_ITEM_SELECT, (e: Event) => this.onItemSelect(e as CustomEvent<{ item: ContextMenuItem }>));
|
|
42
60
|
this._quill.focus();
|
|
43
61
|
}
|
|
44
62
|
|
|
45
|
-
private createComponent() {
|
|
46
|
-
const tpl = html`
|
|
47
|
-
<zn-context-menu></zn-context-menu>`;
|
|
48
|
-
return litToHTML<ContextMenuComponent>(tpl);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
63
|
private onDocumentClick(e: MouseEvent) {
|
|
52
|
-
const
|
|
53
|
-
if (!
|
|
54
|
-
|
|
55
|
-
if (!this._component.contains(target) && !this._quill.root.contains(target)) {
|
|
64
|
+
const path = e.composedPath();
|
|
65
|
+
if (!path.includes(this._menu) && !path.includes(this._quill.root)) {
|
|
56
66
|
this.hide();
|
|
57
67
|
}
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
private updateFromEditor() {
|
|
61
71
|
const info = this.getToolbarQuery();
|
|
62
|
-
if (!info) {
|
|
72
|
+
if (!info || info.start === this._dismissedIndex) {
|
|
63
73
|
this.hide();
|
|
64
74
|
return;
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
const {start, formatQuery} = info;
|
|
68
|
-
this.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this._component.results = this._getOptions().filter(it => !q || it.label.toLowerCase().includes(q) || it?.format?.toLowerCase().includes(q));
|
|
73
|
-
} catch {
|
|
74
|
-
this._component.results = [];
|
|
78
|
+
const matches = filterSlashItems(this._getOptions(), formatQuery);
|
|
79
|
+
if (!matches.length) {
|
|
80
|
+
this.hide();
|
|
81
|
+
return;
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
this.
|
|
84
|
+
this._startIndex = start;
|
|
85
|
+
this._menu.query = formatQuery;
|
|
86
|
+
this._menu.items = matches;
|
|
78
87
|
this.show();
|
|
79
|
-
this.positionComponent();
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
private
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
if (!
|
|
87
|
-
|
|
88
|
-
const bounds = this._quill.getBounds(range.index);
|
|
89
|
-
if (!bounds) return;
|
|
90
|
+
private caretRect(): DOMRect {
|
|
91
|
+
const index = this._startIndex >= 0 ? this._startIndex : (this._quill.getSelection()?.index ?? 0);
|
|
92
|
+
const container = this._quill.container.getBoundingClientRect();
|
|
93
|
+
const bounds = this._quill.getBounds(index);
|
|
94
|
+
if (!bounds) return new DOMRect(container.left, container.top, 0, 0);
|
|
90
95
|
|
|
91
|
-
|
|
92
|
-
const left = Math.max(0, editorBounds.left + bounds.left);
|
|
93
|
-
const top = editorBounds.top + bounds.bottom + 4;
|
|
94
|
-
this._component.setPosition(left, top);
|
|
96
|
+
return new DOMRect(container.left + bounds.left, container.top + bounds.top, 0, bounds.height);
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
private getToolbarQuery(): { start: number; formatQuery: string } | null {
|
|
@@ -120,61 +122,50 @@ class ContextMenu {
|
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
private onKeydown(e: KeyboardEvent) {
|
|
123
|
-
if (!this.
|
|
125
|
+
if (!this._menu.open) return;
|
|
124
126
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (e.key === 'ArrowDown') {
|
|
133
|
-
e.preventDefault();
|
|
134
|
-
e.stopPropagation();
|
|
135
|
-
const next = (this._component.getActiveIndex?.() ?? -1) + 1;
|
|
136
|
-
this._component.setActiveIndex?.(next);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
127
|
+
switch (e.key) {
|
|
128
|
+
case 'Escape':
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
e.stopPropagation();
|
|
131
|
+
this._dismissedIndex = this._startIndex;
|
|
132
|
+
this.hide();
|
|
133
|
+
return;
|
|
139
134
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
135
|
+
case 'ArrowDown':
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
e.stopPropagation();
|
|
138
|
+
this._menu.moveActive(1);
|
|
139
|
+
return;
|
|
147
140
|
|
|
148
|
-
|
|
149
|
-
const idx = this._component.getActiveIndex?.();
|
|
150
|
-
const results = (this._component.results as ResultItem[]) || [];
|
|
151
|
-
const item = (typeof idx === 'number' && idx >= 0 && idx < results.length) ? results[idx] : undefined;
|
|
152
|
-
if (item?.format === 'toolbar' && item?.key) {
|
|
141
|
+
case 'ArrowUp':
|
|
153
142
|
e.preventDefault();
|
|
154
143
|
e.stopPropagation();
|
|
155
|
-
this.
|
|
144
|
+
this._menu.moveActive(-1);
|
|
156
145
|
return;
|
|
157
|
-
|
|
158
|
-
|
|
146
|
+
|
|
147
|
+
case 'Enter': {
|
|
148
|
+
if (!this._menu.activeItem) return;
|
|
149
|
+
|
|
159
150
|
e.preventDefault();
|
|
160
151
|
e.stopPropagation();
|
|
161
|
-
this.
|
|
152
|
+
this._menu.selectActive();
|
|
162
153
|
}
|
|
163
154
|
}
|
|
164
155
|
}
|
|
165
156
|
|
|
166
|
-
private
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
169
|
-
|
|
157
|
+
private onItemSelect(e: CustomEvent<{ item: ContextMenuItem }>) {
|
|
158
|
+
const item = e.detail?.item;
|
|
159
|
+
if (!item) return;
|
|
160
|
+
|
|
161
|
+
if (item.key) {
|
|
162
|
+
this._clickToolbarItem(item.key);
|
|
170
163
|
return;
|
|
171
164
|
}
|
|
172
165
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
this._applySelectedFormat(format, value);
|
|
166
|
+
if (item.format) {
|
|
167
|
+
this._applySelectedFormat(item.format, item.formatValue);
|
|
168
|
+
}
|
|
178
169
|
}
|
|
179
170
|
|
|
180
171
|
private _clickToolbarItem(key: string) {
|
|
@@ -228,8 +219,8 @@ class ContextMenu {
|
|
|
228
219
|
}
|
|
229
220
|
}
|
|
230
221
|
|
|
231
|
-
private _getOptions():
|
|
232
|
-
const options:
|
|
222
|
+
private _getOptions(): ContextMenuItem[] {
|
|
223
|
+
const options: ContextMenuItem[] = [];
|
|
233
224
|
let orderCounter = 0;
|
|
234
225
|
|
|
235
226
|
// 1) Quick Actions
|
|
@@ -251,11 +242,11 @@ class ContextMenu {
|
|
|
251
242
|
|
|
252
243
|
if (label && icon) {
|
|
253
244
|
if (key) {
|
|
254
|
-
options.push({icon, label,
|
|
245
|
+
options.push({icon, label, key: key, order});
|
|
255
246
|
} else if (uri) {
|
|
256
|
-
options.push({icon, label, format: 'dialog',
|
|
247
|
+
options.push({icon, label, format: 'dialog', formatValue: uri, order});
|
|
257
248
|
} else if (content) {
|
|
258
|
-
options.push({icon, label, format: 'insert',
|
|
249
|
+
options.push({icon, label, format: 'insert', formatValue: content, order});
|
|
259
250
|
}
|
|
260
251
|
}
|
|
261
252
|
});
|
|
@@ -269,73 +260,72 @@ class ContextMenu {
|
|
|
269
260
|
if (!label || !icon || !key) return;
|
|
270
261
|
|
|
271
262
|
const order = typeof tool.order === 'number' ? tool.order : orderCounter++;
|
|
272
|
-
options.push({icon, label,
|
|
263
|
+
options.push({icon, label, key: key, order});
|
|
273
264
|
});
|
|
274
265
|
}
|
|
275
266
|
|
|
276
267
|
// 2) Built-in Actions (In order they should appear)
|
|
277
268
|
options.push(
|
|
278
|
-
{icon: 'bold@lu', label: 'Bold', format: 'bold', order: orderCounter++},
|
|
279
|
-
{icon: 'italic@lu', label: 'Italic', format: 'italic', order: orderCounter++},
|
|
280
|
-
{icon: 'underline@lu', label: 'Underline', format: 'underline', order: orderCounter++},
|
|
281
|
-
{icon: 'strikethrough@lu', label: 'Strikethrough', format: 'strike', order: orderCounter++},
|
|
282
|
-
{icon: 'text-quote@lu', label: 'Blockquote', format: 'blockquote', order: orderCounter++},
|
|
269
|
+
{icon: 'bold@lu', label: 'Bold', format: 'bold', keywords: 'bold', order: orderCounter++},
|
|
270
|
+
{icon: 'italic@lu', label: 'Italic', format: 'italic', keywords: 'italic', order: orderCounter++},
|
|
271
|
+
{icon: 'underline@lu', label: 'Underline', format: 'underline', keywords: 'underline', order: orderCounter++},
|
|
272
|
+
{icon: 'strikethrough@lu', label: 'Strikethrough', format: 'strike', keywords: 'strike', order: orderCounter++},
|
|
273
|
+
{icon: 'text-quote@lu', label: 'Blockquote', format: 'blockquote', keywords: 'blockquote', order: orderCounter++},
|
|
283
274
|
);
|
|
284
275
|
if (this._featureConfig.codeEnabled !== false) {
|
|
285
|
-
options.push({icon: 'code@lu', label: 'Inline Code', format: 'code', order: orderCounter++});
|
|
276
|
+
options.push({icon: 'code@lu', label: 'Inline Code', format: 'code', keywords: 'code', order: orderCounter++});
|
|
286
277
|
}
|
|
287
278
|
if (this._featureConfig.codeBlocksEnabled !== false) {
|
|
288
|
-
options.push({icon: 'square-code@lu', label: 'Code Block', format: 'code-block', order: orderCounter++});
|
|
279
|
+
options.push({icon: 'square-code@lu', label: 'Code Block', format: 'code-block', keywords: 'code-block', order: orderCounter++});
|
|
289
280
|
}
|
|
290
281
|
options.push(
|
|
291
|
-
{icon: 'heading-1@lu', label: 'Heading 1', format: 'header',
|
|
292
|
-
{icon: 'heading-2@lu', label: 'Heading 2', format: 'header',
|
|
293
|
-
{icon: 'case-sensitive@lu', label: 'Normal Text', format: 'header',
|
|
294
|
-
{icon: 'list@lu', label: 'Bulleted List', format: 'list',
|
|
295
|
-
{icon: 'list-ordered@lu', label: 'Numbered List', format: 'list',
|
|
296
|
-
{icon: 'list-todo@lu', label: 'Checklist', format: 'list',
|
|
282
|
+
{icon: 'heading-1@lu', label: 'Heading 1', format: 'header', formatValue: '1', keywords: 'header,h1', order: orderCounter++},
|
|
283
|
+
{icon: 'heading-2@lu', label: 'Heading 2', format: 'header', formatValue: '2', keywords: 'header,h2', order: orderCounter++},
|
|
284
|
+
{icon: 'case-sensitive@lu', label: 'Normal Text', format: 'header', formatValue: '', keywords: 'header,paragraph', order: orderCounter++},
|
|
285
|
+
{icon: 'list@lu', label: 'Bulleted List', format: 'list', formatValue: 'bullet', keywords: 'list,bullet', order: orderCounter++},
|
|
286
|
+
{icon: 'list-ordered@lu', label: 'Numbered List', format: 'list', formatValue: 'ordered', keywords: 'list,ordered', order: orderCounter++},
|
|
287
|
+
{icon: 'list-todo@lu', label: 'Checklist', format: 'list', formatValue: 'checked', keywords: 'list,checked,todo', order: orderCounter++}
|
|
297
288
|
);
|
|
298
289
|
if (this._featureConfig.linksEnabled !== false) {
|
|
299
|
-
options.push({icon: 'link@lu', label: 'Link', format: 'link',
|
|
290
|
+
options.push({icon: 'link@lu', label: 'Link', format: 'link', formatValue: true, keywords: 'link', order: orderCounter++});
|
|
300
291
|
}
|
|
301
292
|
if (this._featureConfig.dividersEnabled !== false) {
|
|
302
|
-
options.push({icon: 'minus@lu', label: 'Divider', format: 'divider', order: orderCounter++});
|
|
293
|
+
options.push({icon: 'minus@lu', label: 'Divider', format: 'divider', keywords: 'divider,hr', order: orderCounter++});
|
|
303
294
|
}
|
|
304
295
|
if (this._featureConfig.attachmentsEnabled !== false) {
|
|
305
|
-
options.push({icon: 'paperclip@lu', label: 'Attachment', format: 'attachment', order: orderCounter++});
|
|
296
|
+
options.push({icon: 'paperclip@lu', label: 'Attachment', format: 'attachment', keywords: 'attachment', order: orderCounter++});
|
|
306
297
|
}
|
|
307
298
|
if (this._featureConfig.imagesEnabled !== false) {
|
|
308
|
-
options.push({icon: 'image@lu', label: 'Image', format: 'image', order: orderCounter++});
|
|
299
|
+
options.push({icon: 'image@lu', label: 'Image', format: 'image', keywords: 'image', order: orderCounter++});
|
|
309
300
|
}
|
|
310
301
|
if (this._featureConfig.videosEnabled !== false) {
|
|
311
|
-
options.push({icon: 'video@lu', label: 'Video', format: 'video', order: orderCounter++});
|
|
302
|
+
options.push({icon: 'video@lu', label: 'Video', format: 'video', keywords: 'video', order: orderCounter++});
|
|
312
303
|
}
|
|
313
304
|
if (this._featureConfig.datesEnabled !== false) {
|
|
314
|
-
options.push({icon: 'calendar@lu', label: 'Date', format: 'date', order: orderCounter++});
|
|
305
|
+
options.push({icon: 'calendar@lu', label: 'Date', format: 'date', keywords: 'date', order: orderCounter++});
|
|
315
306
|
}
|
|
316
|
-
options.push({icon: 'remove-formatting@lu', label: 'Clear Formatting', format: 'clean', order: orderCounter++});
|
|
317
|
-
|
|
318
|
-
// Sort options by order property
|
|
319
|
-
options.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
307
|
+
options.push({icon: 'remove-formatting@lu', label: 'Clear Formatting', format: 'clean', keywords: 'clean', order: orderCounter++});
|
|
320
308
|
|
|
321
309
|
return options;
|
|
322
310
|
}
|
|
323
311
|
|
|
324
312
|
private show() {
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
313
|
+
if (this._menu.open) return;
|
|
314
|
+
|
|
315
|
+
this._menu.show();
|
|
316
|
+
this._quill.container.ownerDocument.addEventListener('click', this._docClickHandler);
|
|
329
317
|
}
|
|
330
318
|
|
|
331
319
|
private hide() {
|
|
332
|
-
this._component.hide();
|
|
333
320
|
this._startIndex = -1;
|
|
321
|
+
if (!this._menu.open) return;
|
|
322
|
+
|
|
323
|
+
this._menu.hide();
|
|
334
324
|
this._quill.container.ownerDocument.removeEventListener('click', this._docClickHandler);
|
|
335
325
|
}
|
|
336
326
|
|
|
337
327
|
public isOpen() {
|
|
338
|
-
return this.
|
|
328
|
+
return this._menu?.open ?? false;
|
|
339
329
|
}
|
|
340
330
|
}
|
|
341
331
|
|
|
@@ -331,7 +331,10 @@ class Toolbar extends QuillToolbar {
|
|
|
331
331
|
const isVisible = (el: HTMLElement | null) => (el?.offsetParent !== null);
|
|
332
332
|
|
|
333
333
|
if (isVisible(button)) {
|
|
334
|
-
button
|
|
334
|
+
// zn-button overrides click() without dispatching a DOM event, so the
|
|
335
|
+
// dropdown's trigger listener never hears it — open the dropdown directly
|
|
336
|
+
const dateDropdown = shadow.querySelector('zn-dropdown.toolbar__date-dropdown') as ZnDropdown | null;
|
|
337
|
+
dateDropdown?.show().then();
|
|
335
338
|
return;
|
|
336
339
|
}
|
|
337
340
|
|
|
@@ -137,6 +137,21 @@ export default class ZnSlashMenu extends ZincElement {
|
|
|
137
137
|
disconnectedCallback() {
|
|
138
138
|
super.disconnectedCallback();
|
|
139
139
|
this.stopPositioner();
|
|
140
|
+
this.hidePanelPopover();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private showPanelPopover() {
|
|
144
|
+
const panel = this.panel;
|
|
145
|
+
if (typeof panel?.showPopover === 'function' && !panel.matches(':popover-open')) {
|
|
146
|
+
panel.showPopover();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private hidePanelPopover() {
|
|
151
|
+
const panel = this.panel;
|
|
152
|
+
if (typeof panel?.hidePopover === 'function' && panel.matches(':popover-open')) {
|
|
153
|
+
panel.hidePopover();
|
|
154
|
+
}
|
|
140
155
|
}
|
|
141
156
|
|
|
142
157
|
private startPositioner() {
|
|
@@ -156,6 +171,8 @@ export default class ZnSlashMenu extends ZincElement {
|
|
|
156
171
|
const {anchor, panel} = this;
|
|
157
172
|
if (!this.open || !anchor || !panel) return;
|
|
158
173
|
|
|
174
|
+
this.showPanelPopover();
|
|
175
|
+
|
|
159
176
|
const {x, y} = await computePosition(anchor, panel, {
|
|
160
177
|
placement: this.placement,
|
|
161
178
|
strategy: 'fixed',
|
|
@@ -211,6 +228,7 @@ export default class ZnSlashMenu extends ZincElement {
|
|
|
211
228
|
// mousedown, not click: preventDefault keeps focus (and the caret) in the field
|
|
212
229
|
private readonly handleItemMouseDown = (event: MouseEvent) => {
|
|
213
230
|
event.preventDefault();
|
|
231
|
+
event.stopPropagation();
|
|
214
232
|
|
|
215
233
|
const index = Number((event.currentTarget as HTMLElement).dataset.index);
|
|
216
234
|
const item = this.visibleItems[index];
|
|
@@ -233,20 +251,20 @@ export default class ZnSlashMenu extends ZincElement {
|
|
|
233
251
|
protected updated(changed: PropertyValues) {
|
|
234
252
|
super.updated(changed);
|
|
235
253
|
|
|
236
|
-
// A new list, or a reopen, starts at the top rather than wherever the last one was scrolled to
|
|
237
|
-
if (changed.has('items') || (changed.has('open') && this.open)) {
|
|
238
|
-
this.scrollActiveIntoView();
|
|
239
|
-
}
|
|
240
|
-
|
|
241
254
|
if (changed.has('open') || changed.has('anchor')) {
|
|
242
255
|
if (this.open) {
|
|
243
256
|
this.startPositioner();
|
|
244
257
|
} else {
|
|
245
258
|
this.stopPositioner();
|
|
259
|
+
this.hidePanelPopover();
|
|
246
260
|
}
|
|
247
261
|
}
|
|
248
262
|
|
|
249
263
|
if (this.open) void this.position();
|
|
264
|
+
|
|
265
|
+
if (changed.has('items') || (changed.has('open') && this.open)) {
|
|
266
|
+
this.scrollActiveIntoView();
|
|
267
|
+
}
|
|
250
268
|
}
|
|
251
269
|
|
|
252
270
|
private renderItem(item: SlashMenuItem, index: number, showIcons: boolean) {
|
|
@@ -310,6 +328,7 @@ export default class ZnSlashMenu extends ZincElement {
|
|
|
310
328
|
<div
|
|
311
329
|
part="panel"
|
|
312
330
|
class="slash-menu__panel"
|
|
331
|
+
popover="manual"
|
|
313
332
|
role="listbox"
|
|
314
333
|
aria-hidden=${this.open ? 'false' : 'true'}
|
|
315
334
|
aria-label=${this.query ? `Matches for ${this.query}` : this.heading}>
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
display: flex;
|
|
16
16
|
flex-direction: column;
|
|
17
17
|
position: fixed;
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
inset: 0 auto auto 0;
|
|
19
|
+
margin: 0;
|
|
20
20
|
width: var(--slash-menu-width);
|
|
21
21
|
max-width: var(--auto-size-available-width, none);
|
|
22
22
|
max-height: min(var(--slash-menu-max-height), var(--auto-size-available-height, 100vh));
|
|
@@ -63,10 +63,22 @@ describe('<zn-slash-menu>', () => {
|
|
|
63
63
|
|
|
64
64
|
const item = el.shadowRoot!.querySelector<HTMLButtonElement>('[data-slash-item]')!;
|
|
65
65
|
const event = new MouseEvent('mousedown', {bubbles: true, cancelable: true, composed: true});
|
|
66
|
-
|
|
66
|
+
|
|
67
|
+
let reachedDocument = false;
|
|
68
|
+
const documentListener = () => {
|
|
69
|
+
reachedDocument = true;
|
|
70
|
+
};
|
|
71
|
+
document.addEventListener('mousedown', documentListener);
|
|
72
|
+
try {
|
|
73
|
+
item.dispatchEvent(event);
|
|
74
|
+
} finally {
|
|
75
|
+
document.removeEventListener('mousedown', documentListener);
|
|
76
|
+
}
|
|
67
77
|
|
|
68
78
|
expect(selected).to.deep.equal(['Brand name']);
|
|
69
79
|
expect(event.defaultPrevented, 'mousedown is prevented so the field keeps focus').to.be.true;
|
|
80
|
+
// A selection may open another overlay; the mousedown must not leak to its outside-click dismisser
|
|
81
|
+
expect(reachedDocument, 'mousedown does not bubble to document').to.be.false;
|
|
70
82
|
});
|
|
71
83
|
|
|
72
84
|
it('reports how many matches were not rendered', async () => {
|