@jungherz-de/notionkit-elements 1.10.1 → 1.11.1
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 +641 -0
- package/README.md +6 -6
- package/SKILL.md +3748 -0
- package/dist/components/nk-ai-msg.js +6 -4
- package/dist/components/nk-avatar.js +1 -1
- package/dist/components/nk-banner.js +5 -2
- package/dist/components/nk-board-view.js +2 -2
- package/dist/components/nk-calendar.js +2 -2
- package/dist/components/nk-comment.js +1 -1
- package/dist/components/nk-dialog.js +14 -8
- package/dist/components/nk-fields.js +13 -3
- package/dist/components/nk-filter-bar.js +5 -3
- package/dist/components/nk-list-view.js +2 -2
- package/dist/components/nk-member-row.js +1 -1
- package/dist/components/nk-menu.js +2 -2
- package/dist/components/nk-page.js +3 -2
- package/dist/components/nk-select.js +15 -7
- package/dist/components/nk-switch.js +5 -1
- package/dist/components/nk-table-view.js +2 -2
- package/dist/components/nk-tooltip.js +17 -7
- package/dist/components/nk-tree-item.js +10 -4
- package/dist/components/shared/{avatar-BiqCaHOt.js → avatar-CzPZN3uP.js} +2 -2
- package/dist/components/shared/floating-CtqV6MBj.js +66 -0
- package/dist/components/shared/{property-cell-o7N30kAQ.js → property-cell-Cddz5n2O.js} +1 -1
- package/dist/notionkit-elements.esm.js +137 -56
- package/dist/notionkit-elements.js +137 -56
- package/dist/notionkit-elements.min.js +1 -1
- package/package.json +7 -4
- package/src/components/data/nk-ai-msg.js +6 -4
- package/src/components/data/nk-filter-bar.js +5 -3
- package/src/components/forms/nk-calendar.js +1 -1
- package/src/components/forms/nk-fields.js +13 -3
- package/src/components/forms/nk-select.js +15 -7
- package/src/components/forms/nk-switch.js +5 -1
- package/src/components/overlays/nk-dialog.js +14 -8
- package/src/components/overlays/nk-menu.js +1 -1
- package/src/components/overlays/nk-tooltip.js +16 -6
- package/src/components/page/nk-banner.js +5 -2
- package/src/components/page/nk-page.js +3 -2
- package/src/components/shell/nk-tree-item.js +10 -4
- package/src/util/avatar.js +2 -2
- package/src/util/floating.js +39 -10
- package/dist/components/shared/floating-CxHxy-Cb.js +0 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jungherz-de/notionkit-elements",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
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)",
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"dist/",
|
|
29
29
|
"src/",
|
|
30
|
-
"LICENSE"
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"SKILL.md",
|
|
32
|
+
"CHANGELOG.md"
|
|
31
33
|
],
|
|
32
34
|
"scripts": {
|
|
33
35
|
"prebuild": "node -e \"require('fs').rmSync('dist/components',{recursive:true,force:true})\"",
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
"build:all": "npm run build && npm run build:pages && npm run build:landing && npm run build:app && npm run build:skill && npm run build:meta",
|
|
39
41
|
"check:versions": "node check-versions.mjs",
|
|
40
42
|
"check:coverage": "node tools/check-coverage.mjs",
|
|
43
|
+
"check:package": "node tools/check-package.mjs",
|
|
41
44
|
"test": "playwright test",
|
|
42
45
|
"dev": "rollup -c --watch",
|
|
43
46
|
"serve": "npx serve .",
|
|
@@ -59,10 +62,10 @@
|
|
|
59
62
|
"dark-mode"
|
|
60
63
|
],
|
|
61
64
|
"peerDependencies": {
|
|
62
|
-
"@jungherz-de/notionkit": ">=1.
|
|
65
|
+
"@jungherz-de/notionkit": ">=1.11.0"
|
|
63
66
|
},
|
|
64
67
|
"devDependencies": {
|
|
65
|
-
"@jungherz-de/notionkit": "^1.
|
|
68
|
+
"@jungherz-de/notionkit": "^1.11.1",
|
|
66
69
|
"@playwright/test": "^1.50.0",
|
|
67
70
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
68
71
|
"@rollup/plugin-terser": "^0.4.0",
|
|
@@ -37,13 +37,15 @@ class NkAiMsg extends NkElement {
|
|
|
37
37
|
this._box.classList.toggle('user', user);
|
|
38
38
|
this._box.classList.toggle('bubble', bubble);
|
|
39
39
|
const name = this.getAttribute('name');
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this.
|
|
40
|
+
// Runs on slotchange: write text on a change only (see nk-switch).
|
|
41
|
+
if (this._nameText.data !== (name || '')) this._nameText.data = name || '';
|
|
42
|
+
const badge = this.getAttribute('badge'), badgeText = badge ? ` ${badge}` : '';
|
|
43
|
+
if (this._badge.textContent !== badgeText) this._badge.textContent = badgeText;
|
|
43
44
|
// A bubble stands without avatar and name; the text says who asked.
|
|
44
45
|
this._name.style.display = name && !bubble ? '' : 'none';
|
|
45
46
|
this._avatarSlot.style.display = bubble ? 'none' : '';
|
|
46
|
-
|
|
47
|
+
const avatar = this.getAttribute('avatar') || (user ? (name || 'U').slice(0, 2).toUpperCase() : '✨');
|
|
48
|
+
if (this._avatar.textContent !== avatar) this._avatar.textContent = avatar;
|
|
47
49
|
const color = this.getAttribute('color');
|
|
48
50
|
this._avatar.style.background = color || '';
|
|
49
51
|
this._actions.style.display = this._actionSlot.assignedElements().length ? '' : 'none';
|
|
@@ -58,7 +58,7 @@ class NkFilterBar extends NkElement {
|
|
|
58
58
|
setupEvents() {
|
|
59
59
|
this._onClick = (e) => {
|
|
60
60
|
const remove = e.target.closest('[data-remove]');
|
|
61
|
-
if (remove) {
|
|
61
|
+
if (remove) { const i = Number(remove.dataset.remove); this._filters = this._filters.filter((_, j) => j !== i); this._sync(); this._emitChange(); return; }
|
|
62
62
|
const edit = e.target.closest('[data-edit]');
|
|
63
63
|
if (edit) { const index = Number(edit.dataset.edit); this.emit('nk-action', { action: 'edit', index, filter: this._filters[index], anchor: edit }); return; }
|
|
64
64
|
const btn = e.target.closest('[data-action]');
|
|
@@ -78,8 +78,10 @@ class NkFilterBar extends NkElement {
|
|
|
78
78
|
_emitChange() { this.emit('nk-change', { filters: [...this._filters], search: this._input.value }); }
|
|
79
79
|
onAttributeChanged() { this._sync(); }
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
// A copy each way: the caller's array is never changed – removing a pill
|
|
82
|
+
// only reports the new list in nk-change.
|
|
83
|
+
get filters() { return [...this._filters]; }
|
|
84
|
+
set filters(v) { this._filters = Array.isArray(v) ? [...v] : []; if (this._row) this._sync(); }
|
|
83
85
|
get value() { return this._input?.value ?? ''; }
|
|
84
86
|
set value(v) { if (this._input) this._input.value = v ?? ''; }
|
|
85
87
|
/**
|
|
@@ -43,7 +43,7 @@ import { deepActiveElement } from '../../util/focus.js';
|
|
|
43
43
|
// outside does. Put a floating calendar directly under <body>.
|
|
44
44
|
const STEPS = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };
|
|
45
45
|
const floatSheet = new CSSStyleSheet();
|
|
46
|
-
floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; z-index: 60; pointer-events: none; }`);
|
|
46
|
+
floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; top: 0; left: 0; z-index: 60; pointer-events: none; }`);
|
|
47
47
|
|
|
48
48
|
class NkCalendar extends NkFormElement {
|
|
49
49
|
static get hostStyles() { return floatSheet; }
|
|
@@ -9,12 +9,22 @@ import { NkElement } from '../../base.js';
|
|
|
9
9
|
// nk-field inside renders itself stacked and compact (12px label above a
|
|
10
10
|
// full-width control) – the class markup does the same through
|
|
11
11
|
// `.nk-fields > .nk-field`, which cannot cross the shadow boundary here.
|
|
12
|
+
// `fit` (NotionKit 1.11.0) lets the fields share the row instead of keeping
|
|
13
|
+
// 150px columns: two fields in a wide dialog are two halves.
|
|
12
14
|
class NkFields extends NkElement {
|
|
15
|
+
static get observedAttributes() { return ['fit']; }
|
|
16
|
+
|
|
13
17
|
render() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
this._wrapper.appendChild(
|
|
18
|
+
this._grid = this.createElement('div', ['nk-fields']);
|
|
19
|
+
this._grid.appendChild(document.createElement('slot'));
|
|
20
|
+
this._wrapper.appendChild(this._grid);
|
|
21
|
+
this.onAttributeChanged();
|
|
17
22
|
}
|
|
23
|
+
|
|
24
|
+
onAttributeChanged() { this._grid?.classList.toggle('fit', this.getBoolAttr('fit')); }
|
|
25
|
+
|
|
26
|
+
get fit() { return this.getBoolAttr('fit'); }
|
|
27
|
+
set fit(v) { this.setBoolAttr('fit', v); }
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
customElements.define('nk-fields', NkFields);
|
|
@@ -7,7 +7,9 @@ import { NkFormElement } from '../../base.js';
|
|
|
7
7
|
//
|
|
8
8
|
// The light-DOM <option>s are copied into the shadow <select>. A
|
|
9
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.
|
|
10
|
+
// empty string is a valid value; the live selection survives a rebuild. A
|
|
11
|
+
// value set before its option exists – a framework sets properties before
|
|
12
|
+
// children – waits and is applied once the option arrives.
|
|
11
13
|
class NkSelect extends NkFormElement {
|
|
12
14
|
static get observedAttributes() { return ['value', 'name', 'disabled', 'required', 'compact', 'wide', 'aria-label']; }
|
|
13
15
|
static get observesLightDom() { return true; }
|
|
@@ -50,9 +52,11 @@ class NkSelect extends NkFormElement {
|
|
|
50
52
|
this._select.innerHTML = '';
|
|
51
53
|
for (const node of nodes) this._select.appendChild(node.cloneNode(true));
|
|
52
54
|
|
|
53
|
-
//
|
|
54
|
-
// otherwise the
|
|
55
|
-
|
|
55
|
+
// A value that waited for its option first; then the live selection when
|
|
56
|
+
// it survived; otherwise the value attribute; otherwise the browser
|
|
57
|
+
// default (first option / `selected`).
|
|
58
|
+
if (this._pending != null && this._applyValue(this._pending)) this._pending = null;
|
|
59
|
+
else if (!this._applyValue(previous)) this._applyValue(this.getAttribute('value'));
|
|
56
60
|
this._syncFormValue();
|
|
57
61
|
}
|
|
58
62
|
|
|
@@ -66,6 +70,7 @@ class NkSelect extends NkFormElement {
|
|
|
66
70
|
|
|
67
71
|
setupEvents() {
|
|
68
72
|
this._onChange = () => {
|
|
73
|
+
this._pending = null;
|
|
69
74
|
this._syncFormValue();
|
|
70
75
|
this.emit('nk-change', { value: this._select.value, name: this.name });
|
|
71
76
|
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
@@ -106,10 +111,13 @@ class NkSelect extends NkFormElement {
|
|
|
106
111
|
|
|
107
112
|
focus(options) { this._select?.focus(options); }
|
|
108
113
|
|
|
109
|
-
get value() { return this._select?.value ?? ''; }
|
|
114
|
+
get value() { return this._pending ?? this._select?.value ?? ''; }
|
|
110
115
|
set value(v) {
|
|
111
|
-
|
|
112
|
-
|
|
116
|
+
const value = v == null ? '' : String(v);
|
|
117
|
+
if (!this._select) { this.setAttribute('value', value); return; }
|
|
118
|
+
// No option carries it yet: keep it until one does.
|
|
119
|
+
this._pending = this._applyValue(value) ? null : value;
|
|
120
|
+
this._syncFormValue();
|
|
113
121
|
}
|
|
114
122
|
get selectedIndex() { return this._select?.selectedIndex ?? -1; }
|
|
115
123
|
get options() { return this._select?.options ?? []; }
|
|
@@ -36,7 +36,11 @@ class NkSwitch extends NkFormElement {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
_syncText() {
|
|
39
|
-
|
|
39
|
+
// Only on a change: WebKit fires slotchange for a write to the slot's
|
|
40
|
+
// fallback text, even the same text, and this runs on slotchange – an
|
|
41
|
+
// endless loop that froze Safari with an empty <nk-switch>.
|
|
42
|
+
const text = this.getAttribute('text') ?? '';
|
|
43
|
+
if (this._textNode.data !== text) this._textNode.data = text;
|
|
40
44
|
const hasText = !!(this.getAttribute('text') || this._slot.assignedNodes().some(n => n.nodeType === Node.ELEMENT_NODE || n.data.trim()));
|
|
41
45
|
this._text.hidden = !hasText;
|
|
42
46
|
this._row.style.display = hasText ? '' : 'contents';
|
|
@@ -17,10 +17,13 @@ import { lockScroll, unlockScroll, inertOutside, firstFocusable, deepActiveEleme
|
|
|
17
17
|
// scroll-locked. It lies above the modal and the sheet, so a question can
|
|
18
18
|
// come from either. 440px, `wide` 560px; below 860px a bottom sheet with the
|
|
19
19
|
// buttons stacked, the confirming one on top.
|
|
20
|
-
// A button in slot="actions" with a `value` closes it with that value
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
20
|
+
// A button in slot="actions" with a `value` closes it with that value, and
|
|
21
|
+
// one anywhere inside with `data-close` with that attribute's value – a
|
|
22
|
+
// button with a value elsewhere, an option of <nk-segmented> say, leaves it
|
|
23
|
+
// open. The submit of a <form method="dialog"> inside closes it too, with
|
|
24
|
+
// its submitter's value. Before it closes, nk-close { value } fires and can
|
|
25
|
+
// be cancelled – the place to check an input dialog; Escape and the
|
|
26
|
+
// backdrop close with ''.
|
|
24
27
|
// `returnValue` keeps the last value. `alert` makes it an alertdialog, named
|
|
25
28
|
// and described by its title and text. `title` is the heading, never a
|
|
26
29
|
// tooltip – see NkElement.takeTitle(). Put it directly under <body>.
|
|
@@ -90,11 +93,14 @@ class NkDialog extends NkElement {
|
|
|
90
93
|
setupEvents() {
|
|
91
94
|
this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(''); };
|
|
92
95
|
this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) { e.stopPropagation(); this.close(''); } };
|
|
93
|
-
// A button with a value closes it
|
|
96
|
+
// A button in the actions with a value closes it, and one with data-close
|
|
97
|
+
// anywhere inside; a submit button is the form's.
|
|
94
98
|
this._onClick = (e) => {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
const path = e.composedPath(), own = path.slice(0, Math.max(0, path.indexOf(this))).filter(n => n instanceof Element && this.contains(n));
|
|
100
|
+
const button = own.find(n => n.localName === 'nk-btn' || n.localName === 'button');
|
|
101
|
+
if (!button || button.getAttribute('type') === 'submit' || button.hasAttribute('disabled')) return;
|
|
102
|
+
if (button.hasAttribute('data-close')) { this.close(button.getAttribute('data-close')); return; }
|
|
103
|
+
if (button.hasAttribute('value') && own.some(n => n.getAttribute('slot') === 'actions')) this.close(button.getAttribute('value'));
|
|
98
104
|
};
|
|
99
105
|
this._onSubmit = (e) => {
|
|
100
106
|
if ((e.target.getAttribute('method') || '').toLowerCase() !== 'dialog') return;
|
|
@@ -19,7 +19,7 @@ import { placeUnder } from '../../util/floating.js';
|
|
|
19
19
|
// close it too, a switch row does not. Opened from the keyboard, focus moves
|
|
20
20
|
// to the first item; closing returns it to where it was.
|
|
21
21
|
const floatSheet = new CSSStyleSheet();
|
|
22
|
-
floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; z-index: 60; pointer-events: none; }`);
|
|
22
|
+
floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; top: 0; left: 0; z-index: 60; pointer-events: none; }`);
|
|
23
23
|
|
|
24
24
|
class NkMenu extends NkElement {
|
|
25
25
|
static get hostStyles() { return floatSheet; }
|
|
@@ -14,12 +14,15 @@ import { deepActiveElement } from '../../util/focus.js';
|
|
|
14
14
|
// element. With `for` it belongs to the element with that id and shows its
|
|
15
15
|
// own content and `shortcut`. It appears after `delay` ms (400) under the
|
|
16
16
|
// pointer and at once on keyboard focus, never on touch; leaving, a press,
|
|
17
|
-
// blur, scrolling
|
|
18
|
-
// centred, above it where the window
|
|
19
|
-
// and 8px inside the window
|
|
20
|
-
// text, shortcut) takes an
|
|
21
|
-
//
|
|
22
|
-
//
|
|
17
|
+
// blur, scrolling – the wheel too, inside a shadow root – and Escape hide
|
|
18
|
+
// it. It sits 6px below its target and centred, above it where the window
|
|
19
|
+
// ends – placement="top" prefers above – and 8px inside the window
|
|
20
|
+
// (placeNear in util/floating.js). show(target, text, shortcut) takes an
|
|
21
|
+
// element or a rect, for what has no element of its own: the bars of a
|
|
22
|
+
// Gantt chart; such a tooltip stays until hide(), the pointer on another
|
|
23
|
+
// [data-tooltip], a press or Escape. A line break in the text is kept.
|
|
24
|
+
// While it shows, the target's aria-describedby names it (in the same
|
|
25
|
+
// document or shadow root).
|
|
23
26
|
const hostSheet = new CSSStyleSheet();
|
|
24
27
|
hostSheet.replaceSync(`:host { display: block; position: fixed; top: 0; left: 0; z-index: 130; pointer-events: none; }`);
|
|
25
28
|
let count = 0;
|
|
@@ -44,6 +47,7 @@ class NkTooltip extends NkElement {
|
|
|
44
47
|
_fill(text, key = this.getAttribute('shortcut')) {
|
|
45
48
|
this._slot.style.display = text == null ? '' : 'none';
|
|
46
49
|
this._text.data = text ?? '';
|
|
50
|
+
this._bubble.classList.toggle('lines', (text ?? '').includes('\n'));
|
|
47
51
|
this._key.textContent = key || '';
|
|
48
52
|
this._key.hidden = !key;
|
|
49
53
|
}
|
|
@@ -52,6 +56,9 @@ class NkTooltip extends NkElement {
|
|
|
52
56
|
show(target, text, shortcut) {
|
|
53
57
|
clearTimeout(this._timer);
|
|
54
58
|
if (!target) return;
|
|
59
|
+
// What the pointer is on now: a rect has no element, so moving over the
|
|
60
|
+
// page leaves a tooltip shown for one alone.
|
|
61
|
+
this._target = target instanceof Element ? target : null;
|
|
55
62
|
this._fill(text ?? null, shortcut);
|
|
56
63
|
this._describe(target);
|
|
57
64
|
placeNear(this, target, this.getAttribute('placement') === 'top' ? 'top' : 'bottom');
|
|
@@ -116,6 +123,8 @@ class NkTooltip extends NkElement {
|
|
|
116
123
|
document.addEventListener('focusout', this._onHide);
|
|
117
124
|
document.addEventListener('pointerdown', this._onHide, true);
|
|
118
125
|
document.addEventListener('scroll', this._onHide, true);
|
|
126
|
+
// Scroll events do not leave a shadow root; the wheel does.
|
|
127
|
+
document.addEventListener('wheel', this._onHide, { capture: true, passive: true });
|
|
119
128
|
document.addEventListener('keydown', this._onKey);
|
|
120
129
|
}
|
|
121
130
|
|
|
@@ -125,6 +134,7 @@ class NkTooltip extends NkElement {
|
|
|
125
134
|
document.removeEventListener('focusout', this._onHide);
|
|
126
135
|
document.removeEventListener('pointerdown', this._onHide, true);
|
|
127
136
|
document.removeEventListener('scroll', this._onHide, true);
|
|
137
|
+
document.removeEventListener('wheel', this._onHide, { capture: true });
|
|
128
138
|
document.removeEventListener('keydown', this._onKey);
|
|
129
139
|
this.hide();
|
|
130
140
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
2
|
|
|
3
|
-
// <nk-banner variant="info">ℹ️ <span>Text</span><
|
|
4
|
-
// → <div class="nk-banner info">…<
|
|
3
|
+
// <nk-banner variant="info">ℹ️ <span>Text</span><button slot="action" type="button">Open</button></nk-banner>
|
|
4
|
+
// → <div class="nk-banner info">…<button class="b-action" type="button">Open</button></div>
|
|
5
|
+
//
|
|
6
|
+
// The action is a <button>, which NotionKit (1.11.0) strips down to
|
|
7
|
+
// underlined text; below 860px it moves under the text.
|
|
5
8
|
const VARIANTS = ['info', 'success', 'warning', 'danger'];
|
|
6
9
|
|
|
7
10
|
class NkBanner extends NkElement {
|
|
@@ -43,8 +43,9 @@ class NkPage extends NkElement {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
_sync() {
|
|
46
|
-
const icon = this.getAttribute('icon');
|
|
47
|
-
|
|
46
|
+
const icon = this.getAttribute('icon') || '';
|
|
47
|
+
// Fallback content: write on a change only (see nk-switch).
|
|
48
|
+
if (this._icon.textContent !== icon) this._icon.textContent = icon;
|
|
48
49
|
this._icon.style.display = icon ? '' : 'none';
|
|
49
50
|
this._cover.style.display = this.getBoolAttr('cover') ? '' : 'none';
|
|
50
51
|
// `covered` is the stylesheet's twin of `.nk-cover + .nk-page`: the icon
|
|
@@ -59,7 +59,10 @@ class NkTreeItem extends NkElement {
|
|
|
59
59
|
else if (slot === 'end') end.push(node);
|
|
60
60
|
else if (node.localName === 'nk-tree-item') children.push(node);
|
|
61
61
|
else label.push(node);
|
|
62
|
-
} else if (node.nodeType === Node.TEXT_NODE
|
|
62
|
+
} else if (node.nodeType === Node.TEXT_NODE) {
|
|
63
|
+
// Every text node, empty and blank ones too: a template engine fills
|
|
64
|
+
// an expression's node after the item is built ("${i} ${title}"),
|
|
65
|
+
// and the blank node between two expressions is the space.
|
|
63
66
|
label.push(node);
|
|
64
67
|
}
|
|
65
68
|
}
|
|
@@ -82,9 +85,12 @@ class NkTreeItem extends NkElement {
|
|
|
82
85
|
if (this._hasChildren) this._row.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
83
86
|
else this._row.removeAttribute('aria-expanded');
|
|
84
87
|
this._children.classList.toggle('collapsed', !open || !this._hasChildren);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
this.
|
|
88
|
+
// Fallback content: write on a change only (see nk-switch).
|
|
89
|
+
const icon = this.getAttribute('icon') || '';
|
|
90
|
+
if (this._icon.textContent !== icon) this._icon.textContent = icon;
|
|
91
|
+
this._icon.style.display = icon ? '' : 'none';
|
|
92
|
+
const label = this.getAttribute('label') || '';
|
|
93
|
+
if (this._labelText.data !== label) this._labelText.data = label;
|
|
88
94
|
// .actions is display:none until hover (a hover-parent rule); `hidden`
|
|
89
95
|
// would lose to that rule, so the block is detached instead.
|
|
90
96
|
const wantActions = !this.getBoolAttr('no-actions') && !this._hasEnd;
|
package/src/util/avatar.js
CHANGED
|
@@ -13,5 +13,5 @@ export function paintAvatar(el, color) {
|
|
|
13
13
|
el.style.background = color && !AVATAR_COLORS.includes(color) ? color : '';
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/** "Sara Lindt" → "SL". */
|
|
17
|
-
export const initialsOf = name => String(name || '').split(/\s+/).map(w => w[0]).filter(Boolean).join('').slice(0, 2).toUpperCase();
|
|
16
|
+
/** "Sara Lindt" → "SL", "Planer (Dev)" → "PD", "Anna-Lena Groß" → "AG": the first letter or digit of each word; a word without one is skipped. */
|
|
17
|
+
export const initialsOf = name => String(name || '').split(/\s+/).map(w => w.match(/[\p{L}\p{N}]/u)?.[0]).filter(Boolean).join('').slice(0, 2).toUpperCase();
|
package/src/util/floating.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// Placement for elements that float over the page – <nk-menu floating>,
|
|
2
|
-
// <nk-calendar floating>: the host is a fixed box put under
|
|
2
|
+
// <nk-calendar floating>, <nk-tooltip>: the host is a fixed box put under
|
|
3
|
+
// its anchor. A transformed ancestor – an open nk-modal or nk-dialog –
|
|
4
|
+
// becomes the containing block of a fixed box, so the frame it really sits
|
|
5
|
+
// in is measured and taken off.
|
|
3
6
|
|
|
4
7
|
/** The border box of an element, or of the first descendant with one (display: contents hosts). */
|
|
5
8
|
export function boxOf(el) {
|
|
@@ -11,6 +14,20 @@ export function boxOf(el) {
|
|
|
11
14
|
return el.getBoundingClientRect();
|
|
12
15
|
}
|
|
13
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Where a fixed `host` really sits: the viewport position of its containing
|
|
19
|
+
* block's left, top and right edges – the viewport's own, unless a
|
|
20
|
+
* transformed ancestor takes its place – and the host's size.
|
|
21
|
+
*/
|
|
22
|
+
function frameOf(host) {
|
|
23
|
+
const s = host.style;
|
|
24
|
+
s.top = '0px'; s.left = '0px'; s.right = 'auto';
|
|
25
|
+
const a = host.getBoundingClientRect();
|
|
26
|
+
s.left = 'auto'; s.right = '0px';
|
|
27
|
+
const b = host.getBoundingClientRect();
|
|
28
|
+
return { x: a.left, y: a.top, right: b.right, width: a.width, height: a.height };
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
/**
|
|
15
32
|
* Puts `host` 6px below `target` – an element or a rect ({ left, top, right,
|
|
16
33
|
* bottom }, a DOMRect) – centred on it: above with side="top", or where the
|
|
@@ -18,18 +35,30 @@ export function boxOf(el) {
|
|
|
18
35
|
*/
|
|
19
36
|
export function placeNear(host, target, side = 'bottom', gap = 6) {
|
|
20
37
|
const r = target instanceof Element ? boxOf(target) : target;
|
|
21
|
-
const
|
|
38
|
+
const f = frameOf(host), w = f.width, h = f.height;
|
|
22
39
|
const below = r.bottom + gap, above = r.top - gap - h;
|
|
23
40
|
const top = side === 'top' ? (above < 8 ? below : above) : (below + h > innerHeight - 8 ? above : below);
|
|
24
|
-
|
|
25
|
-
host.style.
|
|
26
|
-
host.style.
|
|
41
|
+
const left = Math.min(Math.max(8, (r.left + r.right) / 2 - w / 2), innerWidth - 8 - w);
|
|
42
|
+
host.style.top = `${Math.round(top - f.y)}px`;
|
|
43
|
+
host.style.left = `${Math.round(left - f.x)}px`;
|
|
44
|
+
host.style.right = 'auto';
|
|
27
45
|
}
|
|
28
46
|
|
|
29
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Puts `host` 6px under `anchor`, right edges aligned – left edges with
|
|
49
|
+
* align="start". Where the window ends below and there is more room above,
|
|
50
|
+
* it opens upwards; either way --_nk-float-max caps its height to the room
|
|
51
|
+
* there, and a longer menu scrolls.
|
|
52
|
+
*/
|
|
30
53
|
export function placeUnder(host, anchor, align) {
|
|
31
|
-
const r = boxOf(anchor);
|
|
32
|
-
host.style.
|
|
33
|
-
|
|
34
|
-
|
|
54
|
+
const r = boxOf(anchor), gap = 6, edge = 8;
|
|
55
|
+
host.style.removeProperty('--_nk-float-max');
|
|
56
|
+
const f = frameOf(host);
|
|
57
|
+
const roomBelow = innerHeight - r.bottom - gap - edge, roomAbove = r.top - gap - edge;
|
|
58
|
+
const up = f.height > roomBelow && roomAbove > roomBelow;
|
|
59
|
+
const room = Math.max(0, Math.floor(up ? roomAbove : roomBelow));
|
|
60
|
+
if (f.height > room) host.style.setProperty('--_nk-float-max', `${room}px`);
|
|
61
|
+
host.style.top = `${(up ? r.top - gap - Math.min(f.height, room) : r.bottom + gap) - f.y}px`;
|
|
62
|
+
if (align === 'start') { host.style.left = `${r.left - f.x}px`; host.style.right = 'auto'; }
|
|
63
|
+
else { host.style.right = `${f.right - Math.min(r.right, innerWidth - edge)}px`; host.style.left = 'auto'; }
|
|
35
64
|
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// Placement for elements that float over the page – <nk-menu floating>,
|
|
2
|
-
// <nk-calendar floating>: the host is a fixed box put under its anchor.
|
|
3
|
-
|
|
4
|
-
/** The border box of an element, or of the first descendant with one (display: contents hosts). */
|
|
5
|
-
function boxOf(el) {
|
|
6
|
-
if (el.getClientRects().length) return el.getBoundingClientRect();
|
|
7
|
-
for (const child of [...(el.shadowRoot?.children ?? []), ...el.children]) {
|
|
8
|
-
const r = boxOf(child);
|
|
9
|
-
if (r.width || r.height) return r;
|
|
10
|
-
}
|
|
11
|
-
return el.getBoundingClientRect();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Puts `host` 6px below `target` – an element or a rect ({ left, top, right,
|
|
16
|
-
* bottom }, a DOMRect) – centred on it: above with side="top", or where the
|
|
17
|
-
* window ends below; kept 8px inside the window. For <nk-tooltip>.
|
|
18
|
-
*/
|
|
19
|
-
function placeNear(host, target, side = 'bottom', gap = 6) {
|
|
20
|
-
const r = target instanceof Element ? boxOf(target) : target;
|
|
21
|
-
const { width: w, height: h } = host.getBoundingClientRect();
|
|
22
|
-
const below = r.bottom + gap, above = r.top - gap - h;
|
|
23
|
-
const top = side === 'top' ? (above < 8 ? below : above) : (below + h > innerHeight - 8 ? above : below);
|
|
24
|
-
host.style.top = `${Math.round(top)}px`;
|
|
25
|
-
host.style.left = `${Math.round(Math.min(Math.max(8, (r.left + r.right) / 2 - w / 2), innerWidth - 8 - w))}px`;
|
|
26
|
-
host.style.right = '';
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Puts `host` 6px under `anchor`, right edges aligned – left edges with align="start". */
|
|
30
|
-
function placeUnder(host, anchor, align) {
|
|
31
|
-
const r = boxOf(anchor);
|
|
32
|
-
host.style.top = `${r.bottom + 6}px`;
|
|
33
|
-
if (align === 'start') { host.style.left = `${r.left}px`; host.style.right = ''; }
|
|
34
|
-
else { host.style.right = `${Math.max(8, innerWidth - r.right)}px`; host.style.left = ''; }
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export { placeNear as a, placeUnder as p };
|