@jungherz-de/notionkit-elements 1.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.
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/components/nk-ai-input-row.js +57 -0
- package/dist/components/nk-ai-msg.js +67 -0
- package/dist/components/nk-ai-thread.js +14 -0
- package/dist/components/nk-app.js +23 -0
- package/dist/components/nk-avatar-group.js +35 -0
- package/dist/components/nk-banner.js +30 -0
- package/dist/components/nk-block-host.js +35 -0
- package/dist/components/nk-board-view.js +129 -0
- package/dist/components/nk-breadcrumb.js +78 -0
- package/dist/components/nk-btn.js +107 -0
- package/dist/components/nk-callout.js +36 -0
- package/dist/components/nk-check.js +77 -0
- package/dist/components/nk-cmdk.js +221 -0
- package/dist/components/nk-code.js +66 -0
- package/dist/components/nk-comment.js +41 -0
- package/dist/components/nk-comments.js +57 -0
- package/dist/components/nk-danger-zone.js +27 -0
- package/dist/components/nk-database.js +100 -0
- package/dist/components/nk-divider.js +12 -0
- package/dist/components/nk-emoji-picker.js +112 -0
- package/dist/components/nk-empty.js +36 -0
- package/dist/components/nk-field.js +50 -0
- package/dist/components/nk-filter-bar.js +78 -0
- package/dist/components/nk-heading.js +29 -0
- package/dist/components/nk-input.js +108 -0
- package/dist/components/nk-kbd.js +15 -0
- package/dist/components/nk-member-list.js +30 -0
- package/dist/components/nk-member-row.js +46 -0
- package/dist/components/nk-mention.js +31 -0
- package/dist/components/nk-menu-item.js +68 -0
- package/dist/components/nk-menu.js +45 -0
- package/dist/components/nk-modal.js +166 -0
- package/dist/components/nk-model-card.js +89 -0
- package/dist/components/nk-page-actions.js +15 -0
- package/dist/components/nk-page-cover.js +35 -0
- package/dist/components/nk-page-title.js +71 -0
- package/dist/components/nk-page.js +71 -0
- package/dist/components/nk-pop.js +90 -0
- package/dist/components/nk-profile-row.js +25 -0
- package/dist/components/nk-progress.js +43 -0
- package/dist/components/nk-quote.js +32 -0
- package/dist/components/nk-radio.js +160 -0
- package/dist/components/nk-section-label.js +42 -0
- package/dist/components/nk-segmented.js +99 -0
- package/dist/components/nk-select.js +127 -0
- package/dist/components/nk-settings-pane.js +40 -0
- package/dist/components/nk-settings-user.js +36 -0
- package/dist/components/nk-sidebar.js +81 -0
- package/dist/components/nk-skeleton.js +31 -0
- package/dist/components/nk-slider.js +85 -0
- package/dist/components/nk-stat.js +40 -0
- package/dist/components/nk-stats.js +14 -0
- package/dist/components/nk-switch.js +69 -0
- package/dist/components/nk-synced.js +26 -0
- package/dist/components/nk-tab.js +54 -0
- package/dist/components/nk-table-view.js +109 -0
- package/dist/components/nk-tabs.js +82 -0
- package/dist/components/nk-tag.js +32 -0
- package/dist/components/nk-template-btn.js +43 -0
- package/dist/components/nk-textarea.js +95 -0
- package/dist/components/nk-theme-toggle.js +67 -0
- package/dist/components/nk-toast.js +51 -0
- package/dist/components/nk-todo.js +73 -0
- package/dist/components/nk-toggle.js +56 -0
- package/dist/components/nk-topbar.js +24 -0
- package/dist/components/nk-tree-item.js +155 -0
- package/dist/components/nk-tree.js +123 -0
- package/dist/components/nk-workspace-switcher.js +82 -0
- package/dist/components/shared/base-6gEcoatG.js +317 -0
- package/dist/components/shared/focus-DXrK9oLe.js +47 -0
- package/dist/components/shared/property-cell-Dn91uFkf.js +92 -0
- package/dist/notionkit-elements.esm.js +4639 -0
- package/dist/notionkit-elements.js +4718 -0
- package/dist/notionkit-elements.min.js +1 -0
- package/package.json +72 -0
- package/src/base.js +296 -0
- package/src/components/content/nk-callout.js +35 -0
- package/src/components/content/nk-code.js +51 -0
- package/src/components/content/nk-divider.js +11 -0
- package/src/components/content/nk-heading.js +28 -0
- package/src/components/content/nk-kbd.js +14 -0
- package/src/components/content/nk-progress.js +42 -0
- package/src/components/content/nk-quote.js +31 -0
- package/src/components/content/nk-tag.js +31 -0
- package/src/components/content/nk-todo.js +72 -0
- package/src/components/content/nk-toggle.js +55 -0
- package/src/components/data/nk-ai-input-row.js +56 -0
- package/src/components/data/nk-ai-msg.js +66 -0
- package/src/components/data/nk-ai-thread.js +13 -0
- package/src/components/data/nk-board-view.js +128 -0
- package/src/components/data/nk-comment.js +40 -0
- package/src/components/data/nk-comments.js +56 -0
- package/src/components/data/nk-database.js +99 -0
- package/src/components/data/nk-filter-bar.js +77 -0
- package/src/components/data/nk-table-view.js +108 -0
- package/src/components/forms/nk-btn.js +106 -0
- package/src/components/forms/nk-check.js +76 -0
- package/src/components/forms/nk-field.js +49 -0
- package/src/components/forms/nk-input.js +107 -0
- package/src/components/forms/nk-radio.js +159 -0
- package/src/components/forms/nk-select.js +126 -0
- package/src/components/forms/nk-slider.js +84 -0
- package/src/components/forms/nk-switch.js +68 -0
- package/src/components/forms/nk-textarea.js +94 -0
- package/src/components/overlays/nk-cmdk.js +201 -0
- package/src/components/overlays/nk-emoji-picker.js +94 -0
- package/src/components/overlays/nk-menu-item.js +67 -0
- package/src/components/overlays/nk-menu.js +44 -0
- package/src/components/overlays/nk-modal.js +165 -0
- package/src/components/overlays/nk-pop.js +89 -0
- package/src/components/overlays/nk-settings-pane.js +39 -0
- package/src/components/overlays/nk-settings-user.js +35 -0
- package/src/components/overlays/nk-toast.js +50 -0
- package/src/components/page/nk-avatar-group.js +34 -0
- package/src/components/page/nk-banner.js +29 -0
- package/src/components/page/nk-block-host.js +34 -0
- package/src/components/page/nk-danger-zone.js +26 -0
- package/src/components/page/nk-empty.js +35 -0
- package/src/components/page/nk-member-list.js +29 -0
- package/src/components/page/nk-member-row.js +45 -0
- package/src/components/page/nk-mention.js +30 -0
- package/src/components/page/nk-model-card.js +88 -0
- package/src/components/page/nk-page-actions.js +14 -0
- package/src/components/page/nk-page-cover.js +34 -0
- package/src/components/page/nk-page-title.js +70 -0
- package/src/components/page/nk-page.js +70 -0
- package/src/components/page/nk-profile-row.js +24 -0
- package/src/components/page/nk-segmented.js +98 -0
- package/src/components/page/nk-skeleton.js +30 -0
- package/src/components/page/nk-stat.js +39 -0
- package/src/components/page/nk-stats.js +13 -0
- package/src/components/page/nk-synced.js +25 -0
- package/src/components/page/nk-tab.js +53 -0
- package/src/components/page/nk-tabs.js +81 -0
- package/src/components/page/nk-template-btn.js +42 -0
- package/src/components/shell/nk-app.js +22 -0
- package/src/components/shell/nk-breadcrumb.js +77 -0
- package/src/components/shell/nk-section-label.js +41 -0
- package/src/components/shell/nk-sidebar.js +80 -0
- package/src/components/shell/nk-theme-toggle.js +66 -0
- package/src/components/shell/nk-topbar.js +23 -0
- package/src/components/shell/nk-tree-item.js +154 -0
- package/src/components/shell/nk-tree.js +122 -0
- package/src/components/shell/nk-workspace-switcher.js +81 -0
- package/src/index.js +88 -0
- package/src/util/emoji.js +17 -0
- package/src/util/focus.js +45 -0
- package/src/util/fuzzy.js +19 -0
- package/src/util/highlight.js +14 -0
- package/src/util/property-cell.js +90 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { NkFormElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-slider name="size" min="12" max="18" value="14" show-value></nk-slider>
|
|
4
|
+
// → <input type="range" class="nk-slider"> <div class="nk-slider-value">14</div>
|
|
5
|
+
const FORWARDED = ['name', 'min', 'max', 'step', 'aria-label'];
|
|
6
|
+
|
|
7
|
+
class NkSlider extends NkFormElement {
|
|
8
|
+
static get observedAttributes() { return ['value', 'disabled', 'show-value', 'unit', ...FORWARDED]; }
|
|
9
|
+
|
|
10
|
+
render() {
|
|
11
|
+
this._input = this.createElement('input', ['nk-slider'], { type: 'range' });
|
|
12
|
+
this._out = this.createElement('div', ['nk-slider-value']);
|
|
13
|
+
this._apply();
|
|
14
|
+
this._input.value = this.getAttribute('value') ?? this._input.value;
|
|
15
|
+
this._defaultValue = this._input.value;
|
|
16
|
+
this._wrapper.appendChild(this._input);
|
|
17
|
+
this._wrapper.appendChild(this._out);
|
|
18
|
+
this._syncOut();
|
|
19
|
+
this._syncFormValue();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_apply() {
|
|
23
|
+
for (const attr of FORWARDED) {
|
|
24
|
+
const v = this.getAttribute(attr);
|
|
25
|
+
v === null ? this._input.removeAttribute(attr) : this._input.setAttribute(attr, v);
|
|
26
|
+
}
|
|
27
|
+
this._input.disabled = this.getBoolAttr('disabled') || !!this._formDisabled;
|
|
28
|
+
this._out.style.display = this.getBoolAttr('show-value') ? '' : 'none';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_syncOut() {
|
|
32
|
+
this._out.textContent = `${this._input.value}${this.getAttribute('unit') || ''}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setupEvents() {
|
|
36
|
+
this._onInput = () => {
|
|
37
|
+
this._syncOut();
|
|
38
|
+
this._syncFormValue();
|
|
39
|
+
this.emit('nk-input', { value: Number(this._input.value), name: this.name });
|
|
40
|
+
};
|
|
41
|
+
this._onChange = () => {
|
|
42
|
+
this._syncFormValue();
|
|
43
|
+
this.emit('nk-change', { value: Number(this._input.value), name: this.name });
|
|
44
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
45
|
+
};
|
|
46
|
+
this._input.addEventListener('input', this._onInput);
|
|
47
|
+
this._input.addEventListener('change', this._onChange);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
teardownEvents() {
|
|
51
|
+
this._input?.removeEventListener('input', this._onInput);
|
|
52
|
+
this._input?.removeEventListener('change', this._onChange);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onAttributeChanged(name) {
|
|
56
|
+
if (!this._input) return;
|
|
57
|
+
if (name === 'value') this._input.value = this.getAttribute('value') ?? '';
|
|
58
|
+
else this._apply();
|
|
59
|
+
this._syncOut();
|
|
60
|
+
this._syncFormValue();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_syncFormValue() { this.setFormValue(this._input.value); }
|
|
64
|
+
resetValue() { this._input.value = this._defaultValue; this._syncOut(); this._syncFormValue(); }
|
|
65
|
+
restoreValue(state) { if (typeof state === 'string') { this._input.value = state; this._syncOut(); this._syncFormValue(); } }
|
|
66
|
+
onFormDisabled(disabled) { this._formDisabled = disabled; this._apply(); }
|
|
67
|
+
|
|
68
|
+
focus(options) { this._input?.focus(options); }
|
|
69
|
+
|
|
70
|
+
get value() { return Number(this._input?.value ?? this.getAttribute('value')); }
|
|
71
|
+
set value(v) {
|
|
72
|
+
if (this._input) { this._input.value = v; this._syncOut(); this._syncFormValue(); }
|
|
73
|
+
else this.setAttribute('value', v);
|
|
74
|
+
}
|
|
75
|
+
get name() { return this.getAttribute('name'); }
|
|
76
|
+
set name(v) { this.setAttribute('name', v); }
|
|
77
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
78
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
79
|
+
get showValue() { return this.getBoolAttr('show-value'); }
|
|
80
|
+
set showValue(v) { this.setBoolAttr('show-value', v); }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
customElements.define('nk-slider', NkSlider);
|
|
84
|
+
export { NkSlider };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { NkFormElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-switch name="notify" checked></nk-switch>
|
|
4
|
+
// → <button class="nk-switch" role="switch" aria-checked="true">
|
|
5
|
+
// notionkit.css styles the state through [aria-checked]; the toggling is here.
|
|
6
|
+
class NkSwitch extends NkFormElement {
|
|
7
|
+
static get observedAttributes() { return ['checked', 'disabled', 'name', 'value', 'label']; }
|
|
8
|
+
|
|
9
|
+
render() {
|
|
10
|
+
this._btn = this.createElement('button', ['nk-switch'], { type: 'button', role: 'switch' });
|
|
11
|
+
this._defaultChecked = this.getBoolAttr('checked');
|
|
12
|
+
this._apply();
|
|
13
|
+
this._wrapper.appendChild(this._btn);
|
|
14
|
+
this._syncFormValue();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_apply() {
|
|
18
|
+
this._btn.setAttribute('aria-checked', this.getBoolAttr('checked') ? 'true' : 'false');
|
|
19
|
+
this._btn.disabled = this.getBoolAttr('disabled') || !!this._formDisabled;
|
|
20
|
+
const label = this.getAttribute('label');
|
|
21
|
+
label ? this._btn.setAttribute('aria-label', label) : this._btn.removeAttribute('aria-label');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setupEvents() {
|
|
25
|
+
this._onClick = () => this.toggle();
|
|
26
|
+
this._btn.addEventListener('click', this._onClick);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
teardownEvents() {
|
|
30
|
+
this._btn?.removeEventListener('click', this._onClick);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
toggle() {
|
|
34
|
+
if (this.getBoolAttr('disabled') || this._formDisabled) return;
|
|
35
|
+
this.checked = !this.checked;
|
|
36
|
+
this.emit('nk-change', { checked: this.checked, value: this.getAttribute('value') || 'on', name: this.name });
|
|
37
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onAttributeChanged() {
|
|
41
|
+
if (!this._btn) return;
|
|
42
|
+
this._apply();
|
|
43
|
+
this._syncFormValue();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_syncFormValue() {
|
|
47
|
+
const val = this.getAttribute('value') || 'on';
|
|
48
|
+
this.setFormValue(this.getBoolAttr('checked') ? val : null, this.getBoolAttr('checked') ? 'checked' : 'unchecked');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
resetValue() { this.setBoolAttr('checked', this._defaultChecked); }
|
|
52
|
+
restoreValue(state) { if (state === 'checked' || state === 'unchecked') this.setBoolAttr('checked', state === 'checked'); }
|
|
53
|
+
onFormDisabled(disabled) { this._formDisabled = disabled; this._apply(); }
|
|
54
|
+
|
|
55
|
+
focus(options) { this._btn?.focus(options); }
|
|
56
|
+
|
|
57
|
+
get checked() { return this.getBoolAttr('checked'); }
|
|
58
|
+
set checked(v) { this.setBoolAttr('checked', v); }
|
|
59
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
60
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
61
|
+
get name() { return this.getAttribute('name'); }
|
|
62
|
+
set name(v) { this.setAttribute('name', v); }
|
|
63
|
+
get value() { return this.getAttribute('value') || 'on'; }
|
|
64
|
+
set value(v) { this.setAttribute('value', v); }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
customElements.define('nk-switch', NkSwitch);
|
|
68
|
+
export { NkSwitch };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { NkFormElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-textarea name="bio" rows="3" wide>Text</nk-textarea> → <textarea class="nk-textarea wide">
|
|
4
|
+
// The initial value is the `value` attribute, or the element's text content.
|
|
5
|
+
const FORWARDED = ['placeholder', 'name', 'rows', 'maxlength', 'minlength', 'autocomplete', 'aria-label'];
|
|
6
|
+
const BOOLEANS = ['required', 'disabled', 'readonly'];
|
|
7
|
+
|
|
8
|
+
class NkTextarea extends NkFormElement {
|
|
9
|
+
static get observedAttributes() { return ['value', 'wide', ...FORWARDED, ...BOOLEANS]; }
|
|
10
|
+
|
|
11
|
+
render() {
|
|
12
|
+
this._input = this.createElement('textarea', ['nk-textarea']);
|
|
13
|
+
for (const attr of [...FORWARDED, ...BOOLEANS]) this._forward(attr);
|
|
14
|
+
this._input.value = this.getAttribute('value') ?? this.textContent.trim();
|
|
15
|
+
this._syncWide();
|
|
16
|
+
this._wrapper.appendChild(this._input);
|
|
17
|
+
this._syncFormValue();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_forward(attr) {
|
|
21
|
+
if (BOOLEANS.includes(attr)) {
|
|
22
|
+
this._input[attr === 'readonly' ? 'readOnly' : attr] = this.getBoolAttr(attr);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const v = this.getAttribute(attr);
|
|
26
|
+
if (v === null) this._input.removeAttribute(attr);
|
|
27
|
+
else this._input.setAttribute(attr, v);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_syncWide() { this._input.classList.toggle('wide', this.getBoolAttr('wide')); }
|
|
31
|
+
|
|
32
|
+
setupEvents() {
|
|
33
|
+
this._onInput = () => {
|
|
34
|
+
this._syncFormValue();
|
|
35
|
+
this.emit('nk-input', { value: this._input.value, name: this.name });
|
|
36
|
+
};
|
|
37
|
+
this._onChange = () => {
|
|
38
|
+
this._syncFormValue();
|
|
39
|
+
this.emit('nk-change', { value: this._input.value, name: this.name });
|
|
40
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
41
|
+
};
|
|
42
|
+
this._input.addEventListener('input', this._onInput);
|
|
43
|
+
this._input.addEventListener('change', this._onChange);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
teardownEvents() {
|
|
47
|
+
this._input?.removeEventListener('input', this._onInput);
|
|
48
|
+
this._input?.removeEventListener('change', this._onChange);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
onAttributeChanged(name) {
|
|
52
|
+
if (!this._input) return;
|
|
53
|
+
if (name === 'value') { this._input.value = this.getAttribute('value') ?? ''; this._syncFormValue(); return; }
|
|
54
|
+
if (name === 'wide') { this._syncWide(); return; }
|
|
55
|
+
this._forward(name);
|
|
56
|
+
this._syncFormValue();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_syncFormValue() {
|
|
60
|
+
this.setFormValue(this._input.value);
|
|
61
|
+
this.syncValidityFrom(this._input);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
resetValue() {
|
|
65
|
+
this._input.value = this.getAttribute('value') ?? this.textContent.trim();
|
|
66
|
+
this._syncFormValue();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
restoreValue(state) {
|
|
70
|
+
if (typeof state === 'string') { this._input.value = state; this._syncFormValue(); }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
onFormDisabled(disabled) { this._input.disabled = disabled || this.getBoolAttr('disabled'); }
|
|
74
|
+
|
|
75
|
+
focus(options) { this._input?.focus(options); }
|
|
76
|
+
blur() { this._input?.blur(); }
|
|
77
|
+
|
|
78
|
+
get value() { return this._input?.value ?? (this.getAttribute('value') ?? ''); }
|
|
79
|
+
set value(v) {
|
|
80
|
+
if (this._input) { this._input.value = v ?? ''; this._syncFormValue(); }
|
|
81
|
+
else this.setAttribute('value', v ?? '');
|
|
82
|
+
}
|
|
83
|
+
get name() { return this.getAttribute('name'); }
|
|
84
|
+
set name(v) { this.setAttribute('name', v); }
|
|
85
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
86
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
87
|
+
get required() { return this.getBoolAttr('required'); }
|
|
88
|
+
set required(v) { this.setBoolAttr('required', v); }
|
|
89
|
+
get wide() { return this.getBoolAttr('wide'); }
|
|
90
|
+
set wide(v) { this.setBoolAttr('wide', v); }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
customElements.define('nk-textarea', NkTextarea);
|
|
94
|
+
export { NkTextarea };
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { NkElement } from '../../base.js';
|
|
2
|
+
import { lockScroll, unlockScroll, inertOutside } from '../../util/focus.js';
|
|
3
|
+
import { fuzzyScore } from '../../util/fuzzy.js';
|
|
4
|
+
|
|
5
|
+
// <nk-cmdk id="palette" placeholder="Search or type a command …"></nk-cmdk>
|
|
6
|
+
// palette.commands = [
|
|
7
|
+
// { group: 'Pages', items: [{ id: 'mvp', icon: '🚀', label: 'NotionKit MVP', keywords: 'project' }] },
|
|
8
|
+
// { group: 'Actions', items: [{ id: 'theme', icon: '🌙', label: 'Toggle theme', shortcut: '⌘⇧L', action: () => … }] },
|
|
9
|
+
// ];
|
|
10
|
+
// → <div class="nk-cmdk-backdrop open"><div class="nk-cmdk"><div class="nk-cmdk-input-row">🔍<input><kbd>esc</kbd></div>
|
|
11
|
+
// <div class="nk-cmdk-list">…</div><div class="nk-cmdk-footer">…</div></div></div>
|
|
12
|
+
// mod+K toggles (attribute `hotkey`), ↑↓ move, Enter picks (nk-command +
|
|
13
|
+
// item.action), Escape / backdrop close. Fuzzy subsequence search over
|
|
14
|
+
// label + keywords. Put the element directly under <body>.
|
|
15
|
+
class NkCmdk extends NkElement {
|
|
16
|
+
static get observedAttributes() { return ['open', 'hotkey', 'placeholder']; }
|
|
17
|
+
|
|
18
|
+
render() {
|
|
19
|
+
this._backdrop = this.createElement('div', ['nk-cmdk-backdrop']);
|
|
20
|
+
this._backdrop.inert = true;
|
|
21
|
+
this._box = this.createElement('div', ['nk-cmdk'], { role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Command palette' });
|
|
22
|
+
const row = this.createElement('div', ['nk-cmdk-input-row']);
|
|
23
|
+
const glass = document.createElement('span');
|
|
24
|
+
glass.style.fontSize = '15px';
|
|
25
|
+
glass.textContent = '🔍';
|
|
26
|
+
this._input = this.createElement('input', [], { type: 'text', autocomplete: 'off', role: 'combobox', 'aria-expanded': 'true', 'aria-autocomplete': 'list' });
|
|
27
|
+
const esc = this.createElement('kbd', ['nk-kbd']);
|
|
28
|
+
esc.textContent = 'esc';
|
|
29
|
+
row.append(glass, this._input, esc);
|
|
30
|
+
this._list = this.createElement('div', ['nk-cmdk-list'], { role: 'listbox', id: 'list' });
|
|
31
|
+
this._input.setAttribute('aria-controls', 'list');
|
|
32
|
+
this._footer = this.createElement('div', ['nk-cmdk-footer']);
|
|
33
|
+
const footerSlot = this.createElement('slot', [], { name: 'footer' });
|
|
34
|
+
footerSlot.innerHTML = '<span><kbd class="nk-kbd">↑</kbd><kbd class="nk-kbd">↓</kbd> navigate</span><span><kbd class="nk-kbd">↵</kbd> open</span><span><kbd class="nk-kbd">⌘</kbd><kbd class="nk-kbd">K</kbd> toggle</span>';
|
|
35
|
+
this._footer.appendChild(footerSlot);
|
|
36
|
+
this._box.append(row, this._list, this._footer);
|
|
37
|
+
this._backdrop.appendChild(this._box);
|
|
38
|
+
this._wrapper.appendChild(this._backdrop);
|
|
39
|
+
this._commands = [];
|
|
40
|
+
this._index = 0;
|
|
41
|
+
this._syncAttrs();
|
|
42
|
+
this._renderList();
|
|
43
|
+
this._syncOpen();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_syncAttrs() { this._input.placeholder = this.getAttribute('placeholder') || 'Search or type a command …'; }
|
|
47
|
+
|
|
48
|
+
/** Filtered, flat list in group order, best matches first within a group. */
|
|
49
|
+
results() {
|
|
50
|
+
const q = this._input?.value ?? '';
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const group of this._commands) {
|
|
53
|
+
const scored = (group.items || []).map(item => ({ item, group: group.group, score: fuzzyScore(q, `${item.label} ${item.keywords || ''}`) })).filter(r => r.score > 0);
|
|
54
|
+
scored.sort((a, b) => b.score - a.score);
|
|
55
|
+
out.push(...scored);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_renderList() {
|
|
61
|
+
if (!this._list) return;
|
|
62
|
+
const results = this.results();
|
|
63
|
+
this._index = Math.min(this._index, Math.max(0, results.length - 1));
|
|
64
|
+
this._list.replaceChildren();
|
|
65
|
+
if (!results.length) {
|
|
66
|
+
const empty = this.createElement('div', ['nk-cmdk-empty']);
|
|
67
|
+
empty.textContent = `No results for “${this._input.value}”`;
|
|
68
|
+
this._list.appendChild(empty);
|
|
69
|
+
this._input.removeAttribute('aria-activedescendant');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let lastGroup = null;
|
|
73
|
+
results.forEach((r, i) => {
|
|
74
|
+
if (r.group && r.group !== lastGroup) {
|
|
75
|
+
const g = this.createElement('div', ['nk-cmdk-group']);
|
|
76
|
+
g.textContent = r.group;
|
|
77
|
+
this._list.appendChild(g);
|
|
78
|
+
lastGroup = r.group;
|
|
79
|
+
}
|
|
80
|
+
const el = this.createElement('div', ['nk-cmdk-item'], { role: 'option', id: `opt-${i}`, 'data-index': i });
|
|
81
|
+
el.classList.toggle('selected', i === this._index);
|
|
82
|
+
el.setAttribute('aria-selected', i === this._index ? 'true' : 'false');
|
|
83
|
+
const icon = this.createElement('span', ['m-icon']);
|
|
84
|
+
icon.textContent = r.item.icon || '';
|
|
85
|
+
const label = document.createElement('span');
|
|
86
|
+
label.textContent = r.item.label;
|
|
87
|
+
el.append(icon, label);
|
|
88
|
+
if (r.item.shortcut) { const sc = this.createElement('span', ['m-shortcut']); sc.textContent = r.item.shortcut; el.appendChild(sc); }
|
|
89
|
+
this._list.appendChild(el);
|
|
90
|
+
});
|
|
91
|
+
this._input.setAttribute('aria-activedescendant', `opt-${this._index}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_move(dir) {
|
|
95
|
+
const n = this.results().length;
|
|
96
|
+
if (!n) return;
|
|
97
|
+
this._index = (this._index + dir + n) % n;
|
|
98
|
+
for (const el of this._list.querySelectorAll('.nk-cmdk-item')) {
|
|
99
|
+
const on = Number(el.dataset.index) === this._index;
|
|
100
|
+
el.classList.toggle('selected', on);
|
|
101
|
+
el.setAttribute('aria-selected', on ? 'true' : 'false');
|
|
102
|
+
if (on) el.scrollIntoView({ block: 'nearest' });
|
|
103
|
+
}
|
|
104
|
+
this._input.setAttribute('aria-activedescendant', `opt-${this._index}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
pick(index = this._index) {
|
|
108
|
+
const r = this.results()[index];
|
|
109
|
+
if (!r) return;
|
|
110
|
+
const query = this._input.value;
|
|
111
|
+
this.close();
|
|
112
|
+
const ok = this.emit('nk-command', { id: r.item.id ?? r.item.label, item: r.item, query });
|
|
113
|
+
if (ok && typeof r.item.action === 'function') r.item.action(r.item);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_matchesHotkey(e) {
|
|
117
|
+
const spec = (this.getAttribute('hotkey') || 'mod+k').toLowerCase().split('+');
|
|
118
|
+
const key = spec.pop();
|
|
119
|
+
const mod = spec.includes('mod') ? (e.metaKey || e.ctrlKey) : true;
|
|
120
|
+
const shift = spec.includes('shift') ? e.shiftKey : !e.shiftKey;
|
|
121
|
+
const alt = spec.includes('alt') ? e.altKey : !e.altKey;
|
|
122
|
+
return mod && shift && alt && e.key.toLowerCase() === key;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_syncOpen() {
|
|
126
|
+
const open = this.getBoolAttr('open');
|
|
127
|
+
this._backdrop.classList.toggle('open', open);
|
|
128
|
+
this._backdrop.inert = !open;
|
|
129
|
+
if (open === this._wasOpen) return;
|
|
130
|
+
this._wasOpen = open;
|
|
131
|
+
if (open) {
|
|
132
|
+
this._returnFocus = document.activeElement;
|
|
133
|
+
this._input.value = '';
|
|
134
|
+
this._index = 0;
|
|
135
|
+
this._renderList();
|
|
136
|
+
lockScroll();
|
|
137
|
+
this._undoInert = inertOutside(this);
|
|
138
|
+
requestAnimationFrame(() => this._input.focus({ preventScroll: true }));
|
|
139
|
+
} else {
|
|
140
|
+
unlockScroll();
|
|
141
|
+
this._undoInert?.(); this._undoInert = null;
|
|
142
|
+
const back = this._returnFocus;
|
|
143
|
+
if (back && typeof back.focus === 'function' && back.isConnected) back.focus({ preventScroll: true });
|
|
144
|
+
this._returnFocus = null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setupEvents() {
|
|
149
|
+
this._onDocKey = (e) => {
|
|
150
|
+
if (this._matchesHotkey(e)) { e.preventDefault(); this.toggle(); return; }
|
|
151
|
+
if (e.key === 'Escape' && this.getBoolAttr('open')) { e.stopPropagation(); this.close(); }
|
|
152
|
+
};
|
|
153
|
+
this._onInput = () => { this._index = 0; this._renderList(); };
|
|
154
|
+
this._onInputKey = (e) => {
|
|
155
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); this._move(1); }
|
|
156
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); this._move(-1); }
|
|
157
|
+
else if (e.key === 'Enter') { e.preventDefault(); this.pick(); }
|
|
158
|
+
};
|
|
159
|
+
this._onListClick = (e) => { const el = e.target.closest('.nk-cmdk-item'); if (el) this.pick(Number(el.dataset.index)); };
|
|
160
|
+
this._onListMove = (e) => { const el = e.target.closest('.nk-cmdk-item'); if (el && Number(el.dataset.index) !== this._index) { this._index = Number(el.dataset.index); this._move(0); } };
|
|
161
|
+
this._onListDown = (e) => e.preventDefault(); // keep the input focused
|
|
162
|
+
this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(); };
|
|
163
|
+
document.addEventListener('keydown', this._onDocKey);
|
|
164
|
+
this._input.addEventListener('input', this._onInput);
|
|
165
|
+
this._input.addEventListener('keydown', this._onInputKey);
|
|
166
|
+
this._list.addEventListener('click', this._onListClick);
|
|
167
|
+
this._list.addEventListener('mousemove', this._onListMove);
|
|
168
|
+
this._list.addEventListener('mousedown', this._onListDown);
|
|
169
|
+
this._backdrop.addEventListener('click', this._onBackdrop);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
teardownEvents() {
|
|
173
|
+
document.removeEventListener('keydown', this._onDocKey);
|
|
174
|
+
this._input?.removeEventListener('input', this._onInput);
|
|
175
|
+
this._input?.removeEventListener('keydown', this._onInputKey);
|
|
176
|
+
this._list?.removeEventListener('click', this._onListClick);
|
|
177
|
+
this._list?.removeEventListener('mousemove', this._onListMove);
|
|
178
|
+
this._list?.removeEventListener('mousedown', this._onListDown);
|
|
179
|
+
this._backdrop?.removeEventListener('click', this._onBackdrop);
|
|
180
|
+
if (this._wasOpen) { unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
onAttributeChanged(name) {
|
|
184
|
+
if (name === 'open') { this._syncOpen(); this.emit('nk-toggle', { open: this.getBoolAttr('open') }); }
|
|
185
|
+
else this._syncAttrs();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
show() { this.setBoolAttr('open', true); }
|
|
189
|
+
close() { this.setBoolAttr('open', false); }
|
|
190
|
+
toggle() { this.setBoolAttr('open', !this.getBoolAttr('open')); }
|
|
191
|
+
|
|
192
|
+
get commands() { return this._commands; }
|
|
193
|
+
set commands(list) { this._commands = Array.isArray(list) ? list : []; this._index = 0; this._renderList(); }
|
|
194
|
+
get open() { return this.getBoolAttr('open'); }
|
|
195
|
+
set open(v) { this.setBoolAttr('open', v); }
|
|
196
|
+
get query() { return this._input?.value ?? ''; }
|
|
197
|
+
set query(v) { if (this._input) { this._input.value = v; this._index = 0; this._renderList(); } }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
customElements.define('nk-cmdk', NkCmdk);
|
|
201
|
+
export { NkCmdk };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { NkElement } from '../../base.js';
|
|
2
|
+
import { EMOJI, EMOJI_CATEGORIES } from '../../util/emoji.js';
|
|
3
|
+
|
|
4
|
+
// <nk-emoji-picker></nk-emoji-picker>
|
|
5
|
+
// → <div class="nk-pop"><input class="nk-emoji-search"><div class="nk-emoji-grid">…</div><div class="nk-emoji-cats">…</div></div>
|
|
6
|
+
// Built-in set with names for search; `picker.emojis = [{ char, name, cat }]`
|
|
7
|
+
// replaces it. A click fires nk-select { emoji }.
|
|
8
|
+
class NkEmojiPicker extends NkElement {
|
|
9
|
+
static get observedAttributes() { return ['placeholder', 'value']; }
|
|
10
|
+
|
|
11
|
+
render() {
|
|
12
|
+
this._box = this.createElement('div', ['nk-pop']);
|
|
13
|
+
this._search = this.createElement('input', ['nk-emoji-search'], { type: 'search', autocomplete: 'off', 'aria-label': 'Search emoji' });
|
|
14
|
+
this._grid = this.createElement('div', ['nk-emoji-grid'], { role: 'listbox' });
|
|
15
|
+
this._cats = this.createElement('div', ['nk-emoji-cats'], { role: 'tablist' });
|
|
16
|
+
this._box.append(this._search, this._grid, this._cats);
|
|
17
|
+
this._wrapper.appendChild(this._box);
|
|
18
|
+
this._emojis = EMOJI;
|
|
19
|
+
this._cat = EMOJI_CATEGORIES[0].id;
|
|
20
|
+
this._renderCats();
|
|
21
|
+
this._renderGrid();
|
|
22
|
+
this._syncAttrs();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_syncAttrs() { this._search.placeholder = this.getAttribute('placeholder') || 'Search…'; }
|
|
26
|
+
|
|
27
|
+
_renderCats() {
|
|
28
|
+
this._cats.replaceChildren();
|
|
29
|
+
const cats = this._categories();
|
|
30
|
+
for (const cat of cats) {
|
|
31
|
+
const el = this.createElement('span', [], { role: 'tab', 'data-cat': cat.id, title: cat.id });
|
|
32
|
+
el.textContent = cat.icon;
|
|
33
|
+
el.classList.toggle('active', cat.id === this._cat);
|
|
34
|
+
this._cats.appendChild(el);
|
|
35
|
+
}
|
|
36
|
+
this._cats.style.display = cats.length > 1 ? '' : 'none';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_categories() {
|
|
40
|
+
if (this._catList) return this._catList;
|
|
41
|
+
const seen = new Map();
|
|
42
|
+
for (const e of this._emojis) if (e.cat && !seen.has(e.cat)) seen.set(e.cat, { id: e.cat, icon: e.icon || EMOJI_CATEGORIES.find(c => c.id === e.cat)?.icon || e.char });
|
|
43
|
+
return [...seen.values()];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_renderGrid() {
|
|
47
|
+
const q = this._search.value.trim().toLowerCase();
|
|
48
|
+
const list = q ? this._emojis.filter(e => (e.name || '').toLowerCase().includes(q) || e.char === q) : this._emojis.filter(e => !e.cat || e.cat === this._cat);
|
|
49
|
+
this._grid.replaceChildren();
|
|
50
|
+
for (const e of list.slice(0, 64)) {
|
|
51
|
+
const s = this.createElement('span', [], { role: 'option', title: e.name || '', 'data-emoji': e.char });
|
|
52
|
+
s.textContent = e.char;
|
|
53
|
+
this._grid.appendChild(s);
|
|
54
|
+
}
|
|
55
|
+
for (const el of this._cats.children) el.classList.toggle('active', !q && el.dataset.cat === this._cat);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setupEvents() {
|
|
59
|
+
this._onInput = () => this._renderGrid();
|
|
60
|
+
this._onGrid = (e) => { const s = e.target.closest('[data-emoji]'); if (s) this.select(s.dataset.emoji); };
|
|
61
|
+
this._onCats = (e) => { const c = e.target.closest('[data-cat]'); if (!c) return; this._cat = c.dataset.cat; this._search.value = ''; this._renderGrid(); };
|
|
62
|
+
this._onKey = (e) => { if (e.key === 'Enter') { const first = this._grid.querySelector('[data-emoji]'); if (first) this.select(first.dataset.emoji); } };
|
|
63
|
+
this._search.addEventListener('input', this._onInput);
|
|
64
|
+
this._search.addEventListener('keydown', this._onKey);
|
|
65
|
+
this._grid.addEventListener('click', this._onGrid);
|
|
66
|
+
this._cats.addEventListener('click', this._onCats);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
teardownEvents() {
|
|
70
|
+
this._search?.removeEventListener('input', this._onInput);
|
|
71
|
+
this._search?.removeEventListener('keydown', this._onKey);
|
|
72
|
+
this._grid?.removeEventListener('click', this._onGrid);
|
|
73
|
+
this._cats?.removeEventListener('click', this._onCats);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
select(emoji) {
|
|
77
|
+
this.setAttribute('value', emoji);
|
|
78
|
+
this.emit('nk-select', { emoji, value: emoji });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
onAttributeChanged() { this._syncAttrs(); }
|
|
82
|
+
focus(o) { this._search?.focus(o); }
|
|
83
|
+
|
|
84
|
+
/** Category strip override: [{ id, icon }]. Emojis without `cat` show in every category. */
|
|
85
|
+
get categories() { return this._categories(); }
|
|
86
|
+
set categories(list) { this._catList = Array.isArray(list) && list.length ? list : null; this._cat = this._categories()[0]?.id; this._renderCats(); this._renderGrid(); }
|
|
87
|
+
get emojis() { return this._emojis; }
|
|
88
|
+
set emojis(list) { this._emojis = Array.isArray(list) ? list : EMOJI; this._cat = this._categories()[0]?.id; this._renderCats(); this._renderGrid(); }
|
|
89
|
+
get value() { return this.getAttribute('value'); }
|
|
90
|
+
set value(v) { this.setAttribute('value', v); }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
customElements.define('nk-emoji-picker', NkEmojiPicker);
|
|
94
|
+
export { NkEmojiPicker };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { NkElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-menu-item icon="✏️" shortcut="⌘E" value="rename">Rename</nk-menu-item>
|
|
4
|
+
// <nk-menu-item type="separator"></nk-menu-item> <nk-menu-item type="label">Danger</nk-menu-item>
|
|
5
|
+
// <nk-menu-item icon="🗑️" danger value="delete">Delete</nk-menu-item>
|
|
6
|
+
// → <div class="nk-menu-item danger"><span class="m-icon">🗑️</span>Delete<span class="m-shortcut">…</span></div>
|
|
7
|
+
class NkMenuItem extends NkElement {
|
|
8
|
+
static get observedAttributes() { return ['icon', 'shortcut', 'danger', 'value', 'type', 'disabled']; }
|
|
9
|
+
|
|
10
|
+
render() { this._build(); }
|
|
11
|
+
|
|
12
|
+
_build() {
|
|
13
|
+
const type = this.getAttribute('type');
|
|
14
|
+
let el;
|
|
15
|
+
if (type === 'separator') {
|
|
16
|
+
el = this.createElement('div', ['nk-menu-sep'], { role: 'separator' });
|
|
17
|
+
} else if (type === 'label') {
|
|
18
|
+
el = this.createElement('div', ['nk-menu-label']);
|
|
19
|
+
el.appendChild(document.createElement('slot'));
|
|
20
|
+
} else {
|
|
21
|
+
el = this.createElement('div', ['nk-menu-item'], { role: 'menuitem', tabindex: '-1' });
|
|
22
|
+
this._icon = this.createElement('span', ['m-icon']);
|
|
23
|
+
const iconSlot = this.createElement('slot', [], { name: 'icon' });
|
|
24
|
+
iconSlot.appendChild(this._icon);
|
|
25
|
+
this._shortcut = this.createElement('span', ['m-shortcut']);
|
|
26
|
+
el.append(iconSlot, document.createElement('slot'), this._shortcut);
|
|
27
|
+
}
|
|
28
|
+
if (this._el) { this._el.removeEventListener('click', this._onClick); this._el.replaceWith(el); } else this._wrapper.appendChild(el);
|
|
29
|
+
this._el = el;
|
|
30
|
+
if (this._onClick) el.addEventListener('click', this._onClick);
|
|
31
|
+
this._sync();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_sync() {
|
|
35
|
+
if (!this._el.classList.contains('nk-menu-item')) return;
|
|
36
|
+
const icon = this.getAttribute('icon');
|
|
37
|
+
this._icon.textContent = icon || '';
|
|
38
|
+
this._icon.style.display = icon ? '' : 'none';
|
|
39
|
+
const sc = this.getAttribute('shortcut');
|
|
40
|
+
this._shortcut.textContent = sc || '';
|
|
41
|
+
this._shortcut.style.display = sc ? '' : 'none';
|
|
42
|
+
this._el.classList.toggle('danger', this.getBoolAttr('danger'));
|
|
43
|
+
this._el.setAttribute('aria-disabled', this.getBoolAttr('disabled') ? 'true' : 'false');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setupEvents() {
|
|
47
|
+
this._onClick = () => this.select();
|
|
48
|
+
this._el.addEventListener('click', this._onClick);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
teardownEvents() { this._el?.removeEventListener('click', this._onClick); }
|
|
52
|
+
|
|
53
|
+
select() {
|
|
54
|
+
if (!this._el.classList.contains('nk-menu-item') || this.getBoolAttr('disabled')) return;
|
|
55
|
+
this.emit('nk-select', { value: this.value, label: this.textContent.trim(), item: this });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
onAttributeChanged(name) { name === 'type' ? this._build() : this._sync(); }
|
|
59
|
+
focus(o) { this._el?.focus(o); }
|
|
60
|
+
get selectable() { return this._el?.classList.contains('nk-menu-item') && !this.getBoolAttr('disabled'); }
|
|
61
|
+
|
|
62
|
+
get value() { return this.getAttribute('value') ?? this.textContent.trim(); }
|
|
63
|
+
set value(v) { this.setAttribute('value', v); }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
customElements.define('nk-menu-item', NkMenuItem);
|
|
67
|
+
export { NkMenuItem };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NkElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-menu>
|
|
4
|
+
// <nk-menu-item type="label">Page</nk-menu-item>
|
|
5
|
+
// <nk-menu-item icon="✏️" shortcut="⌘E" value="rename">Rename</nk-menu-item>
|
|
6
|
+
// <nk-menu-item type="separator"></nk-menu-item>
|
|
7
|
+
// <nk-menu-item icon="🗑️" danger value="delete">Delete</nk-menu-item>
|
|
8
|
+
// </nk-menu>
|
|
9
|
+
// → <div class="nk-pop nk-menu" role="menu">…</div>
|
|
10
|
+
// ↑↓ move between items, Enter selects, nk-select bubbles from the item.
|
|
11
|
+
class NkMenu extends NkElement {
|
|
12
|
+
render() {
|
|
13
|
+
this._box = this.createElement('div', ['nk-pop', 'nk-menu'], { role: 'menu' });
|
|
14
|
+
this._box.appendChild(document.createElement('slot'));
|
|
15
|
+
this._wrapper.appendChild(this._box);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get items() { return [...this.querySelectorAll(':scope > nk-menu-item')].filter(i => i.selectable); }
|
|
19
|
+
|
|
20
|
+
setupEvents() {
|
|
21
|
+
this._onKey = (e) => {
|
|
22
|
+
const dir = { ArrowDown: 1, ArrowUp: -1 }[e.key];
|
|
23
|
+
const current = e.target.closest?.('nk-menu-item');
|
|
24
|
+
const items = this.items;
|
|
25
|
+
if (!items.length) return;
|
|
26
|
+
if (dir) {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
const i = items.indexOf(current);
|
|
29
|
+
items[(i + dir + items.length) % items.length].focus();
|
|
30
|
+
} else if ((e.key === 'Enter' || e.key === ' ') && current) {
|
|
31
|
+
e.preventDefault(); current.select();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
this.addEventListener('keydown', this._onKey);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
teardownEvents() { this.removeEventListener('keydown', this._onKey); }
|
|
38
|
+
|
|
39
|
+
/** Focuses the first item (called by a popover when it opens). */
|
|
40
|
+
focusFirst() { this.items[0]?.focus(); }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
customElements.define('nk-menu', NkMenu);
|
|
44
|
+
export { NkMenu };
|