@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,106 @@
|
|
|
1
|
+
import { NkElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-btn variant="primary" small>Save</nk-btn> → <button class="nk-btn primary small">
|
|
4
|
+
// <nk-btn href="/docs" variant="secondary">Docs</nk-btn> → <a class="nk-btn secondary">
|
|
5
|
+
// <nk-btn variant="topbar">⭐</nk-btn> → <button class="nk-topbar-btn">
|
|
6
|
+
//
|
|
7
|
+
// Modifier classes become attributes. A slotted <svg> is sized by the
|
|
8
|
+
// `.nk-btn ::slotted(svg)` twin; pass the icon itself, never wrapped.
|
|
9
|
+
const VARIANTS = ['primary', 'secondary', 'danger', 'danger-solid', 'topbar', 'share'];
|
|
10
|
+
|
|
11
|
+
class NkBtn extends NkElement {
|
|
12
|
+
static get observedAttributes() { return ['variant', 'small', 'disabled', 'type', 'href', 'title', 'aria-label']; }
|
|
13
|
+
|
|
14
|
+
render() {
|
|
15
|
+
this._build();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_build() {
|
|
19
|
+
const href = this.getAttribute('href');
|
|
20
|
+
const el = href ? this.createElement('a', [], { href }) : this.createElement('button', [], { type: this.getAttribute('type') || 'button' });
|
|
21
|
+
el.className = this._computeClasses().join(' ');
|
|
22
|
+
el.appendChild(document.createElement('slot'));
|
|
23
|
+
this._applyState(el);
|
|
24
|
+
if (this._btn) {
|
|
25
|
+
this._btn.removeEventListener('click', this._onClick);
|
|
26
|
+
this._btn.replaceWith(el);
|
|
27
|
+
} else {
|
|
28
|
+
this._wrapper.appendChild(el);
|
|
29
|
+
}
|
|
30
|
+
this._btn = el;
|
|
31
|
+
if (this._onClick) el.addEventListener('click', this._onClick);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_applyState(el) {
|
|
35
|
+
const disabled = this.getBoolAttr('disabled');
|
|
36
|
+
if (el.tagName === 'BUTTON') el.disabled = disabled;
|
|
37
|
+
else el.setAttribute('aria-disabled', disabled ? 'true' : 'false');
|
|
38
|
+
const title = this.getAttribute('title');
|
|
39
|
+
title ? el.setAttribute('title', title) : el.removeAttribute('title');
|
|
40
|
+
const label = this.getAttribute('aria-label');
|
|
41
|
+
label ? el.setAttribute('aria-label', label) : el.removeAttribute('aria-label');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setupEvents() {
|
|
45
|
+
this._onClick = (e) => {
|
|
46
|
+
if (this.getBoolAttr('disabled')) { e.preventDefault(); e.stopPropagation(); return; }
|
|
47
|
+
// A button inside a shadow root is not a submit button of the outer
|
|
48
|
+
// form; forward the intent to the form the host sits in.
|
|
49
|
+
const type = this.getAttribute('type');
|
|
50
|
+
if (type === 'submit' || type === 'reset') {
|
|
51
|
+
const form = this.closest('form');
|
|
52
|
+
if (form) type === 'submit' ? form.requestSubmit() : form.reset();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
this._btn.addEventListener('click', this._onClick);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
teardownEvents() {
|
|
59
|
+
this._btn?.removeEventListener('click', this._onClick);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onAttributeChanged(name) {
|
|
63
|
+
if (!this._btn) return;
|
|
64
|
+
switch (name) {
|
|
65
|
+
case 'href':
|
|
66
|
+
this._build();
|
|
67
|
+
break;
|
|
68
|
+
case 'variant':
|
|
69
|
+
case 'small':
|
|
70
|
+
this._btn.className = this._computeClasses().join(' ');
|
|
71
|
+
break;
|
|
72
|
+
case 'type':
|
|
73
|
+
if (this._btn.tagName === 'BUTTON') this._btn.setAttribute('type', this.getAttribute('type') || 'button');
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
this._applyState(this._btn);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_computeClasses() {
|
|
81
|
+
const variant = this.getAttribute('variant');
|
|
82
|
+
if (variant === 'topbar') return ['nk-topbar-btn'];
|
|
83
|
+
if (variant === 'share') return ['nk-topbar-btn', 'nk-share-btn'];
|
|
84
|
+
const classes = ['nk-btn'];
|
|
85
|
+
if (VARIANTS.includes(variant)) classes.push(variant);
|
|
86
|
+
if (this.getBoolAttr('small')) classes.push('small');
|
|
87
|
+
return classes;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
focus(options) { this._btn?.focus(options); }
|
|
91
|
+
click() { this._btn?.click(); }
|
|
92
|
+
|
|
93
|
+
get variant() { return this.getAttribute('variant'); }
|
|
94
|
+
set variant(v) { v ? this.setAttribute('variant', v) : this.removeAttribute('variant'); }
|
|
95
|
+
get small() { return this.getBoolAttr('small'); }
|
|
96
|
+
set small(v) { this.setBoolAttr('small', v); }
|
|
97
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
98
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
99
|
+
get type() { return this.getAttribute('type') || 'button'; }
|
|
100
|
+
set type(v) { this.setAttribute('type', v); }
|
|
101
|
+
get href() { return this.getAttribute('href'); }
|
|
102
|
+
set href(v) { v ? this.setAttribute('href', v) : this.removeAttribute('href'); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
customElements.define('nk-btn', NkBtn);
|
|
106
|
+
export { NkBtn };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { NkFormElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-check name="mail" value="weekly" checked>Weekly digest</nk-check>
|
|
4
|
+
// → <label class="nk-check"><input type="checkbox"> Weekly digest</label>
|
|
5
|
+
class NkCheck extends NkFormElement {
|
|
6
|
+
static get observedAttributes() { return ['checked', 'disabled', 'name', 'value', 'indeterminate', 'required']; }
|
|
7
|
+
|
|
8
|
+
render() {
|
|
9
|
+
const label = this.createElement('label', ['nk-check']);
|
|
10
|
+
this._input = this.createElement('input', [], { type: 'checkbox' });
|
|
11
|
+
this._apply();
|
|
12
|
+
this._defaultChecked = this.getBoolAttr('checked');
|
|
13
|
+
label.appendChild(this._input);
|
|
14
|
+
label.appendChild(document.createElement('slot'));
|
|
15
|
+
this._wrapper.appendChild(label);
|
|
16
|
+
this._syncFormValue();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_apply() {
|
|
20
|
+
this._input.checked = this.getBoolAttr('checked');
|
|
21
|
+
this._input.indeterminate = this.getBoolAttr('indeterminate');
|
|
22
|
+
this._input.disabled = this.getBoolAttr('disabled') || !!this._formDisabled;
|
|
23
|
+
this._input.required = this.getBoolAttr('required');
|
|
24
|
+
const name = this.getAttribute('name');
|
|
25
|
+
name ? this._input.setAttribute('name', name) : this._input.removeAttribute('name');
|
|
26
|
+
this._input.value = this.getAttribute('value') || 'on';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setupEvents() {
|
|
30
|
+
this._onChange = () => {
|
|
31
|
+
this._syncing = true;
|
|
32
|
+
this.setBoolAttr('checked', this._input.checked);
|
|
33
|
+
this.removeAttribute('indeterminate');
|
|
34
|
+
this._syncing = false;
|
|
35
|
+
this._syncFormValue();
|
|
36
|
+
this.emit('nk-change', { checked: this._input.checked, value: this._input.value, name: this.name });
|
|
37
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
38
|
+
};
|
|
39
|
+
this._input.addEventListener('change', this._onChange);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
teardownEvents() {
|
|
43
|
+
this._input?.removeEventListener('change', this._onChange);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onAttributeChanged() {
|
|
47
|
+
if (this._syncing || !this._input) return;
|
|
48
|
+
this._apply();
|
|
49
|
+
this._syncFormValue();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_syncFormValue() {
|
|
53
|
+
this.setFormValue(this._input.checked ? this._input.value : null, this._input.checked ? 'checked' : 'unchecked');
|
|
54
|
+
this.syncValidityFrom(this._input);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
resetValue() { this.setBoolAttr('checked', this._defaultChecked); }
|
|
58
|
+
restoreValue(state) { if (state === 'checked' || state === 'unchecked') this.setBoolAttr('checked', state === 'checked'); }
|
|
59
|
+
onFormDisabled(disabled) { this._formDisabled = disabled; this._apply(); }
|
|
60
|
+
|
|
61
|
+
focus(options) { this._input?.focus(options); }
|
|
62
|
+
|
|
63
|
+
get checked() { return this._input?.checked ?? this.getBoolAttr('checked'); }
|
|
64
|
+
set checked(v) { this.setBoolAttr('checked', v); }
|
|
65
|
+
get indeterminate() { return this.getBoolAttr('indeterminate'); }
|
|
66
|
+
set indeterminate(v) { this.setBoolAttr('indeterminate', v); }
|
|
67
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
68
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
69
|
+
get name() { return this.getAttribute('name'); }
|
|
70
|
+
set name(v) { this.setAttribute('name', v); }
|
|
71
|
+
get value() { return this.getAttribute('value') || 'on'; }
|
|
72
|
+
set value(v) { this.setAttribute('value', v); }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
customElements.define('nk-check', NkCheck);
|
|
76
|
+
export { NkCheck };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { NkElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-field label="Display name" desc="Shown next to your comments">
|
|
4
|
+
// <nk-input value="Marcel"></nk-input>
|
|
5
|
+
// </nk-field>
|
|
6
|
+
// → <div class="nk-field"><div><div class="f-label">…</div><div class="f-desc">…</div></div>
|
|
7
|
+
// <div class="f-control">…</div></div>
|
|
8
|
+
// Rich label / description content goes through slot="label" / slot="desc"
|
|
9
|
+
// (twins `.nk-field ::slotted([slot="label"])` etc. in notionkit.css).
|
|
10
|
+
class NkField extends NkElement {
|
|
11
|
+
static get observedAttributes() { return ['label', 'desc']; }
|
|
12
|
+
|
|
13
|
+
render() {
|
|
14
|
+
const field = this.createElement('div', ['nk-field']);
|
|
15
|
+
const text = document.createElement('div');
|
|
16
|
+
this._label = this.createElement('div', ['f-label']);
|
|
17
|
+
const labelSlot = this.createElement('slot', [], { name: 'label' });
|
|
18
|
+
labelSlot.appendChild(this._label);
|
|
19
|
+
this._desc = this.createElement('div', ['f-desc']);
|
|
20
|
+
const descSlot = this.createElement('slot', [], { name: 'desc' });
|
|
21
|
+
descSlot.appendChild(this._desc);
|
|
22
|
+
text.appendChild(labelSlot);
|
|
23
|
+
text.appendChild(descSlot);
|
|
24
|
+
const control = this.createElement('div', ['f-control']);
|
|
25
|
+
control.appendChild(document.createElement('slot'));
|
|
26
|
+
field.appendChild(text);
|
|
27
|
+
field.appendChild(control);
|
|
28
|
+
this._wrapper.appendChild(field);
|
|
29
|
+
this._sync();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
onAttributeChanged() { this._sync(); }
|
|
33
|
+
|
|
34
|
+
_sync() {
|
|
35
|
+
if (!this._label) return;
|
|
36
|
+
this._label.textContent = this.getAttribute('label') || '';
|
|
37
|
+
const desc = this.getAttribute('desc');
|
|
38
|
+
this._desc.textContent = desc || '';
|
|
39
|
+
this._desc.style.display = desc ? '' : 'none';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get label() { return this.getAttribute('label'); }
|
|
43
|
+
set label(v) { this.setAttribute('label', v); }
|
|
44
|
+
get desc() { return this.getAttribute('desc'); }
|
|
45
|
+
set desc(v) { v == null ? this.removeAttribute('desc') : this.setAttribute('desc', v); }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
customElements.define('nk-field', NkField);
|
|
49
|
+
export { NkField };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { NkFormElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-input name="title" value="…" placeholder="…" wide></nk-input> → <input class="nk-input wide">
|
|
4
|
+
// Takes part in the surrounding <form> through ElementInternals.
|
|
5
|
+
const FORWARDED = ['type', 'placeholder', 'name', 'min', 'max', 'step', 'autocomplete', 'pattern', 'maxlength', 'minlength', 'inputmode', 'list', 'aria-label'];
|
|
6
|
+
const BOOLEANS = ['required', 'disabled', 'readonly'];
|
|
7
|
+
|
|
8
|
+
class NkInput extends NkFormElement {
|
|
9
|
+
static get observedAttributes() { return ['value', 'wide', ...FORWARDED, ...BOOLEANS]; }
|
|
10
|
+
|
|
11
|
+
render() {
|
|
12
|
+
this._input = this.createElement('input', ['nk-input']);
|
|
13
|
+
for (const attr of FORWARDED) this._forward(attr);
|
|
14
|
+
for (const attr of BOOLEANS) this._forward(attr);
|
|
15
|
+
this._input.value = this.getAttribute('value') ?? '';
|
|
16
|
+
this._defaultValue = this._input.value;
|
|
17
|
+
this._syncWide();
|
|
18
|
+
this._wrapper.appendChild(this._input);
|
|
19
|
+
this._syncFormValue();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_forward(attr) {
|
|
23
|
+
if (BOOLEANS.includes(attr)) {
|
|
24
|
+
this._input[attr === 'readonly' ? 'readOnly' : attr] = this.getBoolAttr(attr);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const v = this.getAttribute(attr);
|
|
28
|
+
if (v === null) this._input.removeAttribute(attr);
|
|
29
|
+
else this._input.setAttribute(attr, v);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_syncWide() {
|
|
33
|
+
this._input.classList.toggle('wide', this.getBoolAttr('wide'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setupEvents() {
|
|
37
|
+
this._onInput = () => {
|
|
38
|
+
this._syncFormValue();
|
|
39
|
+
this.emit('nk-input', { value: this._input.value, name: this.name });
|
|
40
|
+
};
|
|
41
|
+
this._onChange = () => {
|
|
42
|
+
this._syncFormValue();
|
|
43
|
+
this.emit('nk-change', { value: 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') ?? ''; this._syncFormValue(); return; }
|
|
58
|
+
if (name === 'wide') { this._syncWide(); return; }
|
|
59
|
+
this._forward(name);
|
|
60
|
+
this._syncFormValue();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_syncFormValue() {
|
|
64
|
+
this.setFormValue(this._input.value);
|
|
65
|
+
this.syncValidityFrom(this._input);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
resetValue() {
|
|
69
|
+
this._input.value = this.getAttribute('value') ?? '';
|
|
70
|
+
this._syncFormValue();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
restoreValue(state) {
|
|
74
|
+
if (typeof state === 'string') { this._input.value = state; this._syncFormValue(); }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
onFormDisabled(disabled) {
|
|
78
|
+
this._input.disabled = disabled || this.getBoolAttr('disabled');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
focus(options) { this._input?.focus(options); }
|
|
82
|
+
blur() { this._input?.blur(); }
|
|
83
|
+
select() { this._input?.select(); }
|
|
84
|
+
|
|
85
|
+
get value() { return this._input?.value ?? (this.getAttribute('value') ?? ''); }
|
|
86
|
+
set value(v) {
|
|
87
|
+
if (this._input) { this._input.value = v ?? ''; this._syncFormValue(); }
|
|
88
|
+
else this.setAttribute('value', v ?? '');
|
|
89
|
+
}
|
|
90
|
+
get name() { return this.getAttribute('name'); }
|
|
91
|
+
set name(v) { this.setAttribute('name', v); }
|
|
92
|
+
get type() { return this.getAttribute('type') || 'text'; }
|
|
93
|
+
set type(v) { this.setAttribute('type', v); }
|
|
94
|
+
get placeholder() { return this.getAttribute('placeholder'); }
|
|
95
|
+
set placeholder(v) { this.setAttribute('placeholder', v); }
|
|
96
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
97
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
98
|
+
get required() { return this.getBoolAttr('required'); }
|
|
99
|
+
set required(v) { this.setBoolAttr('required', v); }
|
|
100
|
+
get readonly() { return this.getBoolAttr('readonly'); }
|
|
101
|
+
set readonly(v) { this.setBoolAttr('readonly', v); }
|
|
102
|
+
get wide() { return this.getBoolAttr('wide'); }
|
|
103
|
+
set wide(v) { this.setBoolAttr('wide', v); }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
customElements.define('nk-input', NkInput);
|
|
107
|
+
export { NkInput };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { NkFormElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-radio name="style" value="concise" checked>Concise</nk-radio>
|
|
4
|
+
// → <label class="nk-check"><input type="radio"> Concise</label>
|
|
5
|
+
//
|
|
6
|
+
// ── Grouping ──
|
|
7
|
+
// Every <nk-radio> keeps its <input type="radio"> in its own shadow root, and
|
|
8
|
+
// native radio grouping works per tree — it does not reach across shadow
|
|
9
|
+
// boundaries. Two <nk-radio name="x"> would therefore both stay checked. So
|
|
10
|
+
// the group is kept here, following the native definition: same `name`,
|
|
11
|
+
// same containing tree, same form owner. One tab stop per group, arrow keys
|
|
12
|
+
// move within it (wrapping, skipping disabled), and select as they go.
|
|
13
|
+
|
|
14
|
+
const ARROW_KEYS = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 };
|
|
15
|
+
|
|
16
|
+
/** Form owner, also for a peer that has not been upgraded yet. */
|
|
17
|
+
function ownerForm(el) {
|
|
18
|
+
return (el.form !== undefined ? el.form : el.closest('form')) ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Only the selected radio is a tab stop; arrow keys move within the group. */
|
|
22
|
+
function syncGroupTabIndex(group) {
|
|
23
|
+
const enabled = group.filter(el => !el.disabled);
|
|
24
|
+
if (!enabled.length) return;
|
|
25
|
+
const focusable = enabled.find(el => el.checked) || enabled[0];
|
|
26
|
+
for (const el of group) {
|
|
27
|
+
if (el._input) el._input.tabIndex = el === focusable ? 0 : -1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class NkRadio extends NkFormElement {
|
|
32
|
+
static get observedAttributes() { return ['checked', 'disabled', 'name', 'value', 'required']; }
|
|
33
|
+
|
|
34
|
+
render() {
|
|
35
|
+
const label = this.createElement('label', ['nk-check']);
|
|
36
|
+
this._input = this.createElement('input', [], { type: 'radio' });
|
|
37
|
+
// `name` on the inner input is inert (it is alone in its tree); it stays
|
|
38
|
+
// as a devtools courtesy. The group above is the real one.
|
|
39
|
+
this._apply();
|
|
40
|
+
this._defaultChecked = this.getBoolAttr('checked');
|
|
41
|
+
label.appendChild(this._input);
|
|
42
|
+
label.appendChild(document.createElement('slot'));
|
|
43
|
+
this._wrapper.appendChild(label);
|
|
44
|
+
this._syncFormValue();
|
|
45
|
+
// Elements upgrade in document order, so the last `checked` one in the
|
|
46
|
+
// markup wins the group — the same outcome native radios produce.
|
|
47
|
+
if (this._input.checked) this._uncheckPeers();
|
|
48
|
+
syncGroupTabIndex(this._group());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_apply() {
|
|
52
|
+
this._input.checked = this.getBoolAttr('checked');
|
|
53
|
+
this._input.disabled = this.getBoolAttr('disabled') || !!this._formDisabled;
|
|
54
|
+
this._input.required = this.getBoolAttr('required');
|
|
55
|
+
const name = this.getAttribute('name');
|
|
56
|
+
name ? this._input.setAttribute('name', name) : this._input.removeAttribute('name');
|
|
57
|
+
this._input.value = this.getAttribute('value') || '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setupEvents() {
|
|
61
|
+
this._onChange = () => this._applyChange();
|
|
62
|
+
this._onKeyDown = (e) => {
|
|
63
|
+
const dir = ARROW_KEYS[e.key];
|
|
64
|
+
if (!dir || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
65
|
+
const group = this._group().filter(el => !el.disabled);
|
|
66
|
+
if (group.length < 2) return;
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
const next = group[(group.indexOf(this) + dir + group.length) % group.length];
|
|
69
|
+
next._input.focus();
|
|
70
|
+
next._input.checked = true;
|
|
71
|
+
next._applyChange();
|
|
72
|
+
};
|
|
73
|
+
this._input.addEventListener('change', this._onChange);
|
|
74
|
+
this._input.addEventListener('keydown', this._onKeyDown);
|
|
75
|
+
syncGroupTabIndex(this._group());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
teardownEvents() {
|
|
79
|
+
this._input?.removeEventListener('change', this._onChange);
|
|
80
|
+
this._input?.removeEventListener('keydown', this._onKeyDown);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Shared by user change and arrow-key selection, so both look identical. */
|
|
84
|
+
_applyChange() {
|
|
85
|
+
if (this._input.checked) this._uncheckPeers();
|
|
86
|
+
this._syncing = true;
|
|
87
|
+
this.setBoolAttr('checked', this._input.checked);
|
|
88
|
+
this._syncing = false;
|
|
89
|
+
this._syncFormValue();
|
|
90
|
+
syncGroupTabIndex(this._group());
|
|
91
|
+
this.emit('nk-change', { checked: this._input.checked, value: this._input.value, name: this.name });
|
|
92
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Same name, same tree, same form owner — in document order, this element included. */
|
|
96
|
+
_group() {
|
|
97
|
+
const name = this.getAttribute('name');
|
|
98
|
+
const root = this.getRootNode();
|
|
99
|
+
if (!name || typeof root?.querySelectorAll !== 'function') return [this];
|
|
100
|
+
const form = ownerForm(this);
|
|
101
|
+
return [...root.querySelectorAll('nk-radio')].filter(
|
|
102
|
+
el => el.getAttribute('name') === name && ownerForm(el) === form
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_uncheckPeers() {
|
|
107
|
+
for (const el of this._group()) {
|
|
108
|
+
if (el !== this && el.checked) el.checked = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onAttributeChanged(name) {
|
|
113
|
+
if (this._syncing || !this._input) return;
|
|
114
|
+
this._apply();
|
|
115
|
+
if (name === 'checked' && this._input.checked) this._uncheckPeers();
|
|
116
|
+
this._syncFormValue();
|
|
117
|
+
syncGroupTabIndex(this._group());
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_syncFormValue() {
|
|
121
|
+
const val = this.getAttribute('value') || '';
|
|
122
|
+
this.setFormValue(this._input.checked ? val : null, this._input.checked ? 'checked' : 'unchecked');
|
|
123
|
+
this.syncValidityFrom(this._input);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
resetValue() {
|
|
127
|
+
this._input.checked = this._defaultChecked;
|
|
128
|
+
this.setBoolAttr('checked', this._defaultChecked);
|
|
129
|
+
this._syncFormValue();
|
|
130
|
+
syncGroupTabIndex(this._group());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
restoreValue(state) {
|
|
134
|
+
if (state === 'checked') this.checked = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
onFormDisabled(disabled) { this._formDisabled = disabled; this._apply(); syncGroupTabIndex(this._group()); }
|
|
138
|
+
|
|
139
|
+
focus(options) { this._input?.focus(options); }
|
|
140
|
+
|
|
141
|
+
get checked() { return this._input?.checked ?? this.getBoolAttr('checked'); }
|
|
142
|
+
set checked(v) {
|
|
143
|
+
if (this._input) this._input.checked = v;
|
|
144
|
+
// Also covers the case where the attribute is already present, so
|
|
145
|
+
// setBoolAttr stays silent and onAttributeChanged never runs.
|
|
146
|
+
if (v) this._uncheckPeers();
|
|
147
|
+
this.setBoolAttr('checked', v);
|
|
148
|
+
if (this._input) { this._syncFormValue(); syncGroupTabIndex(this._group()); }
|
|
149
|
+
}
|
|
150
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
151
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
152
|
+
get name() { return this.getAttribute('name'); }
|
|
153
|
+
set name(v) { this.setAttribute('name', v); }
|
|
154
|
+
get value() { return this.getAttribute('value') || ''; }
|
|
155
|
+
set value(v) { this.setAttribute('value', v); }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
customElements.define('nk-radio', NkRadio);
|
|
159
|
+
export { NkRadio };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { NkFormElement } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
// <nk-select name="role" value="editor">
|
|
4
|
+
// <option value="viewer">Viewer</option>
|
|
5
|
+
// <option value="editor">Editor</option>
|
|
6
|
+
// </nk-select>
|
|
7
|
+
//
|
|
8
|
+
// The light-DOM <option>s are copied into the shadow <select>. A
|
|
9
|
+
// MutationObserver keeps the copy in step when a framework swaps them; the
|
|
10
|
+
// empty string is a valid value; the live selection survives a rebuild.
|
|
11
|
+
class NkSelect extends NkFormElement {
|
|
12
|
+
static get observedAttributes() { return ['value', 'name', 'disabled', 'required', 'compact', 'aria-label']; }
|
|
13
|
+
static get observesLightDom() { return true; }
|
|
14
|
+
|
|
15
|
+
render() {
|
|
16
|
+
this._select = this.createElement('select', ['nk-select']);
|
|
17
|
+
this._forwardAll();
|
|
18
|
+
this._wrapper.appendChild(this._select);
|
|
19
|
+
this._defaultValue = this.getAttribute('value');
|
|
20
|
+
// Children may not be parsed yet when connectedCallback runs.
|
|
21
|
+
this.projectLightDom();
|
|
22
|
+
requestAnimationFrame(() => this.projectLightDom());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_forwardAll() {
|
|
26
|
+
const name = this.getAttribute('name');
|
|
27
|
+
name ? this._select.setAttribute('name', name) : this._select.removeAttribute('name');
|
|
28
|
+
const label = this.getAttribute('aria-label');
|
|
29
|
+
label ? this._select.setAttribute('aria-label', label) : this._select.removeAttribute('aria-label');
|
|
30
|
+
this._select.disabled = this.getBoolAttr('disabled');
|
|
31
|
+
this._select.required = this.getBoolAttr('required');
|
|
32
|
+
this._select.classList.toggle('compact', this.getBoolAttr('compact'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
projectLightDom() {
|
|
36
|
+
if (!this._select) return;
|
|
37
|
+
// The options are pure data — the clones carry no listeners — so skipping
|
|
38
|
+
// an unchanged rebuild is safe, and it keeps an open dropdown from
|
|
39
|
+
// snapping shut on light-DOM churn elsewhere.
|
|
40
|
+
const nodes = [...this.children].filter(n => n.matches('option, optgroup'));
|
|
41
|
+
const signature = nodes.map(n => n.outerHTML).join('');
|
|
42
|
+
if (signature === this._optionSignature) return;
|
|
43
|
+
this._optionSignature = signature;
|
|
44
|
+
|
|
45
|
+
// innerHTML = '' drops the selection; remember it first. selectedIndex -1
|
|
46
|
+
// means "nothing selected", which is not an option whose value is "".
|
|
47
|
+
const previous = this._select.selectedIndex >= 0 ? this._select.value : null;
|
|
48
|
+
|
|
49
|
+
this._select.innerHTML = '';
|
|
50
|
+
for (const node of nodes) this._select.appendChild(node.cloneNode(true));
|
|
51
|
+
|
|
52
|
+
// Keep the live selection when it survived; otherwise the value attribute;
|
|
53
|
+
// otherwise the browser default (first option / `selected`).
|
|
54
|
+
if (!this._applyValue(previous)) this._applyValue(this.getAttribute('value'));
|
|
55
|
+
this._syncFormValue();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Selects `value` if an option carries it, and reports whether it did. */
|
|
59
|
+
_applyValue(value) {
|
|
60
|
+
if (value === null) return false;
|
|
61
|
+
if (![...this._select.options].some(o => o.value === value)) return false;
|
|
62
|
+
this._select.value = value;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setupEvents() {
|
|
67
|
+
this._onChange = () => {
|
|
68
|
+
this._syncFormValue();
|
|
69
|
+
this.emit('nk-change', { value: this._select.value, name: this.name });
|
|
70
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
71
|
+
};
|
|
72
|
+
this._select.addEventListener('change', this._onChange);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
teardownEvents() {
|
|
76
|
+
this._select?.removeEventListener('change', this._onChange);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
onAttributeChanged(name) {
|
|
80
|
+
if (!this._select) return;
|
|
81
|
+
if (name === 'value') {
|
|
82
|
+
this._applyValue(this.getAttribute('value'));
|
|
83
|
+
this._syncFormValue();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this._forwardAll();
|
|
87
|
+
this._syncFormValue();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_syncFormValue() {
|
|
91
|
+
this.setFormValue(this._select.value);
|
|
92
|
+
this.syncValidityFrom(this._select);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
resetValue() {
|
|
96
|
+
if (!this._applyValue(this._defaultValue)) this._select.selectedIndex = 0;
|
|
97
|
+
this._syncFormValue();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
restoreValue(state) {
|
|
101
|
+
if (typeof state === 'string') { this._applyValue(state); this._syncFormValue(); }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
onFormDisabled(disabled) { this._select.disabled = disabled || this.getBoolAttr('disabled'); }
|
|
105
|
+
|
|
106
|
+
focus(options) { this._select?.focus(options); }
|
|
107
|
+
|
|
108
|
+
get value() { return this._select?.value ?? ''; }
|
|
109
|
+
set value(v) {
|
|
110
|
+
if (this._select) { this._select.value = v; this._syncFormValue(); }
|
|
111
|
+
else this.setAttribute('value', v);
|
|
112
|
+
}
|
|
113
|
+
get selectedIndex() { return this._select?.selectedIndex ?? -1; }
|
|
114
|
+
get options() { return this._select?.options ?? []; }
|
|
115
|
+
get name() { return this.getAttribute('name'); }
|
|
116
|
+
set name(v) { this.setAttribute('name', v); }
|
|
117
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
118
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
119
|
+
get required() { return this.getBoolAttr('required'); }
|
|
120
|
+
set required(v) { this.setBoolAttr('required', v); }
|
|
121
|
+
get compact() { return this.getBoolAttr('compact'); }
|
|
122
|
+
set compact(v) { this.setBoolAttr('compact', v); }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
customElements.define('nk-select', NkSelect);
|
|
126
|
+
export { NkSelect };
|