@jungherz-de/notionkit-elements 1.7.0 β 1.9.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/README.md +15 -15
- package/dist/components/nk-bookmark.js +69 -0
- package/dist/components/nk-calendar-view.js +138 -0
- package/dist/components/nk-calendar.js +356 -0
- package/dist/components/nk-copy-field.js +88 -0
- package/dist/components/nk-image-picker.js +130 -0
- package/dist/components/nk-menu.js +2 -14
- package/dist/components/nk-peek.js +122 -0
- package/dist/components/nk-table-view.js +9 -3
- package/dist/components/shared/dates-a32DcW1l.js +67 -0
- package/dist/components/shared/floating-B8CwbXnV.js +22 -0
- package/dist/notionkit-elements.esm.js +974 -20
- package/dist/notionkit-elements.js +979 -19
- package/dist/notionkit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/components/content/nk-bookmark.js +67 -0
- package/src/components/data/nk-calendar-view.js +136 -0
- package/src/components/data/nk-table-view.js +9 -3
- package/src/components/forms/nk-calendar.js +354 -0
- package/src/components/forms/nk-copy-field.js +86 -0
- package/src/components/forms/nk-image-picker.js +128 -0
- package/src/components/overlays/nk-menu.js +2 -14
- package/src/components/overlays/nk-peek.js +120 -0
- package/src/index.js +8 -0
- package/src/util/dates.js +65 -0
- package/src/util/floating.js +20 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { NkElement } from './base.js';
|
|
2
|
+
import { d as deepActiveElement, l as lockScroll, i as inertOutside, u as unlockScroll } from './shared/focus-BNAChOXO.js';
|
|
3
|
+
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
4
|
+
|
|
5
|
+
// <nk-peek id="peek" label="Database Table-View">
|
|
6
|
+
// <nk-btn slot="actions" variant="topbar" aria-label="Open as page"><svg β¦></svg></nk-btn>
|
|
7
|
+
// <nk-page-title>ποΈ Database Table-View</nk-page-title>
|
|
8
|
+
// <nk-props>β¦</nk-props>
|
|
9
|
+
// <div class="nk-prose">β¦</div>
|
|
10
|
+
// <nk-comments>β¦</nk-comments>
|
|
11
|
+
// </nk-peek>
|
|
12
|
+
// β <div class="nk-peek-backdrop open"><aside class="nk-peek" role="dialog">
|
|
13
|
+
// <div class="pk-bar"><button class="nk-topbar-btn">Β»</button>β¦actionsβ¦</div>
|
|
14
|
+
// <div class="pk-body">β¦</div></aside></div>
|
|
15
|
+
//
|
|
16
|
+
// Notion's side peek (NotionKit 1.8.0): a row beside the table, which stays
|
|
17
|
+
// usable β no scrim, nothing inert. show() slides it in and moves focus to
|
|
18
|
+
// it; Β», Escape and a click elsewhere on the page close it, and Escape and Β»
|
|
19
|
+
// hand focus back. A click that calls show() again β on another row β only
|
|
20
|
+
// swaps the content, and clicks inside other overlays (a menu opened from
|
|
21
|
+
// the peek) leave it open. Below 860px it is a bottom sheet over a dimmed
|
|
22
|
+
// page; there it is modal: the page is inert and scroll-locked, the dimmed
|
|
23
|
+
// page takes the tap that closes it. Put it directly under <body>.
|
|
24
|
+
const CLOSE_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 17 5-5-5-5M13 17l5-5-5-5"/></svg>';
|
|
25
|
+
const OVERLAYS = new Set(['nk-menu', 'nk-pop', 'nk-modal', 'nk-sheet', 'nk-cmdk', 'nk-toast']);
|
|
26
|
+
const OVERLAY_CLASSES = ['nk-pop', 'nk-modal-backdrop', 'nk-sheet-backdrop', 'nk-cmdk-backdrop'];
|
|
27
|
+
const phone = () => matchMedia('(max-width: 860px)').matches;
|
|
28
|
+
|
|
29
|
+
class NkPeek extends NkElement {
|
|
30
|
+
static get observedAttributes() { return ['open', 'label', 'close-label']; }
|
|
31
|
+
|
|
32
|
+
render() {
|
|
33
|
+
this._backdrop = this.createElement('div', ['nk-peek-backdrop']);
|
|
34
|
+
this._box = this.createElement('aside', ['nk-peek'], { role: 'dialog', tabindex: '-1' });
|
|
35
|
+
const bar = this.createElement('div', ['pk-bar']);
|
|
36
|
+
this._close = this.createElement('button', ['nk-topbar-btn'], { type: 'button' });
|
|
37
|
+
this._close.innerHTML = CLOSE_ICON;
|
|
38
|
+
bar.append(this._close, this.createElement('slot', [], { name: 'actions' }));
|
|
39
|
+
const body = this.createElement('div', ['pk-body']);
|
|
40
|
+
body.appendChild(document.createElement('slot'));
|
|
41
|
+
this._box.append(bar, body);
|
|
42
|
+
this._backdrop.appendChild(this._box);
|
|
43
|
+
this._wrapper.appendChild(this._backdrop);
|
|
44
|
+
this._syncLabels();
|
|
45
|
+
this._syncOpen();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_syncLabels() {
|
|
49
|
+
const label = this.getAttribute('label');
|
|
50
|
+
if (label) this._box.setAttribute('aria-label', label); else this._box.removeAttribute('aria-label');
|
|
51
|
+
this._close.setAttribute('aria-label', this.getAttribute('close-label') || 'Close');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_syncOpen() {
|
|
55
|
+
const open = this.getBoolAttr('open');
|
|
56
|
+
this._backdrop.classList.toggle('open', open);
|
|
57
|
+
if (open === this._wasOpen) return;
|
|
58
|
+
this._wasOpen = open;
|
|
59
|
+
if (open) {
|
|
60
|
+
this._returnFocus = deepActiveElement();
|
|
61
|
+
if (phone()) {
|
|
62
|
+
this._modal = true;
|
|
63
|
+
this._box.setAttribute('aria-modal', 'true');
|
|
64
|
+
lockScroll();
|
|
65
|
+
this._undoInert = inertOutside(this);
|
|
66
|
+
}
|
|
67
|
+
requestAnimationFrame(() => this._box.focus({ preventScroll: true }));
|
|
68
|
+
} else {
|
|
69
|
+
if (this._modal) { this._modal = false; this._box.removeAttribute('aria-modal'); unlockScroll(); this._undoInert?.(); this._undoInert = null; }
|
|
70
|
+
const active = deepActiveElement();
|
|
71
|
+
const inside = active && (this.contains(active) || this._shadow.contains(active) || active === document.body);
|
|
72
|
+
const back = this._returnFocus;
|
|
73
|
+
this._returnFocus = null;
|
|
74
|
+
if (inside && back?.isConnected && typeof back.focus === 'function') back.focus({ preventScroll: true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setupEvents() {
|
|
79
|
+
this._onClose = () => this.close();
|
|
80
|
+
this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(); };
|
|
81
|
+
this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) this.close(); };
|
|
82
|
+
// Bubble phase, after the page's own handlers: a click that showed the
|
|
83
|
+
// peek again (another row) must not close it.
|
|
84
|
+
this._onOutside = (e) => {
|
|
85
|
+
if (!this.getBoolAttr('open') || this._shownAt >= e.timeStamp) return;
|
|
86
|
+
const path = e.composedPath();
|
|
87
|
+
if (path.includes(this)) return;
|
|
88
|
+
if (path.some(n => n.nodeType === 1 && (OVERLAYS.has(n.localName) || OVERLAY_CLASSES.some(c => n.classList.contains(c))))) return;
|
|
89
|
+
this.close();
|
|
90
|
+
};
|
|
91
|
+
this._close.addEventListener('click', this._onClose);
|
|
92
|
+
this._backdrop.addEventListener('click', this._onBackdrop);
|
|
93
|
+
document.addEventListener('keydown', this._onKey);
|
|
94
|
+
document.addEventListener('click', this._onOutside);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
teardownEvents() {
|
|
98
|
+
this._close?.removeEventListener('click', this._onClose);
|
|
99
|
+
this._backdrop?.removeEventListener('click', this._onBackdrop);
|
|
100
|
+
document.removeEventListener('keydown', this._onKey);
|
|
101
|
+
document.removeEventListener('click', this._onOutside);
|
|
102
|
+
if (this._modal) { this._modal = false; unlockScroll(); this._undoInert?.(); this._undoInert = null; }
|
|
103
|
+
this._wasOpen = false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
onAttributeChanged(name) {
|
|
107
|
+
if (name !== 'open') { this._syncLabels(); return; }
|
|
108
|
+
this._syncOpen();
|
|
109
|
+
this.emit('nk-toggle', { open: this.getBoolAttr('open') });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
show() { this._shownAt = performance.now(); this.setBoolAttr('open', true); }
|
|
113
|
+
close() { this.setBoolAttr('open', false); }
|
|
114
|
+
toggle() { this.getBoolAttr('open') ? this.close() : this.show(); }
|
|
115
|
+
|
|
116
|
+
get open() { return this.getBoolAttr('open'); }
|
|
117
|
+
set open(v) { v ? this.show() : this.close(); }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
customElements.define('nk-peek', NkPeek);
|
|
121
|
+
|
|
122
|
+
export { NkPeek };
|
|
@@ -7,8 +7,10 @@ import './shared/avatar-BiqCaHOt.js';
|
|
|
7
7
|
// `wrap` lets cell text break (`.nk-table.wrap`), like Notion's "wrap column".
|
|
8
8
|
// Standalone: view.columns = [β¦]; view.rows = [β¦]; Inside <nk-database> the
|
|
9
9
|
// database pushes `data`. β <div class="nk-table-wrap"><table class="nk-table">β¦</table><div class="nk-new-row">οΌ New page</div></div>
|
|
10
|
-
// Cells are plain markup from renderPropertyCell(); rows fire nk-select
|
|
11
|
-
//
|
|
10
|
+
// Cells are plain markup from renderPropertyCell(); rows fire nk-select
|
|
11
|
+
// { row, id, key, cell } β `key` and `cell` say which cell was hit, to open
|
|
12
|
+
// an editor there, like the date picker under a due date β checkboxes
|
|
13
|
+
// nk-change, headers nk-action { action: 'sort' } (and sort locally
|
|
12
14
|
// with `sortable`), the add row nk-action { action: 'new-row' }.
|
|
13
15
|
class NkTableView extends NkElement {
|
|
14
16
|
static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'new-row', 'new-row-label', 'sortable', 'sort-key', 'sort-dir', 'wrap']; }
|
|
@@ -76,7 +78,11 @@ class NkTableView extends NkElement {
|
|
|
76
78
|
}
|
|
77
79
|
if (e.target.closest('input[type=checkbox], a')) return;
|
|
78
80
|
const tr = e.target.closest('tr[data-id]');
|
|
79
|
-
if (tr)
|
|
81
|
+
if (!tr) return;
|
|
82
|
+
// Which cell: `key` names its column, `cell` is there to anchor an editor to.
|
|
83
|
+
const cell = e.target.closest('td');
|
|
84
|
+
const row = this._rowById(tr.dataset.id);
|
|
85
|
+
this.emit('nk-select', { row, id: row?.id, value: row?.id, key: cell ? this._columns[cell.cellIndex]?.key : undefined, cell });
|
|
80
86
|
};
|
|
81
87
|
this._onChange = (e) => {
|
|
82
88
|
const input = e.target;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Calendar days for nk-calendar and nk-calendar-view: local days as
|
|
2
|
+
// 'YYYY-MM-DD', no library. Weeks are ISO weeks (Monday-based, the week with
|
|
3
|
+
// the year's first Thursday); the first weekday comes from the locale.
|
|
4
|
+
const pad = n => String(n).padStart(2, '0');
|
|
5
|
+
|
|
6
|
+
const isoOf = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
7
|
+
const addDays = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
|
|
8
|
+
|
|
9
|
+
/** 'YYYY-MM-DD[THH:MM]', 'D.M.YYYY', a Date or { start } β a local Date at midnight, or null. */
|
|
10
|
+
function parseDay(v) {
|
|
11
|
+
if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : new Date(v.getFullYear(), v.getMonth(), v.getDate());
|
|
12
|
+
if (v && typeof v === 'object') return v.start ? parseDay(v.start) : null;
|
|
13
|
+
const s = String(v ?? '').trim();
|
|
14
|
+
let m = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
15
|
+
if (m) return new Date(+m[1], +m[2] - 1, +m[3]);
|
|
16
|
+
m = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
|
17
|
+
return m ? new Date(+m[3], +m[2] - 1, +m[1]) : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isoWeek(d) {
|
|
21
|
+
const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
22
|
+
t.setUTCDate(t.getUTCDate() + 4 - (t.getUTCDay() || 7));
|
|
23
|
+
return Math.ceil(((t - Date.UTC(t.getUTCFullYear(), 0, 1)) / 864e5 + 1) / 7);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The page's language unless the element names one. */
|
|
27
|
+
const resolveLocale = attr => attr || document.documentElement.lang || navigator.language || 'en';
|
|
28
|
+
|
|
29
|
+
/** 0 = Sunday β¦ 6 = Saturday, from Intl.Locale's week info; Monday where the browser cannot say. */
|
|
30
|
+
function firstWeekday(locale) {
|
|
31
|
+
try {
|
|
32
|
+
const l = new Intl.Locale(locale), info = l.getWeekInfo?.() ?? l.weekInfo;
|
|
33
|
+
if (info?.firstDay) return info.firstDay % 7;
|
|
34
|
+
} catch { /* an unknown tag */ }
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Short weekday names from `weekStart` on; `length` cuts them (2 β "Mo"). */
|
|
39
|
+
function weekdayNames(locale, weekStart, length = 0) {
|
|
40
|
+
const f = new Intl.DateTimeFormat(locale, { weekday: 'short' });
|
|
41
|
+
// 7 June 2026 is a Sunday, so 7 + n is weekday n.
|
|
42
|
+
return Array.from({ length: 7 }, (_, i) => {
|
|
43
|
+
const name = f.format(new Date(2026, 5, 7 + ((weekStart + i) % 7))).replace('.', '');
|
|
44
|
+
return length ? name.slice(0, length) : name;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A month from its week start: `first`, `day(i)` for the i-th cell, the `rows` it needs. */
|
|
49
|
+
function monthGrid(month, weekStart) {
|
|
50
|
+
const [y, m] = month.split('-').map(Number), first = new Date(y, m - 1, 1), offset = (first.getDay() - weekStart + 7) % 7;
|
|
51
|
+
return { m, first, day: i => new Date(y, m - 1, 1 - offset + i), rows: Math.ceil((offset + new Date(y, m, 0).getDate()) / 7) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stepMonth(month, n) {
|
|
55
|
+
const [y, m] = month.split('-').map(Number);
|
|
56
|
+
return isoOf(new Date(y, m - 1 + n, 1)).slice(0, 7);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** "KW" for German, "W" elsewhere β the head of the week column. */
|
|
60
|
+
const weekLabel = locale => (/^de\b/i.test(locale) ? 'KW' : 'W');
|
|
61
|
+
|
|
62
|
+
const CHEVRONS = {
|
|
63
|
+
prev: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 6l-6 6 6 6"/></svg>',
|
|
64
|
+
next: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export { CHEVRONS as C, weekdayNames as a, isoWeek as b, addDays as c, firstWeekday as f, isoOf as i, monthGrid as m, parseDay as p, resolveLocale as r, stepMonth as s, weekLabel as w };
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
/** Puts `host` 6px under `anchor`, right edges aligned β left edges with align="start". */
|
|
15
|
+
function placeUnder(host, anchor, align) {
|
|
16
|
+
const r = boxOf(anchor);
|
|
17
|
+
host.style.top = `${r.bottom + 6}px`;
|
|
18
|
+
if (align === 'start') { host.style.left = `${r.left}px`; host.style.right = ''; }
|
|
19
|
+
else { host.style.right = `${Math.max(8, innerWidth - r.right)}px`; host.style.left = ''; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { placeUnder as p };
|