@jungherz-de/notionkit-elements 1.11.1 → 1.12.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/CHANGELOG.md +20 -0
- package/README.md +6 -6
- package/SKILL.md +81 -61
- package/dist/components/nk-board-view.js +1 -1
- package/dist/components/nk-calendar.js +1 -1
- package/dist/components/nk-list-view.js +1 -1
- package/dist/components/nk-menu.js +1 -1
- package/dist/components/nk-panel.js +25 -3
- package/dist/components/nk-panels.js +13 -4
- package/dist/components/nk-prop.js +7 -2
- package/dist/components/nk-props.js +13 -4
- package/dist/components/nk-steps.js +50 -9
- package/dist/components/nk-table-view.js +14 -4
- package/dist/components/nk-tabs.js +24 -1
- package/dist/components/nk-tooltip.js +1 -1
- package/dist/components/shared/{floating-CtqV6MBj.js → floating-DOK73b_D.js} +1 -1
- package/dist/components/shared/{property-cell-Cddz5n2O.js → property-cell-DKvv6TIG.js} +31 -2
- package/dist/notionkit-elements.esm.js +243 -96
- package/dist/notionkit-elements.js +243 -96
- package/dist/notionkit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/components/data/nk-table-view.js +13 -3
- package/src/components/page/nk-panel.js +25 -3
- package/src/components/page/nk-panels.js +13 -4
- package/src/components/page/nk-prop.js +7 -2
- package/src/components/page/nk-props.js +13 -4
- package/src/components/page/nk-steps.js +50 -9
- package/src/components/page/nk-tabs.js +24 -1
- package/src/util/property-cell.js +31 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jungherz-de/notionkit-elements",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Vanilla JavaScript Web Components wrapping the NotionKit CSS foundation – <nk-*> custom elements with Shadow DOM, automatic theme sync and native form participation.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jungherz GmbH <info@jungherz.com> (https://www.jungherz.com)",
|
|
@@ -62,10 +62,10 @@
|
|
|
62
62
|
"dark-mode"
|
|
63
63
|
],
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"@jungherz-de/notionkit": ">=1.
|
|
65
|
+
"@jungherz-de/notionkit": ">=1.12.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@jungherz-de/notionkit": "^1.
|
|
68
|
+
"@jungherz-de/notionkit": "^1.12.0",
|
|
69
69
|
"@playwright/test": "^1.50.0",
|
|
70
70
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
71
71
|
"@rollup/plugin-terser": "^0.4.0",
|
|
@@ -9,7 +9,10 @@ import { renderPropertyCell, compareBy } from '../../util/property-cell.js';
|
|
|
9
9
|
// { row, id, key, cell } – `key` and `cell` say which cell was hit, to open
|
|
10
10
|
// an editor there, like the date picker under a due date – checkboxes
|
|
11
11
|
// nk-change, headers nk-action { action: 'sort' } (and sort locally
|
|
12
|
-
// with `sortable`), the add row nk-action { action: 'new-row' }.
|
|
12
|
+
// with `sortable`), the add row nk-action { action: 'new-row' }. A column of
|
|
13
|
+
// type 'actions' (NotionKit 1.12.0) sets buttons in each row; a click on one
|
|
14
|
+
// fires nk-action { action, row, id, anchor } instead of selecting the row,
|
|
15
|
+
// and its header sorts nothing.
|
|
13
16
|
class NkTableView extends NkElement {
|
|
14
17
|
static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'new-row', 'new-row-label', 'sortable', 'sort-key', 'sort-dir', 'wrap']; }
|
|
15
18
|
|
|
@@ -39,9 +42,10 @@ class NkTableView extends NkElement {
|
|
|
39
42
|
this._table.classList.toggle('wrap', this.getBoolAttr('wrap'));
|
|
40
43
|
const tr = document.createElement('tr');
|
|
41
44
|
for (const col of this._columns) {
|
|
42
|
-
const
|
|
45
|
+
const actions = col.type === 'actions';
|
|
46
|
+
const th = this.createElement('th', actions ? ['actions'] : [], actions ? { scope: 'col' } : { 'data-key': col.key, scope: 'col' });
|
|
43
47
|
if (col.icon) { const i = this.createElement('span', ['th-icon']); i.textContent = col.icon; th.appendChild(i); }
|
|
44
|
-
th.appendChild(document.createTextNode(col.label ?? col.key));
|
|
48
|
+
th.appendChild(document.createTextNode(col.label ?? (actions ? '' : col.key)));
|
|
45
49
|
if (col.key === this.getAttribute('sort-key')) th.appendChild(document.createTextNode(this.getAttribute('sort-dir') === 'desc' ? ' ↓' : ' ↑'));
|
|
46
50
|
if (col.width) th.style.width = col.width;
|
|
47
51
|
tr.appendChild(th);
|
|
@@ -74,6 +78,12 @@ class NkTableView extends NkElement {
|
|
|
74
78
|
if (ok && this.getBoolAttr('sortable')) { this.setAttribute('sort-key', key); this.setAttribute('sort-dir', dir); }
|
|
75
79
|
return;
|
|
76
80
|
}
|
|
81
|
+
const act = e.target.closest('button[data-action]');
|
|
82
|
+
if (act) {
|
|
83
|
+
const row = this._rowById(act.closest('tr[data-id]')?.dataset.id);
|
|
84
|
+
if (!act.disabled) this.emit('nk-action', { action: act.dataset.action, row, id: row?.id, anchor: act });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
77
87
|
if (e.target.closest('input[type=checkbox], a')) return;
|
|
78
88
|
const tr = e.target.closest('tr[data-id]');
|
|
79
89
|
if (!tr) return;
|
|
@@ -4,9 +4,13 @@ import { NkElement } from '../../base.js';
|
|
|
4
4
|
// <nk-panel href="/roadmap" cover="covers/aurora.svg" icon="🚀" title="Roadmap"><p>2 min ago</p></nk-panel>
|
|
5
5
|
// → <a class="nk-panel" href="/roadmap"><div class="nk-cover"><img src="…" alt=""></div>
|
|
6
6
|
// <div class="nk-page-icon">🚀</div><h3>Roadmap</h3>…</a>
|
|
7
|
+
// <nk-panel title="Contacts database"><nk-tag slot="end" color="green">Connected</nk-tag>…</nk-panel>
|
|
8
|
+
// → … <div class="p-head"><h3>Contacts database</h3><span class="p-end">…</span></div>
|
|
7
9
|
// `cover` without a value draws the token gradient, with one a picture; `icon`
|
|
8
10
|
// overlaps the cover as on a page – a page tile, as on Notion's Home. With
|
|
9
11
|
// `href` the panel is a link. `title` is the heading, never a tooltip.
|
|
12
|
+
// slot="end" (NotionKit 1.12.0) sits at the right edge of the title – a state
|
|
13
|
+
// as a tag, a button – and moves under it where both do not fit.
|
|
10
14
|
class NkPanel extends NkElement {
|
|
11
15
|
static get observedAttributes() { return ['title', 'icon', 'cover', 'href', 'target']; }
|
|
12
16
|
|
|
@@ -15,6 +19,11 @@ class NkPanel extends NkElement {
|
|
|
15
19
|
this._img = this.createElement('img', [], { alt: '' });
|
|
16
20
|
this._icon = this.createElement('div', ['nk-page-icon']);
|
|
17
21
|
this._heading = document.createElement('h3');
|
|
22
|
+
this._endSlot = this.createElement('slot', [], { name: 'end' });
|
|
23
|
+
this._end = this.createElement('span', ['p-end']);
|
|
24
|
+
this._end.appendChild(this._endSlot);
|
|
25
|
+
this._head = this.createElement('div', ['p-head']);
|
|
26
|
+
this._head.append(this._heading, this._end);
|
|
18
27
|
this._slot = document.createElement('slot');
|
|
19
28
|
this.takeTitle();
|
|
20
29
|
this._build();
|
|
@@ -23,7 +32,7 @@ class NkPanel extends NkElement {
|
|
|
23
32
|
_build() {
|
|
24
33
|
const href = this.getAttribute('href');
|
|
25
34
|
const box = this.createElement(href ? 'a' : 'div', ['nk-panel']);
|
|
26
|
-
box.append(this._cover, this._icon, this.
|
|
35
|
+
box.append(this._cover, this._icon, this._head, this._slot);
|
|
27
36
|
this._wrapper.replaceChildren(box);
|
|
28
37
|
this._box = box;
|
|
29
38
|
this._sync();
|
|
@@ -39,10 +48,23 @@ class NkPanel extends NkElement {
|
|
|
39
48
|
const icon = this.getAttribute('icon');
|
|
40
49
|
this._icon.textContent = icon || '';
|
|
41
50
|
this._icon.style.display = icon ? '' : 'none';
|
|
42
|
-
|
|
43
|
-
this._heading.
|
|
51
|
+
const title = this._titleText || '';
|
|
52
|
+
if (this._heading.textContent !== title) this._heading.textContent = title;
|
|
53
|
+
this._heading.style.display = title ? '' : 'none';
|
|
54
|
+
// The end and the whole head leave the layout when they are empty, so a
|
|
55
|
+
// title alone lays out as it always did.
|
|
56
|
+
const end = this._endSlot.assignedNodes().some(n => n.nodeType === Node.ELEMENT_NODE || n.textContent.trim());
|
|
57
|
+
this._end.style.display = end ? '' : 'none';
|
|
58
|
+
this._head.style.display = title || end ? '' : 'none';
|
|
44
59
|
}
|
|
45
60
|
|
|
61
|
+
setupEvents() {
|
|
62
|
+
this._onEnd = () => this._sync();
|
|
63
|
+
this._endSlot.addEventListener('slotchange', this._onEnd);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
teardownEvents() { this._endSlot?.removeEventListener('slotchange', this._onEnd); }
|
|
67
|
+
|
|
46
68
|
onAttributeChanged(name, _old, value) {
|
|
47
69
|
if (name === 'title' && !this.takeTitle(value)) return;
|
|
48
70
|
if (name === 'href' && (value === null) !== (this._box.localName === 'div')) { this._build(); return; }
|
|
@@ -3,13 +3,22 @@ import { NkElement } from '../../base.js';
|
|
|
3
3
|
// <nk-panels><nk-panel …></nk-panel><nk-panel …></nk-panel></nk-panels>
|
|
4
4
|
// → <div class="nk-panels">…</div>: columns of at least 200px that share the
|
|
5
5
|
// row and fall to one on a phone. The <nk-panel> hosts are display: contents,
|
|
6
|
-
// so each .nk-panel is a cell of this grid.
|
|
6
|
+
// so each .nk-panel is a cell of this grid. `flush` (NotionKit 1.12.0) drops
|
|
7
|
+
// the grid's outer margin.
|
|
7
8
|
class NkPanels extends NkElement {
|
|
9
|
+
static get observedAttributes() { return ['flush']; }
|
|
10
|
+
|
|
8
11
|
render() {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
this._wrapper.appendChild(
|
|
12
|
+
this._grid = this.createElement('div', ['nk-panels']);
|
|
13
|
+
this._grid.appendChild(document.createElement('slot'));
|
|
14
|
+
this._wrapper.appendChild(this._grid);
|
|
15
|
+
this.onAttributeChanged();
|
|
12
16
|
}
|
|
17
|
+
|
|
18
|
+
onAttributeChanged() { this._grid?.classList.toggle('flush', this.getBoolAttr('flush')); }
|
|
19
|
+
|
|
20
|
+
get flush() { return this.getBoolAttr('flush'); }
|
|
21
|
+
set flush(v) { this.setBoolAttr('flush', v); }
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
customElements.define('nk-panels', NkPanels);
|
|
@@ -5,9 +5,11 @@ import { NkElement } from '../../base.js';
|
|
|
5
5
|
// The value is the element's content – tags, an avatar and a name, a date, a
|
|
6
6
|
// <nk-progress wide>. slot="icon" replaces the icon glyph. A click on the
|
|
7
7
|
// name or the value fires nk-action { action: 'name' | 'value', label }, the
|
|
8
|
-
// moment Notion opens the property's editor.
|
|
8
|
+
// moment Notion opens the property's editor. `text` (NotionKit 1.12.0) lets a
|
|
9
|
+
// value that is text – an address, a model name with a tag – flow as text
|
|
10
|
+
// instead of setting its parts one under the other.
|
|
9
11
|
class NkProp extends NkElement {
|
|
10
|
-
static get observedAttributes() { return ['label', 'icon']; }
|
|
12
|
+
static get observedAttributes() { return ['label', 'icon', 'text']; }
|
|
11
13
|
|
|
12
14
|
render() {
|
|
13
15
|
this._row = this.createElement('div', ['nk-prop']);
|
|
@@ -29,6 +31,7 @@ class NkProp extends NkElement {
|
|
|
29
31
|
this._icon.textContent = icon || '';
|
|
30
32
|
this._icon.style.display = icon ? '' : 'none';
|
|
31
33
|
this._label.data = this.getAttribute('label') || '';
|
|
34
|
+
this._value.classList.toggle('text', this.getBoolAttr('text'));
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
setupEvents() {
|
|
@@ -46,6 +49,8 @@ class NkProp extends NkElement {
|
|
|
46
49
|
set label(v) { this.setAttribute('label', v); }
|
|
47
50
|
get icon() { return this.getAttribute('icon'); }
|
|
48
51
|
set icon(v) { v == null ? this.removeAttribute('icon') : this.setAttribute('icon', v); }
|
|
52
|
+
get text() { return this.getBoolAttr('text'); }
|
|
53
|
+
set text(v) { this.setBoolAttr('text', v); }
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
customElements.define('nk-prop', NkProp);
|
|
@@ -6,13 +6,22 @@ import { NkElement } from '../../base.js';
|
|
|
6
6
|
// </nk-props>
|
|
7
7
|
// → <dl class="nk-props">…</dl> – the properties under a page title. The rows
|
|
8
8
|
// are <nk-prop> hosts with display: contents, so each .nk-prop is a row of
|
|
9
|
-
// this list.
|
|
9
|
+
// this list. `flush` (NotionKit 1.12.0) drops the outer margin meant for the
|
|
10
|
+
// flow under a title – for a list inside a panel or a flex column.
|
|
10
11
|
class NkProps extends NkElement {
|
|
12
|
+
static get observedAttributes() { return ['flush']; }
|
|
13
|
+
|
|
11
14
|
render() {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
this._wrapper.appendChild(
|
|
15
|
+
this._list = this.createElement('dl', ['nk-props']);
|
|
16
|
+
this._list.appendChild(document.createElement('slot'));
|
|
17
|
+
this._wrapper.appendChild(this._list);
|
|
18
|
+
this.onAttributeChanged();
|
|
15
19
|
}
|
|
20
|
+
|
|
21
|
+
onAttributeChanged() { this._list?.classList.toggle('flush', this.getBoolAttr('flush')); }
|
|
22
|
+
|
|
23
|
+
get flush() { return this.getBoolAttr('flush'); }
|
|
24
|
+
set flush(v) { this.setBoolAttr('flush', v); }
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
customElements.define('nk-props', NkProps);
|
|
@@ -10,9 +10,15 @@ import { NkElement } from '../../base.js';
|
|
|
10
10
|
// A setup in a few steps (NotionKit 1.7.0). `current` counts from 1; the
|
|
11
11
|
// steps before it are done, one past the last marks all of them done. The
|
|
12
12
|
// `steps` attribute is a comma-separated list; the property also takes
|
|
13
|
-
// { label, desc } objects for a line under the label
|
|
13
|
+
// { label, desc, state, value } objects for a line under the label and a
|
|
14
|
+
// state of its own – 'done', 'skipped' or 'open' (NotionKit 1.12.0) – for a
|
|
15
|
+
// wizard that lets a step be skipped while a later one is done.
|
|
16
|
+
// `selectable` makes each label a button: a click or Enter fires
|
|
17
|
+
// nk-select { index, value, step } – index counts from 1, like `current` –
|
|
18
|
+
// and, unless cancelled, makes that step current. `horizontal` sets the
|
|
19
|
+
// steps in one row above a wizard.
|
|
14
20
|
class NkSteps extends NkElement {
|
|
15
|
-
static get observedAttributes() { return ['steps', 'current', 'label']; }
|
|
21
|
+
static get observedAttributes() { return ['steps', 'current', 'label', 'selectable', 'horizontal']; }
|
|
16
22
|
|
|
17
23
|
render() {
|
|
18
24
|
this._list = this.createElement('ol', ['nk-steps']);
|
|
@@ -25,32 +31,67 @@ class NkSteps extends NkElement {
|
|
|
25
31
|
return (this.getAttribute('steps') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
_step(i) {
|
|
35
|
+
const step = this._items()[i];
|
|
36
|
+
return typeof step === 'object' && step ? step : step === undefined ? undefined : { label: String(step) };
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
_sync() {
|
|
29
40
|
const label = this.getAttribute('label');
|
|
30
41
|
if (label) this._list.setAttribute('aria-label', label); else this._list.removeAttribute('aria-label');
|
|
31
|
-
|
|
32
|
-
this.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
this._list.classList.toggle('horizontal', this.getBoolAttr('horizontal'));
|
|
43
|
+
const current = this.current, selectable = this.getBoolAttr('selectable');
|
|
44
|
+
// Rebuilding drops the focused label; the new one at that place takes it back.
|
|
45
|
+
const focused = this.shadowRoot?.activeElement?.closest?.('li')?.dataset.index;
|
|
46
|
+
this._list.replaceChildren(...this._items().map((_, i) => {
|
|
47
|
+
const n = i + 1, { label: text, desc, state } = this._step(i);
|
|
48
|
+
// A state of its own wins; without one, the steps before `current` are done.
|
|
49
|
+
const done = state ? state === 'done' : n < current, skipped = state === 'skipped';
|
|
50
|
+
const li = this.createElement('li', ['nk-step'], { 'data-index': String(n) });
|
|
51
|
+
li.classList.toggle('done', done);
|
|
52
|
+
li.classList.toggle('skipped', skipped);
|
|
36
53
|
li.classList.toggle('current', n === current);
|
|
37
54
|
if (n === current) li.setAttribute('aria-current', 'step');
|
|
38
55
|
const mark = this.createElement('span', ['st-mark'], { 'aria-hidden': 'true' });
|
|
39
|
-
mark.textContent =
|
|
40
|
-
const body = document.createElement('span');
|
|
56
|
+
mark.textContent = done ? '✓' : skipped ? '–' : String(n);
|
|
57
|
+
const body = selectable ? this.createElement('button', ['st-label'], { type: 'button' }) : document.createElement('span');
|
|
41
58
|
body.textContent = text ?? '';
|
|
42
59
|
if (desc) { const d = this.createElement('span', ['st-desc']); d.textContent = desc; body.appendChild(d); }
|
|
43
60
|
li.append(mark, body);
|
|
44
61
|
return li;
|
|
45
62
|
}));
|
|
63
|
+
if (focused) this._list.querySelector(`li[data-index="${focused}"] .st-label`)?.focus();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setupEvents() {
|
|
67
|
+
this._onClick = (e) => {
|
|
68
|
+
if (!this.getBoolAttr('selectable')) return;
|
|
69
|
+
const li = e.target.closest?.('li.nk-step');
|
|
70
|
+
if (li && this._list.contains(li)) this.select(Number(li.dataset.index));
|
|
71
|
+
};
|
|
72
|
+
this._list.addEventListener('click', this._onClick);
|
|
46
73
|
}
|
|
47
74
|
|
|
75
|
+
teardownEvents() { this._list?.removeEventListener('click', this._onClick); }
|
|
48
76
|
onAttributeChanged() { this._sync(); }
|
|
49
77
|
|
|
78
|
+
/** Jumps to step `index` (from 1): fires nk-select { index, value, step } and, unless cancelled, makes it current. */
|
|
79
|
+
select(index) {
|
|
80
|
+
const step = this._step(index - 1);
|
|
81
|
+
if (!step) return false;
|
|
82
|
+
const ok = this.emit('nk-select', { index, value: step.value ?? step.label, step });
|
|
83
|
+
if (ok) this.current = index;
|
|
84
|
+
return ok;
|
|
85
|
+
}
|
|
86
|
+
|
|
50
87
|
get steps() { return this._items(); }
|
|
51
88
|
set steps(v) { this._steps = Array.isArray(v) ? v : null; if (this._list) this._sync(); }
|
|
52
89
|
get current() { const n = parseInt(this.getAttribute('current'), 10); return Number.isFinite(n) ? n : 1; }
|
|
53
90
|
set current(v) { this.setAttribute('current', String(v)); }
|
|
91
|
+
get selectable() { return this.getBoolAttr('selectable'); }
|
|
92
|
+
set selectable(v) { this.setBoolAttr('selectable', v); }
|
|
93
|
+
get horizontal() { return this.getBoolAttr('horizontal'); }
|
|
94
|
+
set horizontal(v) { this.setBoolAttr('horizontal', v); }
|
|
54
95
|
/** Moves on to the next step; returns the new current step. */
|
|
55
96
|
next() { this.current = Math.min(this.current + 1, this._items().length + 1); return this.current; }
|
|
56
97
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
|
+
import { boxOf } from '../../util/floating.js';
|
|
2
3
|
|
|
3
4
|
// <nk-tabs value="notes">
|
|
4
5
|
// <nk-tab value="notes">📝 Notes</nk-tab>
|
|
@@ -9,10 +10,13 @@ import { NkElement } from '../../base.js';
|
|
|
9
10
|
// → <div class="nk-tabs" role="tablist">…</div> + the panels below it.
|
|
10
11
|
// The tabs keep one tab active and hide every panel whose data-tab does not
|
|
11
12
|
// match `value` (through the hidden attribute – the panels stay yours).
|
|
13
|
+
// `scroll` (NotionKit 1.12.0) keeps many tabs in one row that scrolls
|
|
14
|
+
// sideways and holds the active tab in view – after the first layout, on
|
|
15
|
+
// every change and when the row's width changes – as <nk-segmented scroll>.
|
|
12
16
|
const KEYS = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };
|
|
13
17
|
|
|
14
18
|
class NkTabs extends NkElement {
|
|
15
|
-
static get observedAttributes() { return ['value']; }
|
|
19
|
+
static get observedAttributes() { return ['value', 'scroll']; }
|
|
16
20
|
|
|
17
21
|
render() {
|
|
18
22
|
this._strip = this.createElement('div', ['nk-tabs'], { role: 'tablist' });
|
|
@@ -40,6 +44,20 @@ class NkTabs extends NkElement {
|
|
|
40
44
|
}
|
|
41
45
|
for (const panel of this.panels) panel.hidden = panel.dataset.tab !== value;
|
|
42
46
|
this._value = value;
|
|
47
|
+
this._strip.classList.toggle('scroll', this.getBoolAttr('scroll'));
|
|
48
|
+
this._reveal(this._revealed ? 'smooth' : 'instant');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Scrolls the row the least distance that shows the active tab whole. */
|
|
52
|
+
_reveal(behavior) {
|
|
53
|
+
if (!this.getBoolAttr('scroll') || !this._strip?.clientWidth) return;
|
|
54
|
+
const tab = this.tabs.find(t => t.hasAttribute('active'));
|
|
55
|
+
if (!tab) return;
|
|
56
|
+
const row = this._strip.getBoundingClientRect(), r = boxOf(tab), pad = 8;
|
|
57
|
+
const delta = r.left < row.left ? r.left - row.left - pad : r.right > row.right ? r.right - row.right + pad : 0;
|
|
58
|
+
const smooth = behavior === 'smooth' && !matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
59
|
+
if (delta) this._strip.scrollBy({ left: delta, behavior: smooth ? 'smooth' : 'instant' });
|
|
60
|
+
this._revealed = true;
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
setupEvents() {
|
|
@@ -58,6 +76,8 @@ class NkTabs extends NkElement {
|
|
|
58
76
|
this._tabSlot.addEventListener('slotchange', this._onSlot);
|
|
59
77
|
this._panelSlot.addEventListener('slotchange', this._onSlot);
|
|
60
78
|
this.addEventListener('keydown', this._onKey);
|
|
79
|
+
this._resize = new ResizeObserver(() => this._reveal('instant'));
|
|
80
|
+
this._resize.observe(this._strip);
|
|
61
81
|
this._sync();
|
|
62
82
|
}
|
|
63
83
|
|
|
@@ -65,6 +85,7 @@ class NkTabs extends NkElement {
|
|
|
65
85
|
this._tabSlot?.removeEventListener('slotchange', this._onSlot);
|
|
66
86
|
this._panelSlot?.removeEventListener('slotchange', this._onSlot);
|
|
67
87
|
this.removeEventListener('keydown', this._onKey);
|
|
88
|
+
this._resize?.disconnect();
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
onAttributeChanged() {
|
|
@@ -75,6 +96,8 @@ class NkTabs extends NkElement {
|
|
|
75
96
|
|
|
76
97
|
get value() { return this._value ?? this.getAttribute('value'); }
|
|
77
98
|
set value(v) { this.setAttribute('value', v); }
|
|
99
|
+
get scroll() { return this.getBoolAttr('scroll'); }
|
|
100
|
+
set scroll(v) { this.setBoolAttr('scroll', v); }
|
|
78
101
|
}
|
|
79
102
|
|
|
80
103
|
customElements.define('nk-tabs', NkTabs);
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
// be styled. Exported for consumers who render tables themselves.
|
|
5
5
|
//
|
|
6
6
|
// column: { key, label, type, icon, options: [{ value, label, color }], title }
|
|
7
|
-
// types: text | select | multi-select | date | person | checkbox | url | number | progress
|
|
7
|
+
// types: text | select | multi-select | date | person | checkbox | url | number | progress | actions
|
|
8
|
+
// url: a string is an address outside, opened in a new tab; { href, label,
|
|
9
|
+
// target } a link of your own – to another page of the app, with its text.
|
|
10
|
+
// actions: buttons for the row from column.actions [{ action, label, icon,
|
|
11
|
+
// danger, disabled, tooltip }]; the row's value – action names or objects –
|
|
12
|
+
// picks which of them it shows, all of them without one.
|
|
8
13
|
import { paintAvatar, initialsOf } from './avatar.js';
|
|
9
14
|
|
|
10
15
|
const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
|
|
@@ -23,6 +28,7 @@ export function tagFor(column, value) {
|
|
|
23
28
|
|
|
24
29
|
export function renderPropertyCell(column, value, row = {}) {
|
|
25
30
|
const type = column.type || 'text';
|
|
31
|
+
if (type === 'actions') return actions(column, value);
|
|
26
32
|
if (value === undefined || value === null || value === '') {
|
|
27
33
|
if (type === 'checkbox') return checkbox(column, false, row);
|
|
28
34
|
if (type === 'progress') return progress(0);
|
|
@@ -47,7 +53,14 @@ export function renderPropertyCell(column, value, row = {}) {
|
|
|
47
53
|
return cell;
|
|
48
54
|
}
|
|
49
55
|
case 'checkbox': return checkbox(column, !!value, row);
|
|
50
|
-
case 'url': {
|
|
56
|
+
case 'url': {
|
|
57
|
+
const link = typeof value === 'object' ? value : { href: String(value), label: String(value).replace(/^https?:\/\//, ''), target: '_blank' };
|
|
58
|
+
const a = el('a', null, link.label ?? link.href);
|
|
59
|
+
a.href = link.href ?? '';
|
|
60
|
+
if (link.target) a.target = link.target;
|
|
61
|
+
if (link.target === '_blank') a.rel = 'noopener';
|
|
62
|
+
return a;
|
|
63
|
+
}
|
|
51
64
|
// column.locale / column.format: Intl.NumberFormat locale and options, e.g. { minimumFractionDigits: 1 }.
|
|
52
65
|
case 'number': return el('span', null, typeof value === 'number' ? value.toLocaleString(column.locale, column.format) : String(value));
|
|
53
66
|
case 'progress': return progress(Number(value) || 0);
|
|
@@ -63,6 +76,22 @@ export function renderPropertyCell(column, value, row = {}) {
|
|
|
63
76
|
}
|
|
64
77
|
}
|
|
65
78
|
|
|
79
|
+
function actions(column, value) {
|
|
80
|
+
const all = column.actions || [];
|
|
81
|
+
const list = value == null ? all
|
|
82
|
+
: (Array.isArray(value) ? value : [value]).map(v => typeof v === 'string' ? all.find(a => a.action === v) ?? { action: v, label: v } : v);
|
|
83
|
+
const wrap = el('span', 'row-actions');
|
|
84
|
+
for (const a of list) {
|
|
85
|
+
const b = el('button', `nk-btn ${a.danger ? 'danger' : 'secondary'} small`, [a.icon, a.label ?? a.action].filter(Boolean).join(' '));
|
|
86
|
+
b.type = 'button';
|
|
87
|
+
b.dataset.action = a.action;
|
|
88
|
+
b.disabled = !!a.disabled;
|
|
89
|
+
if (a.tooltip) b.dataset.tooltip = a.tooltip;
|
|
90
|
+
wrap.appendChild(b);
|
|
91
|
+
}
|
|
92
|
+
return wrap;
|
|
93
|
+
}
|
|
94
|
+
|
|
66
95
|
function checkbox(column, checked, row) {
|
|
67
96
|
const label = el('label', 'nk-check');
|
|
68
97
|
const input = el('input');
|