@jungherz-de/notionkit-elements 1.20.0 → 1.21.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 +61 -0
- package/README.md +6 -5
- package/SKILL.md +37 -34
- package/dist/components/base.js +2 -2
- package/dist/components/nk-table-view.js +46 -0
- package/dist/components/nk-tooltip.js +61 -5
- package/dist/notionkit-elements.esm.js +111 -9
- package/dist/notionkit-elements.esm.min.js +1 -1
- package/dist/notionkit-elements.esm.min.js.map +1 -1
- package/dist/notionkit-elements.js +111 -9
- package/dist/notionkit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/components/data/nk-table-view.js +46 -0
- package/src/components/overlays/nk-tooltip.js +61 -5
- package/src/util/strings.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jungherz-de/notionkit-elements",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.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)",
|
|
@@ -63,10 +63,10 @@
|
|
|
63
63
|
"dark-mode"
|
|
64
64
|
],
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@jungherz-de/notionkit": ">=1.
|
|
66
|
+
"@jungherz-de/notionkit": ">=1.21.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@jungherz-de/notionkit": "^1.
|
|
69
|
+
"@jungherz-de/notionkit": "^1.21.0",
|
|
70
70
|
"@playwright/test": "^1.50.0",
|
|
71
71
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
72
72
|
"@rollup/plugin-terser": "^0.4.0",
|
|
@@ -13,6 +13,15 @@ import { renderPropertyCell, compareBy } from '../../util/property-cell.js';
|
|
|
13
13
|
// type 'actions' (NotionKit 1.12.0) sets buttons in each row; a click on one
|
|
14
14
|
// fires nk-action { action, row, id, anchor } instead of selecting the row,
|
|
15
15
|
// and its header sorts nothing.
|
|
16
|
+
//
|
|
17
|
+
// A checkbox column with `selectAll: true` (1.21.0) is a selection column:
|
|
18
|
+
// its header holds a checkbox instead of a name – empty, a dash for some
|
|
19
|
+
// rows, a check for all – and sorts nothing (th.select-all, NotionKit
|
|
20
|
+
// 1.21.0). A click or Space sets the column for every row the view has –
|
|
21
|
+
// a dash selects all, as Notion does –, renders again and fires one
|
|
22
|
+
// nk-change { key, value, all: true, rows }, so an app that keeps its own
|
|
23
|
+
// selection follows with one update. What "all" means beyond these rows –
|
|
24
|
+
// more pages, a filter on the server – stays with the app.
|
|
16
25
|
class NkTableView extends NkElement {
|
|
17
26
|
static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'new-row', 'new-row-label', 'sortable', 'sort-key', 'sort-dir', 'wrap']; }
|
|
18
27
|
|
|
@@ -42,6 +51,7 @@ class NkTableView extends NkElement {
|
|
|
42
51
|
this._table.classList.toggle('wrap', this.getBoolAttr('wrap'));
|
|
43
52
|
const tr = document.createElement('tr');
|
|
44
53
|
for (const col of this._columns) {
|
|
54
|
+
if (col.type === 'checkbox' && col.selectAll) { tr.appendChild(this._selectAllHeader(col)); continue; }
|
|
45
55
|
const actions = col.type === 'actions';
|
|
46
56
|
const th = this.createElement('th', actions ? ['actions'] : [], actions ? { scope: 'col' } : { 'data-key': col.key, scope: 'col' });
|
|
47
57
|
if (col.icon) { const i = this.createElement('span', ['th-icon']); i.textContent = col.icon; th.appendChild(i); }
|
|
@@ -65,6 +75,40 @@ class NkTableView extends NkElement {
|
|
|
65
75
|
}
|
|
66
76
|
this._newRow.textContent = this.getAttribute('new-row-label') || this.str('newPage');
|
|
67
77
|
this._newRow.style.display = this.getBoolAttr('new-row') ? '' : 'none';
|
|
78
|
+
this._syncSelectAll();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The header of a selection column: the cells' own checkbox, named "Select all". */
|
|
82
|
+
_selectAllHeader(col) {
|
|
83
|
+
const th = this.createElement('th', ['select-all'], { scope: 'col' });
|
|
84
|
+
const label = this.createElement('label', ['nk-check']);
|
|
85
|
+
label.appendChild(this.createElement('input', [], { type: 'checkbox', 'aria-label': this.str('selectAll'), 'data-select-all': col.key }));
|
|
86
|
+
th.appendChild(label);
|
|
87
|
+
if (col.width) th.style.width = col.width;
|
|
88
|
+
return th;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Every header box shows its column: checked for all rows, a dash for some, empty for none – and nothing to choose without rows. */
|
|
92
|
+
_syncSelectAll() {
|
|
93
|
+
for (const input of this._thead?.querySelectorAll('input[data-select-all]') ?? []) {
|
|
94
|
+
const key = input.dataset.selectAll, on = this._rows.filter(r => r[key]).length;
|
|
95
|
+
input.checked = on > 0 && on === this._rows.length;
|
|
96
|
+
input.indeterminate = on > 0 && on < this._rows.length;
|
|
97
|
+
input.disabled = !this._rows.length;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A click or Space on a header box: the column for every row, one event. */
|
|
102
|
+
_selectAll(input) {
|
|
103
|
+
const key = input.dataset.selectAll;
|
|
104
|
+
// A dash selects all, as Notion does; only a full column clears.
|
|
105
|
+
const value = !(this._rows.length && this._rows.every(r => r[key]));
|
|
106
|
+
for (const row of this._rows) row[key] = value;
|
|
107
|
+
const focused = this._shadow.activeElement === input;
|
|
108
|
+
this._render();
|
|
109
|
+
// Space should leave the focus where it was – on the new header box.
|
|
110
|
+
if (focused) [...this._thead.querySelectorAll('input[data-select-all]')].find(i => i.dataset.selectAll === key)?.focus();
|
|
111
|
+
this.emit('nk-change', { key, value, all: true, rows: this._rows });
|
|
68
112
|
}
|
|
69
113
|
|
|
70
114
|
setupEvents() {
|
|
@@ -95,8 +139,10 @@ class NkTableView extends NkElement {
|
|
|
95
139
|
this._onChange = (e) => {
|
|
96
140
|
const input = e.target;
|
|
97
141
|
if (input.type !== 'checkbox') return;
|
|
142
|
+
if (input.dataset.selectAll != null) { this._selectAll(input); return; }
|
|
98
143
|
const row = this._rowById(input.dataset.rowId);
|
|
99
144
|
if (row) row[input.dataset.key] = input.checked;
|
|
145
|
+
this._syncSelectAll();
|
|
100
146
|
this.emit('nk-change', { row, key: input.dataset.key, value: input.checked });
|
|
101
147
|
};
|
|
102
148
|
this._onKey = (e) => { if ((e.key === 'Enter' || e.key === ' ') && e.target === this._newRow) { e.preventDefault(); this.emit('nk-action', { action: 'new-row' }); } };
|
|
@@ -23,6 +23,17 @@ import { deepActiveElement } from '../../util/focus.js';
|
|
|
23
23
|
// [data-tooltip], a press or Escape. A line break in the text is kept.
|
|
24
24
|
// While it shows, the target's aria-describedby names it (in the same
|
|
25
25
|
// document or shadow root).
|
|
26
|
+
//
|
|
27
|
+
// Following the pointer and the focus inside a shadow root (1.20.1):
|
|
28
|
+
// pointerover, focusin and focusout carry a relatedTarget, and when target
|
|
29
|
+
// and relatedTarget share a shadow tree the DOM standard ends the event path
|
|
30
|
+
// at its host – the document never hears of a move from one [data-tooltip]
|
|
31
|
+
// to the next in the same root. So the pointer is also read from
|
|
32
|
+
// pointermove, which has no relatedTarget, and acted on only when the
|
|
33
|
+
// element under it changes; and a key that moved the focus – Tab, the arrow
|
|
34
|
+
// keys of a group – is caught on keyup, comparing the focused element with
|
|
35
|
+
// the one before the key went down. A move by script alone inside one
|
|
36
|
+
// shadow root stays unseen.
|
|
26
37
|
const hostSheet = new CSSStyleSheet();
|
|
27
38
|
hostSheet.replaceSync(`:host { display: block; position: fixed; top: 0; left: 0; z-index: 130; pointer-events: none; }`);
|
|
28
39
|
let count = 0;
|
|
@@ -100,14 +111,50 @@ class NkTooltip extends NkElement {
|
|
|
100
111
|
this.show(el, own ? null : el.getAttribute('data-tooltip'), own ? this.getAttribute('shortcut') : el.getAttribute('data-tooltip-key'));
|
|
101
112
|
}
|
|
102
113
|
|
|
114
|
+
/**
|
|
115
|
+
* The pointer is over `el` now (null: no [data-tooltip]). pointerover acts
|
|
116
|
+
* on every boundary it crosses, as it always has; pointermove – the only
|
|
117
|
+
* one that reaches the document from inside a shadow root – only when the
|
|
118
|
+
* element under the pointer changed, so a hand resting on the page leaves
|
|
119
|
+
* a tooltip the keyboard or show(rect) opened alone.
|
|
120
|
+
*/
|
|
121
|
+
_pointer(e, move) {
|
|
122
|
+
const el = e.pointerType === 'touch' ? null : this._targetOf(e);
|
|
123
|
+
if (move && el === this._hover) return;
|
|
124
|
+
this._hover = el;
|
|
125
|
+
if (el === this._target) return;
|
|
126
|
+
this.hide();
|
|
127
|
+
if (!el) return;
|
|
128
|
+
this._target = el;
|
|
129
|
+
this._timer = setTimeout(() => this._showFor(el), this._delay);
|
|
130
|
+
}
|
|
131
|
+
|
|
103
132
|
setupEvents() {
|
|
104
|
-
this._onOver = (e) =>
|
|
105
|
-
|
|
106
|
-
|
|
133
|
+
this._onOver = (e) => this._pointer(e, false);
|
|
134
|
+
this._onMove = (e) => this._pointer(e, true);
|
|
135
|
+
// The pointer left the window (no relatedTarget): a tooltip – shown or
|
|
136
|
+
// still waiting – for what it was on goes with it. One the keyboard or
|
|
137
|
+
// show(rect) opened is not the pointer's and stays.
|
|
138
|
+
this._onOut = (e) => {
|
|
139
|
+
if (e.relatedTarget || e.pointerType === 'touch') return;
|
|
140
|
+
if (this._target && this._target === this._hover) this.hide();
|
|
141
|
+
this._hover = null;
|
|
142
|
+
};
|
|
143
|
+
// Capture, before an element's own keydown moves the focus (a roving tab
|
|
144
|
+
// stop does); not for a held key's repeats, so keyup compares with where
|
|
145
|
+
// the focus was before the key went down.
|
|
146
|
+
this._onKeyDown = (e) => { if (!e.repeat) this._focusBefore = deepActiveElement(); };
|
|
147
|
+
this._onKeyUp = (e) => {
|
|
148
|
+
const active = deepActiveElement();
|
|
149
|
+
if (active === this._focusBefore) return;
|
|
150
|
+
this._focusBefore = active;
|
|
151
|
+
// keyup goes to the focused element, so its path is the focus's path.
|
|
152
|
+
const el = this._targetOf(e);
|
|
153
|
+
if (el && el === this._target) return; // focusin got there first
|
|
107
154
|
this.hide();
|
|
108
|
-
if (!el) return;
|
|
155
|
+
if (!el || !active?.matches(':focus-visible')) return;
|
|
109
156
|
this._target = el;
|
|
110
|
-
this.
|
|
157
|
+
this._showFor(el);
|
|
111
158
|
};
|
|
112
159
|
this._onFocus = (e) => {
|
|
113
160
|
const el = this._targetOf(e);
|
|
@@ -119,7 +166,11 @@ class NkTooltip extends NkElement {
|
|
|
119
166
|
this._onHide = () => this.hide();
|
|
120
167
|
this._onKey = (e) => { if (e.key === 'Escape' && this.open) this.hide(); };
|
|
121
168
|
document.addEventListener('pointerover', this._onOver);
|
|
169
|
+
document.addEventListener('pointermove', this._onMove, { capture: true, passive: true });
|
|
170
|
+
document.addEventListener('pointerout', this._onOut);
|
|
122
171
|
document.addEventListener('focusin', this._onFocus);
|
|
172
|
+
document.addEventListener('keydown', this._onKeyDown, true);
|
|
173
|
+
document.addEventListener('keyup', this._onKeyUp, true);
|
|
123
174
|
document.addEventListener('focusout', this._onHide);
|
|
124
175
|
document.addEventListener('pointerdown', this._onHide, true);
|
|
125
176
|
document.addEventListener('scroll', this._onHide, true);
|
|
@@ -130,13 +181,18 @@ class NkTooltip extends NkElement {
|
|
|
130
181
|
|
|
131
182
|
teardownEvents() {
|
|
132
183
|
document.removeEventListener('pointerover', this._onOver);
|
|
184
|
+
document.removeEventListener('pointermove', this._onMove, { capture: true });
|
|
185
|
+
document.removeEventListener('pointerout', this._onOut);
|
|
133
186
|
document.removeEventListener('focusin', this._onFocus);
|
|
187
|
+
document.removeEventListener('keydown', this._onKeyDown, true);
|
|
188
|
+
document.removeEventListener('keyup', this._onKeyUp, true);
|
|
134
189
|
document.removeEventListener('focusout', this._onHide);
|
|
135
190
|
document.removeEventListener('pointerdown', this._onHide, true);
|
|
136
191
|
document.removeEventListener('scroll', this._onHide, true);
|
|
137
192
|
document.removeEventListener('wheel', this._onHide, { capture: true });
|
|
138
193
|
document.removeEventListener('keydown', this._onKey);
|
|
139
194
|
this.hide();
|
|
195
|
+
this._hover = this._focusBefore = null;
|
|
140
196
|
}
|
|
141
197
|
|
|
142
198
|
onAttributeChanged(name) { if (name === 'shortcut' && !this._text.data) this._fill(null); if (name === 'for') this.hide(); }
|
package/src/util/strings.js
CHANGED
|
@@ -19,7 +19,7 @@ const BUILT_IN = {
|
|
|
19
19
|
appearance: 'Appearance: {mode} – click: {next}', themeAuto: 'automatic (follows the system)', themeLight: 'light', themeDark: 'dark',
|
|
20
20
|
message: 'Message', breadcrumb: 'Breadcrumb', commandPalette: 'Command palette', comment: 'Comment',
|
|
21
21
|
searchEmoji: 'Search emoji', settings: 'Settings', title: 'Title', changeIcon: 'Change icon',
|
|
22
|
-
add: 'Add', more: 'More',
|
|
22
|
+
add: 'Add', more: 'More', selectAll: 'Select all',
|
|
23
23
|
},
|
|
24
24
|
de: {
|
|
25
25
|
copy: 'Kopieren', copied: 'Kopiert', show: 'Zeigen', hide: 'Verbergen',
|
|
@@ -34,7 +34,7 @@ const BUILT_IN = {
|
|
|
34
34
|
appearance: 'Darstellung: {mode} – Klick: {next}', themeAuto: 'automatisch (wie das System)', themeLight: 'hell', themeDark: 'dunkel',
|
|
35
35
|
message: 'Nachricht', breadcrumb: 'Pfad', commandPalette: 'Befehlspalette', comment: 'Kommentar',
|
|
36
36
|
searchEmoji: 'Emoji suchen', settings: 'Einstellungen', title: 'Titel', changeIcon: 'Symbol ändern',
|
|
37
|
-
add: 'Hinzufügen', more: 'Mehr',
|
|
37
|
+
add: 'Hinzufügen', more: 'Mehr', selectAll: 'Alle auswählen',
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
40
|
const custom = {};
|