@kubex/zinc 1.1.87 → 1.1.89
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 +1045 -1045
- package/dist/vscode.html-custom-data.json +89 -89
- package/dist/web-types.json +215 -215
- package/dist/zn.d.ts +6 -32
- package/dist/zn.min.js +229 -248
- 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 +1 -0
- package/src/components/slash-menu/slash-menu.test.ts +13 -1
- package/src/components/theme-editor/theme-editor.component.ts +0 -1
- package/src/components/theme-editor/theme-editor.scss +0 -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
|
|
|
@@ -228,6 +228,7 @@ export default class ZnSlashMenu extends ZincElement {
|
|
|
228
228
|
// mousedown, not click: preventDefault keeps focus (and the caret) in the field
|
|
229
229
|
private readonly handleItemMouseDown = (event: MouseEvent) => {
|
|
230
230
|
event.preventDefault();
|
|
231
|
+
event.stopPropagation();
|
|
231
232
|
|
|
232
233
|
const index = Number((event.currentTarget as HTMLElement).dataset.index);
|
|
233
234
|
const item = this.visibleItems[index];
|
|
@@ -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 () => {
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
|
|
2
|
-
import {property, state} from 'lit/decorators.js';
|
|
3
|
-
import ZincElement from "../../../../internal/zinc-element";
|
|
4
|
-
|
|
5
|
-
import styles from './context-menu.scss';
|
|
6
|
-
|
|
7
|
-
export interface ResultItem {
|
|
8
|
-
icon: string;
|
|
9
|
-
label: string;
|
|
10
|
-
format?: string;
|
|
11
|
-
key?: string;
|
|
12
|
-
value?: string | boolean;
|
|
13
|
-
order?: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export default class ContextMenuComponent extends ZincElement {
|
|
17
|
-
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
18
|
-
|
|
19
|
-
@property({type: Boolean, reflect: true}) open = false;
|
|
20
|
-
@property({type: String}) query = '';
|
|
21
|
-
@property({type: Array}) results: ResultItem[] = [];
|
|
22
|
-
|
|
23
|
-
@state() private _activeIndex = -1;
|
|
24
|
-
|
|
25
|
-
public show() {
|
|
26
|
-
this.open = true;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
public hide() {
|
|
30
|
-
this.open = false;
|
|
31
|
-
this._activeIndex = -1;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
setPosition(left: number, top: number) {
|
|
35
|
-
this.style.left = `${Math.max(0, left)}px`;
|
|
36
|
-
this.style.top = `${top}px`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
setActiveIndex(index: number) {
|
|
40
|
-
const len = this.results?.length ?? 0;
|
|
41
|
-
if (!len) {
|
|
42
|
-
this._activeIndex = -1;
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (index < 0) {
|
|
47
|
-
index = len - 1;
|
|
48
|
-
}
|
|
49
|
-
if (index >= len) {
|
|
50
|
-
index = 0;
|
|
51
|
-
}
|
|
52
|
-
this._activeIndex = index;
|
|
53
|
-
this.requestUpdate();
|
|
54
|
-
|
|
55
|
-
requestAnimationFrame(() => {
|
|
56
|
-
const items = Array.from(this.renderRoot.querySelectorAll<HTMLButtonElement>('[data-toolbar-option]'));
|
|
57
|
-
const active = items[this._activeIndex];
|
|
58
|
-
active?.scrollIntoView?.({block: 'nearest'});
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
getActiveIndex() {
|
|
63
|
-
return this._activeIndex;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private _onClickItem = (e: MouseEvent) => {
|
|
67
|
-
const target = e.currentTarget as HTMLElement | null;
|
|
68
|
-
if (!target) return;
|
|
69
|
-
|
|
70
|
-
const idx = parseInt(target.dataset.index || '-1', 10);
|
|
71
|
-
if (Number.isNaN(idx)) return;
|
|
72
|
-
|
|
73
|
-
this.setActiveIndex(idx);
|
|
74
|
-
|
|
75
|
-
const item = this.results?.[idx];
|
|
76
|
-
if (!item) return;
|
|
77
|
-
|
|
78
|
-
this.dispatchEvent(new CustomEvent('zn-format-select', {
|
|
79
|
-
bubbles: true,
|
|
80
|
-
composed: true,
|
|
81
|
-
detail: {icon: item.icon, label: item.label, format: item.format, value: item.value, key: item.key}
|
|
82
|
-
}));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
protected willUpdate(changed: PropertyValues) {
|
|
86
|
-
if (changed.has('results')) {
|
|
87
|
-
// Reset active index when results change
|
|
88
|
-
this._activeIndex = this.results?.length ? 0 : -1;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
render() {
|
|
93
|
-
return html`
|
|
94
|
-
<div class="header">${this.query ? `Search: ${this.query}` : 'Options'}</div>
|
|
95
|
-
${Array.isArray(this.results) && this.results.length > 0 ? (
|
|
96
|
-
this.results.slice(0, 20).map((res, i) => html`
|
|
97
|
-
<button
|
|
98
|
-
type="button"
|
|
99
|
-
class="item"
|
|
100
|
-
role="option"
|
|
101
|
-
aria-selected="${String(i === this._activeIndex)}"
|
|
102
|
-
data-toolbar-option
|
|
103
|
-
data-index="${i}"
|
|
104
|
-
@click="${this._onClickItem}"
|
|
105
|
-
>
|
|
106
|
-
<zn-icon src="${res.icon}" size="16"></zn-icon>
|
|
107
|
-
<span class="label">${res.label}</span>
|
|
108
|
-
</button>
|
|
109
|
-
`)
|
|
110
|
-
) : html`
|
|
111
|
-
<div class="empty">${this.query ? 'No results' : 'Type something'}</div>`}
|
|
112
|
-
`;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
ContextMenuComponent.define('zn-context-menu');
|