@jungherz-de/notionkit-elements 1.8.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 +12 -12
- package/dist/components/nk-calendar-view.js +138 -0
- package/dist/components/nk-calendar.js +356 -0
- package/dist/components/nk-menu.js +2 -14
- 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 +582 -20
- package/dist/notionkit-elements.js +583 -19
- package/dist/notionkit-elements.min.js +1 -1
- package/package.json +3 -3
- 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/overlays/nk-menu.js +2 -14
- package/src/index.js +2 -0
- package/src/util/dates.js +65 -0
- package/src/util/floating.js +20 -0
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
<h1 align="center">π§© NotionKit Elements</h1>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<a href="https://www.npmjs.com/package/@jungherz-de/notionkit-elements"><img src="https://img.shields.io/badge/version-1.
|
|
4
|
+
<a href="https://www.npmjs.com/package/@jungherz-de/notionkit-elements"><img src="https://img.shields.io/badge/version-1.9.0-2383e2?style=flat-square" alt="Version"></a>
|
|
5
5
|
<a href="#"><img src="https://img.shields.io/badge/vanilla_JS-no_dependencies-448361?style=flat-square" alt="Vanilla JS"></a>
|
|
6
|
-
<a href="#"><img src="https://img.shields.io/badge/elements-
|
|
6
|
+
<a href="#"><img src="https://img.shields.io/badge/elements-85-529cca?style=flat-square" alt="85 Elements"></a>
|
|
7
7
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-9065b0?style=flat-square" alt="MIT License"></a>
|
|
8
|
-
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.
|
|
8
|
+
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.9.0-lightgrey?style=flat-square" alt="Changelog"></a>
|
|
9
9
|
<a href="https://www.npmjs.com/package/@jungherz-de/notionkit-elements"><img src="https://img.shields.io/badge/npm-%40jungherz--de%2Fnotionkit--elements-cb3837?style=flat-square&logo=npm" alt="npm"></a>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
13
|
<strong>Drop-in Web Components for <a href="https://github.com/JUNGHERZ/NotionKit">NotionKit CSS</a></strong><br>
|
|
14
|
-
|
|
14
|
+
85 vanilla JavaScript custom elements wrapping NotionKit's calm, document-centric workspace look.<br>
|
|
15
15
|
Shadow DOM · Native form participation · Pixel parity with the class markup · Zero dependencies.
|
|
16
16
|
</p>
|
|
17
17
|
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
## β¨ What is NotionKit Elements?
|
|
30
30
|
|
|
31
|
-
NotionKit Elements is the companion library to [NotionKit CSS](https://github.com/JUNGHERZ/NotionKit). It provides **
|
|
31
|
+
NotionKit Elements is the companion library to [NotionKit CSS](https://github.com/JUNGHERZ/NotionKit). It provides **85 Web Components** β from buttons, form controls and the date picker over the sidebar, page tree and page shell with its properties to the settings modal, the bottom sheet, the side peek, βK palette, database views β a calendar among them β with Notionβs toolbar and AI thread β that encapsulate the class markup of the foundation into simple, declarative custom elements.
|
|
32
32
|
|
|
33
33
|
It is the **app layer** of the NotionKit family β three layers, one design language: [NotionKit](https://notionkit.jungherz.com) is the pure-CSS foundation, NotionKit Elements wraps it into web components for application UIs, and [NotionKit Web](https://notionkit-web.jungherz.com) is the Astro template for complete websites on top of the same foundation. The word stem stays the same across the layers: `.nk-callout` becomes `<nk-callout>`, a modifier class becomes an attribute, a state class becomes a boolean attribute.
|
|
34
34
|
|
|
@@ -56,10 +56,10 @@ It is the **app layer** of the NotionKit family β three layers, one design lan
|
|
|
56
56
|
|---|---|
|
|
57
57
|
| π― **Pixel parity** | Every element renders identically to its class markup β verified by a Playwright pixel test in light and dark; the reference app differs by 0.00 % from the class version |
|
|
58
58
|
| π **Shadow DOM** | Each root adopts the NotionKit *components* sheet via `adoptedStyleSheets`; tokens are inherited from the document, so one `:root { --nk-accent: β¦ }` re-brands everything |
|
|
59
|
-
| π§© **
|
|
59
|
+
| π§© **85 Elements** | Forms with copy field, image picker and date picker, content blocks with bookmarks, app shell & tree with the β° drawer, page shell with properties, panels, avatars and steps, overlays (modal, sheet, side peek, βK palette, menus that float in and become sheets on a phone, popover, emoji picker, toast), database table, board, list & calendar with toolbar and filter pills, comments, AI thread |
|
|
60
60
|
| ποΈ **Form participation** | Input, textarea, select, switch, check, radio, slider, segmented and model card work natively with `<form>` via `ElementInternals`; radios group across shadow roots |
|
|
61
61
|
| π **Theme sync** | One observer mirrors `data-theme` on `<html>` into every element |
|
|
62
|
-
| πͺΆ **Lightweight** |
|
|
62
|
+
| πͺΆ **Lightweight** | 248 KB minified / 49 KB gzipped (IIFE, foundation sheet included), no external dependencies |
|
|
63
63
|
| π¦ **Four bundle shapes** | IIFE, minified IIFE, ESM, and per-component ESM entries on a stable `base.js`, which import NotionKit's sheet instead of carrying a copy |
|
|
64
64
|
| π€ **AI-ready** | `SKILL.md` with copy-paste markup, six app skeletons and the rules β an agent given only that file built a working app in the release test |
|
|
65
65
|
|
|
@@ -71,8 +71,8 @@ It is the **app layer** of the NotionKit family β three layers, one design lan
|
|
|
71
71
|
<!DOCTYPE html>
|
|
72
72
|
<html lang="en" data-theme="light">
|
|
73
73
|
<head>
|
|
74
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.
|
|
75
|
-
<script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.
|
|
74
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.9.0/notionkit.min.css">
|
|
75
|
+
<script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.9.0/dist/notionkit-elements.min.js"></script>
|
|
76
76
|
</head>
|
|
77
77
|
<body class="nk-body">
|
|
78
78
|
<nk-field label="Display name" desc="Shown next to your comments">
|
|
@@ -102,18 +102,18 @@ The per-component files import NotionKit's sheet as `@jungherz-de/notionkit/noti
|
|
|
102
102
|
|
|
103
103
|
Do not mix the full bundle with the per-component files: each brings its own `NkElement`. With the bundle, take `componentsSheet` and `NkElement` from the bundle (`NotionKitElements.componentsSheet` from the `<script>` build).
|
|
104
104
|
|
|
105
|
-
Two prerequisites, always: `notionkit.css` on the document (the peer dependency, `>= 1.
|
|
105
|
+
Two prerequisites, always: `notionkit.css` on the document (the peer dependency, `>= 1.9.0`; 1.9.0 brings the date picker and the calendar view, 1.8.0 the side peek, the bookmark, the copy field and pictures on the profile row, 1.7.0 the sheet, menus that float in and become sheets on a phone, the sidebar drawer, the database toolbar with filter pills and the steps, 1.7.1 minified files that carry all of it, 1.6.0 page properties, page options, list view, panels, prose and the avatar, 1.5.1 the safe areas, 1.5.0 the Notion-2025 tokens, the nine tag colours and the filled inputs; from 1.5.0 on, Elements and the foundation share one version number β the elements are built and tested against the same release) and `class="nk-body"` on `<body>`. The elements ship no visual CSS of their own β every rule comes from the foundation. The bundle injects the design tokens once as `@layer notionkit-defaults`, so any plain `:root { --nk-accent: β¦ }` of yours re-brands every element in both themes.
|
|
106
106
|
|
|
107
107
|
## Elements
|
|
108
108
|
|
|
109
109
|
| Group | Elements |
|
|
110
110
|
|---|---|
|
|
111
|
-
| Forms & controls | `nk-btn` `nk-input` `nk-textarea` `nk-select` `nk-switch` `nk-check` `nk-radio` `nk-slider` `nk-field` `nk-fields` `nk-copy-field` `nk-image-picker` |
|
|
111
|
+
| Forms & controls | `nk-btn` `nk-input` `nk-textarea` `nk-select` `nk-switch` `nk-check` `nk-radio` `nk-slider` `nk-field` `nk-fields` `nk-copy-field` `nk-image-picker` `nk-calendar` |
|
|
112
112
|
| Content | `nk-tag` `nk-progress` `nk-callout` `nk-bookmark` `nk-divider` `nk-heading` `nk-toggle` `nk-todo` `nk-kbd` `nk-code` `nk-quote` |
|
|
113
113
|
| App shell & navigation | `nk-app` `nk-sidebar` `nk-workspace-switcher` `nk-section-label` `nk-tree` `nk-tree-item` `nk-topbar` `nk-breadcrumb` `nk-theme-toggle` `nk-tab-bar` `nk-tab-bar-item` |
|
|
114
114
|
| Page shell & blocks | `nk-page` `nk-page-cover` `nk-page-title` `nk-page-actions` `nk-props` `nk-prop` `nk-panels` `nk-panel` `nk-avatar` `nk-block-host` `nk-banner` `nk-empty` `nk-skeleton` `nk-synced` `nk-tabs` `nk-tab` `nk-segmented` `nk-steps` `nk-stats` `nk-stat` `nk-avatar-group` `nk-mention` `nk-template-btn` `nk-model-card` `nk-profile-row` `nk-danger-zone` `nk-member-list` `nk-member-row` |
|
|
115
115
|
| Overlays | `nk-modal` `nk-sheet` `nk-peek` `nk-settings-pane` `nk-settings-user` `nk-cmdk` `nk-menu` `nk-menu-item` `nk-pop` `nk-emoji-picker` `nk-toast` |
|
|
116
|
-
| Data & collaboration | `nk-database` `nk-table-view` `nk-board-view` `nk-list-view` `nk-filter-bar` `nk-comments` `nk-comment` `nk-ai-thread` `nk-ai-msg` `nk-ai-input-row` (+ the exported `renderPropertyCell()`) |
|
|
116
|
+
| Data & collaboration | `nk-database` `nk-table-view` `nk-board-view` `nk-list-view` `nk-calendar-view` `nk-filter-bar` `nk-comments` `nk-comment` `nk-ai-thread` `nk-ai-msg` `nk-ai-input-row` (+ the exported `renderPropertyCell()`) |
|
|
117
117
|
|
|
118
118
|
Static content goes in through attributes and slots, dynamic data through properties (`tree.data`, `database.rows`, `palette.commands`). Interactions fire custom events with fixed names: `nk-select`, `nk-change`, `nk-view-change`, `nk-command`, `nk-toggle`, `nk-submit`, `nk-action`. No fetching, no two-way binding β the elements render what they get.
|
|
119
119
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { NkElement } from './base.js';
|
|
2
|
+
import { C as CHEVRONS, i as isoOf, r as resolveLocale, f as firstWeekday, m as monthGrid, p as parseDay, w as weekLabel, a as weekdayNames, b as isoWeek, s as stepMonth } from './shared/dates-a32DcW1l.js';
|
|
3
|
+
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
4
|
+
|
|
5
|
+
// <nk-calendar-view name="calendar" label="π
Calendar" date-key="due" weeks></nk-calendar-view>
|
|
6
|
+
// The fourth database view (NotionKit 1.9.0): a month, the rows as cards on
|
|
7
|
+
// their dates. `date-key` names the date column (default: the first one),
|
|
8
|
+
// `title-key` the title (default: the title column); dates are YYYY-MM-DD or
|
|
9
|
+
// D.M.YYYY, a range { start, end } stands on its start. month (YYYY-MM;
|
|
10
|
+
// default today's), weeks (the ISO week in front of each row), week-start
|
|
11
|
+
// (0 = Sunday β¦ 6 = Saturday; default from the locale), weekend (weekdays
|
|
12
|
+
// washed like the days of other months, "6,0"), today, locale and the texts
|
|
13
|
+
// today-label, prev-label, next-label, week-label.
|
|
14
|
+
// Standalone: view.columns = [β¦]; view.rows = [β¦]. Inside <nk-database> the
|
|
15
|
+
// database pushes the data. A card fires nk-select { row, id, value } like a
|
|
16
|
+
// row of the table, Today and βΉ βΊ nk-month { month }.
|
|
17
|
+
// β <div class="nk-calendar-view weeks"><div class="cv-head"><div class="cv-title">May 2026</div><button class="cal-nav">Today</button>βΉ βΊ</div>
|
|
18
|
+
// <div class="cv-grid"><div class="cv-wd">W</div>β¦<div class="cv-week">21</div><div class="cv-day today"><span class="cv-num">20</span><button class="cv-item">ποΈ β¦</button></div>β¦</div></div>
|
|
19
|
+
class NkCalendarView extends NkElement {
|
|
20
|
+
static get observedAttributes() {
|
|
21
|
+
return ['name', 'label', 'badge', 'count', 'date-key', 'title-key', 'month', 'weeks', 'week-start', 'weekend', 'today', 'locale',
|
|
22
|
+
'today-label', 'prev-label', 'next-label', 'week-label'];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
render() {
|
|
26
|
+
this._box = this.createElement('div', ['nk-calendar-view']);
|
|
27
|
+
const head = this.createElement('div', ['cv-head']);
|
|
28
|
+
this._title = this.createElement('div', ['cv-title']);
|
|
29
|
+
this._todayBtn = this.createElement('button', ['cal-nav'], { type: 'button' });
|
|
30
|
+
this._prev = this.createElement('button', ['cal-nav'], { type: 'button' });
|
|
31
|
+
this._prev.innerHTML = CHEVRONS.prev;
|
|
32
|
+
this._next = this.createElement('button', ['cal-nav'], { type: 'button' });
|
|
33
|
+
this._next.innerHTML = CHEVRONS.next;
|
|
34
|
+
head.append(this._title, this._todayBtn, this._prev, this._next);
|
|
35
|
+
this._grid = this.createElement('div', ['cv-grid']);
|
|
36
|
+
this._box.append(head, this._grid);
|
|
37
|
+
this._wrapper.appendChild(this._box);
|
|
38
|
+
this._columns ??= []; this._rows ??= [];
|
|
39
|
+
this._month ??= this._attrMonth() ?? this._todayIso().slice(0, 7);
|
|
40
|
+
this._render();
|
|
41
|
+
this.closest('nk-database')?.requestSync?.();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_attrMonth() { const m = this.getAttribute('month'); return /^\d{4}-\d{2}$/.test(m || '') ? m : null; }
|
|
45
|
+
_todayIso() { return this.getAttribute('today') || isoOf(new Date()); }
|
|
46
|
+
_dateColumn() {
|
|
47
|
+
const key = this.getAttribute('date-key');
|
|
48
|
+
return this._columns.find(c => c.key === key) || this._columns.find(c => c.type === 'date') || null;
|
|
49
|
+
}
|
|
50
|
+
_titleColumn() {
|
|
51
|
+
const key = this.getAttribute('title-key');
|
|
52
|
+
return this._columns.find(c => c.key === key) || this._columns.find(c => c.title) || this._columns[0] || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_render() {
|
|
56
|
+
if (!this._grid) return;
|
|
57
|
+
const locale = resolveLocale(this.getAttribute('locale')), weeks = this.getBoolAttr('weeks');
|
|
58
|
+
const n = Number(this.getAttribute('week-start'));
|
|
59
|
+
const ws = this.hasAttribute('week-start') && Number.isInteger(n) && n >= 0 && n <= 6 ? n : firstWeekday(locale);
|
|
60
|
+
const g = monthGrid(this._month, ws), today = this._todayIso();
|
|
61
|
+
const weekend = new Set((this.getAttribute('weekend') || '').split(',').map(s => s.trim()).filter(Boolean).map(Number));
|
|
62
|
+
const dateCol = this._dateColumn(), titleCol = this._titleColumn(), byDay = {};
|
|
63
|
+
if (dateCol) for (const row of this._rows) {
|
|
64
|
+
const d = parseDay(row[dateCol.key]);
|
|
65
|
+
if (d) (byDay[isoOf(d)] ||= []).push(row);
|
|
66
|
+
}
|
|
67
|
+
this._box.classList.toggle('weeks', weeks);
|
|
68
|
+
this._title.textContent = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(g.first);
|
|
69
|
+
this._todayBtn.textContent = this.getAttribute('today-label') || 'Today';
|
|
70
|
+
this._prev.setAttribute('aria-label', this.getAttribute('prev-label') || 'Previous month');
|
|
71
|
+
this._next.setAttribute('aria-label', this.getAttribute('next-label') || 'Next month');
|
|
72
|
+
|
|
73
|
+
const cell = (cls, text) => { const el = this.createElement('div', [cls]); el.textContent = text; return el; };
|
|
74
|
+
const nodes = [];
|
|
75
|
+
if (weeks) nodes.push(cell('cv-wd', this.getAttribute('week-label') || weekLabel(locale)));
|
|
76
|
+
for (const w of weekdayNames(locale, ws)) nodes.push(cell('cv-wd', w));
|
|
77
|
+
for (let i = 0; i < g.rows * 7; i++) {
|
|
78
|
+
const d = g.day(i), iso = isoOf(d);
|
|
79
|
+
if (weeks && i % 7 === 0) nodes.push(cell('cv-week', isoWeek(d)));
|
|
80
|
+
const day = this.createElement('div', ['cv-day', d.getMonth() !== g.m - 1 && 'out', weekend.has(d.getDay()) && 'off', iso === today && 'today'].filter(Boolean));
|
|
81
|
+
const num = this.createElement('span', ['cv-num']);
|
|
82
|
+
num.textContent = d.getDate();
|
|
83
|
+
day.appendChild(num);
|
|
84
|
+
for (const row of byDay[iso] || []) {
|
|
85
|
+
const item = this.createElement('button', ['cv-item'], { type: 'button', 'data-id': row.id ?? '' });
|
|
86
|
+
const title = titleCol ? String(row[titleCol.key] ?? '') : '';
|
|
87
|
+
item.textContent = row.icon ? `${row.icon} ${title}` : title;
|
|
88
|
+
day.appendChild(item);
|
|
89
|
+
}
|
|
90
|
+
nodes.push(day);
|
|
91
|
+
}
|
|
92
|
+
this._grid.replaceChildren(...nodes);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_goMonth(month) {
|
|
96
|
+
if (month === this._month) return;
|
|
97
|
+
this._month = month;
|
|
98
|
+
this._render();
|
|
99
|
+
this.emit('nk-month', { month });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setupEvents() {
|
|
103
|
+
this._onClick = (e) => {
|
|
104
|
+
if (e.target.closest('.cal-nav') === this._todayBtn) { this._goMonth(this._todayIso().slice(0, 7)); return; }
|
|
105
|
+
if (e.target.closest('.cal-nav') === this._prev) { this._goMonth(stepMonth(this._month, -1)); return; }
|
|
106
|
+
if (e.target.closest('.cal-nav') === this._next) { this._goMonth(stepMonth(this._month, 1)); return; }
|
|
107
|
+
const item = e.target.closest('.cv-item');
|
|
108
|
+
if (item) { const row = this._rowById(item.dataset.id); this.emit('nk-select', { row, id: row?.id, value: row?.id }); }
|
|
109
|
+
};
|
|
110
|
+
this._box.addEventListener('click', this._onClick);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
teardownEvents() { this._box?.removeEventListener('click', this._onClick); }
|
|
114
|
+
|
|
115
|
+
_rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
|
|
116
|
+
|
|
117
|
+
onAttributeChanged(name) {
|
|
118
|
+
if (name === 'month') this._month = this._attrMonth() ?? this._month;
|
|
119
|
+
this._render();
|
|
120
|
+
if (name === 'label' || name === 'badge' || name === 'count') this.closest('nk-database')?.requestSync?.();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get name() { return this.getAttribute('name') || 'calendar'; }
|
|
124
|
+
/** The month shown, YYYY-MM. */
|
|
125
|
+
get month() { return this._month; }
|
|
126
|
+
set month(v) { if (!/^\d{4}-\d{2}$/.test(v)) return; if (this._grid) this._goMonth(v); else this._month = v; }
|
|
127
|
+
get columns() { return this._columns; }
|
|
128
|
+
set columns(v) { this._columns = Array.isArray(v) ? v : []; this._render(); }
|
|
129
|
+
get rows() { return this._rows; }
|
|
130
|
+
set rows(v) { this._rows = Array.isArray(v) ? v : []; this._render(); }
|
|
131
|
+
set data({ columns, rows }) { this.setData(columns, rows); }
|
|
132
|
+
setData(columns, rows) { this._columns = columns || []; this._rows = rows || []; this._render(); }
|
|
133
|
+
refresh() { this._render(); }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
customElements.define('nk-calendar-view', NkCalendarView);
|
|
137
|
+
|
|
138
|
+
export { NkCalendarView };
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { NkFormElement } from './base.js';
|
|
2
|
+
import { p as parseDay, i as isoOf, C as CHEVRONS, r as resolveLocale, f as firstWeekday, m as monthGrid, w as weekLabel, a as weekdayNames, b as isoWeek, s as stepMonth, c as addDays } from './shared/dates-a32DcW1l.js';
|
|
3
|
+
import { p as placeUnder } from './shared/floating-B8CwbXnV.js';
|
|
4
|
+
import { d as deepActiveElement } from './shared/focus-BNAChOXO.js';
|
|
5
|
+
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
6
|
+
|
|
7
|
+
// <nk-calendar name="due" value="2026-06-02" weeks weekend="6,0"></nk-calendar>
|
|
8
|
+
// <nk-calendar range value="2026-06-08/2026-06-12"></nk-calendar>
|
|
9
|
+
// <nk-calendar floating sheet id="picker"></nk-calendar> picker.show(button)
|
|
10
|
+
// β [<div class="nk-pop floating sheet open">]<div class="nk-calendar weeks">
|
|
11
|
+
// <div class="cal-head"><div class="cal-title">June 2026</div><button class="cal-nav">Today</button>βΉ βΊ</div>
|
|
12
|
+
// <div class="cal-grid"><span class="cal-wd">W</span>β¦<span class="cal-week">23</span><button class="cal-day selected">2</button>β¦</div>
|
|
13
|
+
// [<div class="cal-foot"><input class="nk-input" type="time"></div>]</div>
|
|
14
|
+
//
|
|
15
|
+
// Notion's date picker (NotionKit 1.9.0), after GlassKit Elements'
|
|
16
|
+
// glk-calendar, with what a planning tool needs on top: a range, ISO
|
|
17
|
+
// calendar weeks, days not worked, marks and a time.
|
|
18
|
+
// value: YYYY-MM-DD; with `range` the ISO interval "start/end"; with `time`
|
|
19
|
+
// YYYY-MM-DDTHH:MM. Also as the properties value, start, end.
|
|
20
|
+
// month (YYYY-MM shown; default the value's, else today's), min, max, today,
|
|
21
|
+
// locale (default the page's lang), week-start (0 = Sunday β¦ 6 = Saturday;
|
|
22
|
+
// default from the locale, Monday where the browser cannot say), weeks,
|
|
23
|
+
// weekend (weekdays not worked, "6,0" for Saturday and Sunday), range, time,
|
|
24
|
+
// clearable, label and the texts today-label, prev-label, next-label,
|
|
25
|
+
// week-label, time-label, clear-label. `time` belongs to single days; a
|
|
26
|
+
// range has none.
|
|
27
|
+
// days (property, or JSON attribute): { 'YYYY-MM-DD': { off, label, marks } }
|
|
28
|
+
// β `off` greys a holiday, `label` names the day (tooltip and screen
|
|
29
|
+
// reader), `marks` puts up to three dots in the nine colours on it.
|
|
30
|
+
// Keys: arrows by a day or a week, Home/End to the ends of the week,
|
|
31
|
+
// PageUp/PageDown by a month (Shift: a year); Enter or Space picks. Arrows
|
|
32
|
+
// only move, so the picker stays quiet while someone looks around. Days
|
|
33
|
+
// outside min/max are aria-disabled β in the arrow path and announced, never
|
|
34
|
+
// picked. A range takes two picks, in either order. Form-associated: the
|
|
35
|
+
// value goes with the form, reset restores it.
|
|
36
|
+
// Events: nk-change { value, start, end, time } when a pick β for a range the
|
|
37
|
+
// second one β, a new time or Clear changes the value; nk-month { month } when the month
|
|
38
|
+
// changes; nk-toggle { open } with `floating`. `required` makes an empty
|
|
39
|
+
// value invalid.
|
|
40
|
+
// `floating` makes it a popover over the page β show(anchor) puts it under
|
|
41
|
+
// the anchor, right edges aligned (align="start": left edges) β and `sheet`
|
|
42
|
+
// a bottom sheet with 44px cells on a phone, as <nk-menu floating sheet>.
|
|
43
|
+
// A pick closes it β the second of a range; with `time` Escape or a tap
|
|
44
|
+
// outside does. Put a floating calendar directly under <body>.
|
|
45
|
+
const STEPS = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };
|
|
46
|
+
const floatSheet = new CSSStyleSheet();
|
|
47
|
+
floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; z-index: 60; pointer-events: none; }`);
|
|
48
|
+
|
|
49
|
+
class NkCalendar extends NkFormElement {
|
|
50
|
+
static get hostStyles() { return floatSheet; }
|
|
51
|
+
static get observedAttributes() {
|
|
52
|
+
return ['value', 'month', 'min', 'max', 'today', 'locale', 'week-start', 'weeks', 'weekend', 'range', 'time', 'clearable', 'days',
|
|
53
|
+
'label', 'today-label', 'prev-label', 'next-label', 'week-label', 'time-label', 'clear-label', 'floating', 'sheet', 'open', 'align', 'required'];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
render() {
|
|
57
|
+
// A value or month set as a property before the first connect wins over the attributes.
|
|
58
|
+
this._readValue(this._pendingValue ?? this.getAttribute('value'));
|
|
59
|
+
const month = this._pendingMonth || this._attrMonth();
|
|
60
|
+
if (month) this._month = month;
|
|
61
|
+
this._initialValue = this.getAttribute('value') ?? '';
|
|
62
|
+
this._build();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ββ value ββ
|
|
66
|
+
_readValue(v) {
|
|
67
|
+
const s = String(v ?? '');
|
|
68
|
+
this._start = this._end = this._day = null;
|
|
69
|
+
this._time = '';
|
|
70
|
+
if (this.getBoolAttr('range')) {
|
|
71
|
+
const [a, b] = s.split('/');
|
|
72
|
+
this._start = parseDay(a) ? isoOf(parseDay(a)) : null;
|
|
73
|
+
this._end = parseDay(b) ? isoOf(parseDay(b)) : null;
|
|
74
|
+
} else if (parseDay(s)) {
|
|
75
|
+
this._day = isoOf(parseDay(s));
|
|
76
|
+
this._time = s.match(/T(\d{2}:\d{2})/)?.[1] ?? '';
|
|
77
|
+
}
|
|
78
|
+
// A new value brings its month; without one the month stays.
|
|
79
|
+
const shown = this._day || this._start;
|
|
80
|
+
this._month = shown ? shown.slice(0, 7) : (this._month || this._attrMonth() || this._todayIso().slice(0, 7));
|
|
81
|
+
this._focus = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_attrMonth() { const m = this.getAttribute('month'); return /^\d{4}-\d{2}$/.test(m || '') ? m : null; }
|
|
85
|
+
_todayIso() { return this.getAttribute('today') || isoOf(new Date()); }
|
|
86
|
+
|
|
87
|
+
get value() {
|
|
88
|
+
if (this.getBoolAttr('range')) return this._start ? `${this._start}${this._end ? `/${this._end}` : ''}` : '';
|
|
89
|
+
return this._day ? `${this._day}${this.getBoolAttr('time') && this._time ? `T${this._time}` : ''}` : '';
|
|
90
|
+
}
|
|
91
|
+
set value(v) {
|
|
92
|
+
this._readValue(v);
|
|
93
|
+
if (this._grid) this._draw(); else this._pendingValue = String(v ?? '');
|
|
94
|
+
}
|
|
95
|
+
get start() { return this.getBoolAttr('range') ? this._start : this._day; }
|
|
96
|
+
get end() { return this.getBoolAttr('range') ? this._end : this._day; }
|
|
97
|
+
get month() { return this._month; }
|
|
98
|
+
set month(v) {
|
|
99
|
+
if (!/^\d{4}-\d{2}$/.test(v)) return;
|
|
100
|
+
this._month = v; this._focus = null;
|
|
101
|
+
if (this._grid) this._draw(); else this._pendingMonth = v;
|
|
102
|
+
}
|
|
103
|
+
get days() { return this._days ?? {}; }
|
|
104
|
+
set days(v) { this._days = v && typeof v === 'object' ? v : {}; if (this._grid) this._draw(); }
|
|
105
|
+
|
|
106
|
+
// ββ markup ββ
|
|
107
|
+
_build() {
|
|
108
|
+
const floating = this.getBoolAttr('floating');
|
|
109
|
+
this._cal = this.createElement('div', ['nk-calendar'], { role: 'group' });
|
|
110
|
+
const head = this.createElement('div', ['cal-head']);
|
|
111
|
+
this._title = this.createElement('div', ['cal-title'], { 'aria-live': 'polite' });
|
|
112
|
+
this._todayBtn = this.createElement('button', ['cal-nav'], { type: 'button' });
|
|
113
|
+
this._prev = this.createElement('button', ['cal-nav'], { type: 'button' });
|
|
114
|
+
this._prev.innerHTML = CHEVRONS.prev;
|
|
115
|
+
this._next = this.createElement('button', ['cal-nav'], { type: 'button' });
|
|
116
|
+
this._next.innerHTML = CHEVRONS.next;
|
|
117
|
+
head.append(this._title, this._todayBtn, this._prev, this._next);
|
|
118
|
+
this._grid = this.createElement('div', ['cal-grid']);
|
|
119
|
+
this._foot = this.createElement('div', ['cal-foot']);
|
|
120
|
+
this._timeInput = this.createElement('input', ['nk-input'], { type: 'time' });
|
|
121
|
+
this._clear = this.createElement('button', ['cal-nav'], { type: 'button' });
|
|
122
|
+
this._cal.append(head, this._grid);
|
|
123
|
+
this._pop = floating ? this.createElement('div', ['nk-pop', 'floating']) : null;
|
|
124
|
+
if (this._pop) this._pop.appendChild(this._cal);
|
|
125
|
+
this._wrapper.replaceChildren(this._pop ?? this._cal);
|
|
126
|
+
this._draw();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
get _hasTime() { return this.getBoolAttr('time') && !this.getBoolAttr('range'); }
|
|
130
|
+
get _locale() { return resolveLocale(this.getAttribute('locale')); }
|
|
131
|
+
get _weekStart() {
|
|
132
|
+
const n = Number(this.getAttribute('week-start'));
|
|
133
|
+
return this.hasAttribute('week-start') && Number.isInteger(n) && n >= 0 && n <= 6 ? n : firstWeekday(this._locale);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
_syncPop() {
|
|
137
|
+
this._pop?.classList.toggle('sheet', this.getBoolAttr('sheet'));
|
|
138
|
+
this._pop?.classList.toggle('open', this.getBoolAttr('open'));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_draw() {
|
|
142
|
+
const locale = this._locale, ws = this._weekStart, weeks = this.getBoolAttr('weeks'), range = this.getBoolAttr('range');
|
|
143
|
+
const g = monthGrid(this._month, ws), today = this._todayIso();
|
|
144
|
+
const min = this.getAttribute('min') || '', max = this.getAttribute('max') || '';
|
|
145
|
+
const weekend = new Set((this.getAttribute('weekend') || '').split(',').map(s => s.trim()).filter(Boolean).map(Number));
|
|
146
|
+
let days = this._days;
|
|
147
|
+
if (!days && this.hasAttribute('days')) { try { days = JSON.parse(this.getAttribute('days')); } catch { days = {}; } }
|
|
148
|
+
days ??= {};
|
|
149
|
+
const long = new Intl.DateTimeFormat(locale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
|
|
150
|
+
this._cal.classList.toggle('weeks', weeks);
|
|
151
|
+
this._syncPop();
|
|
152
|
+
const label = this.getAttribute('label');
|
|
153
|
+
if (label) this._cal.setAttribute('aria-label', label); else this._cal.removeAttribute('aria-label');
|
|
154
|
+
this._title.textContent = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(g.first);
|
|
155
|
+
this._todayBtn.textContent = this.getAttribute('today-label') || 'Today';
|
|
156
|
+
this._prev.setAttribute('aria-label', this.getAttribute('prev-label') || 'Previous month');
|
|
157
|
+
this._next.setAttribute('aria-label', this.getAttribute('next-label') || 'Next month');
|
|
158
|
+
|
|
159
|
+
// The one tab stop: the focused day, else the chosen one, else the 1st.
|
|
160
|
+
const cells = Array.from({ length: 42 }, (_, i) => isoOf(g.day(i)));
|
|
161
|
+
const chosen = range ? this._start : this._day;
|
|
162
|
+
if (!this._focus || !cells.includes(this._focus)) this._focus = cells.includes(chosen) ? chosen : `${this._month}-01`;
|
|
163
|
+
|
|
164
|
+
const nodes = [];
|
|
165
|
+
if (weeks) nodes.push(this._head(this.getAttribute('week-label') || weekLabel(locale)));
|
|
166
|
+
for (const w of weekdayNames(locale, ws, 2)) nodes.push(this._head(w));
|
|
167
|
+
for (let i = 0; i < 42; i++) {
|
|
168
|
+
const d = g.day(i), iso = cells[i], info = days[iso] || {};
|
|
169
|
+
if (weeks && i % 7 === 0) { const wk = this.createElement('span', ['cal-week'], { 'aria-hidden': 'true' }); wk.textContent = isoWeek(d); nodes.push(wk); }
|
|
170
|
+
const inRange = range && this._start && this._end && iso > this._start && iso < this._end;
|
|
171
|
+
const cls = ['cal-day', d.getMonth() !== g.m - 1 && 'out', (weekend.has(d.getDay()) || info.off) && 'off', iso === today && 'today',
|
|
172
|
+
!range && iso === this._day && 'selected', range && iso === this._start && 'start', range && iso === (this._end || this._start) && this._start && 'end', inRange && 'in-range'].filter(Boolean);
|
|
173
|
+
const day = this.createElement('button', cls, { type: 'button', 'data-date': iso, tabindex: iso === this._focus ? '0' : '-1',
|
|
174
|
+
'aria-pressed': String(cls.includes('selected') || cls.includes('start') || cls.includes('end')),
|
|
175
|
+
'aria-label': `${long.format(d)}${info.label ? `, ${info.label}` : ''}` });
|
|
176
|
+
if (info.label) day.title = info.label;
|
|
177
|
+
if ((min && iso < min) || (max && iso > max)) day.setAttribute('aria-disabled', 'true');
|
|
178
|
+
day.textContent = d.getDate();
|
|
179
|
+
const marks = (info.marks || []).slice(0, 3);
|
|
180
|
+
if (marks.length) {
|
|
181
|
+
const m = this.createElement('span', ['cal-marks']);
|
|
182
|
+
for (const tone of marks) m.appendChild(this.createElement('i', [tone]));
|
|
183
|
+
day.appendChild(m);
|
|
184
|
+
}
|
|
185
|
+
nodes.push(day);
|
|
186
|
+
}
|
|
187
|
+
// The days are drawn anew; focus inside the grid moves to the new tab stop.
|
|
188
|
+
const focused = this._grid.contains(this._shadow.activeElement);
|
|
189
|
+
this._grid.replaceChildren(...nodes);
|
|
190
|
+
if (focused) this.focusDay();
|
|
191
|
+
|
|
192
|
+
const time = this._hasTime, clearable = this.getBoolAttr('clearable');
|
|
193
|
+
this._timeInput.value = this._time;
|
|
194
|
+
this._timeInput.setAttribute('aria-label', this.getAttribute('time-label') || 'Time');
|
|
195
|
+
this._clear.textContent = this.getAttribute('clear-label') || 'Clear';
|
|
196
|
+
this._foot.replaceChildren(...[time && this._timeInput, clearable && this._clear].filter(Boolean));
|
|
197
|
+
if (time || clearable) this._cal.appendChild(this._foot); else this._foot.remove();
|
|
198
|
+
this._syncForm();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
_syncForm() {
|
|
202
|
+
this.setFormValue(this.value);
|
|
203
|
+
if (this.getBoolAttr('required') && !this.value) this.setValidity({ valueMissing: true }, 'Please choose a date.', this._grid?.querySelector('[tabindex="0"]') ?? undefined);
|
|
204
|
+
else this.setValidity({});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
_head(text) {
|
|
208
|
+
const s = this.createElement('span', ['cal-wd'], { 'aria-hidden': 'true' });
|
|
209
|
+
s.textContent = text;
|
|
210
|
+
return s;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ββ interaction ββ
|
|
214
|
+
_goMonth(month, focus) {
|
|
215
|
+
if (month !== this._month) { this._month = month; this.emit('nk-month', { month }); }
|
|
216
|
+
this._focus = focus ?? null;
|
|
217
|
+
this._draw();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
_pick(iso) {
|
|
221
|
+
const day = this._grid.querySelector(`[data-date="${iso}"]`);
|
|
222
|
+
if (day?.getAttribute('aria-disabled') === 'true') return;
|
|
223
|
+
const before = this.value;
|
|
224
|
+
let done = true;
|
|
225
|
+
if (this.getBoolAttr('range')) {
|
|
226
|
+
if (!this._start || this._end) { this._start = iso; this._end = null; done = false; }
|
|
227
|
+
else if (iso < this._start) { this._end = this._start; this._start = iso; }
|
|
228
|
+
else this._end = iso;
|
|
229
|
+
} else this._day = iso;
|
|
230
|
+
this._focus = iso;
|
|
231
|
+
if (iso.slice(0, 7) !== this._month) this._goMonth(iso.slice(0, 7), iso); else this._draw();
|
|
232
|
+
if (!done) return;
|
|
233
|
+
// Only a new value is a change; the same day again just closes a popover.
|
|
234
|
+
if (this.value !== before) this.emit('nk-change', { value: this.value, start: this.start, end: this.end, time: this._time || null });
|
|
235
|
+
if (this.getBoolAttr('floating') && !this._hasTime) this.close();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
setupEvents() {
|
|
239
|
+
this._onGrid = (e) => { const day = e.target.closest('.cal-day'); if (day) this._pick(day.dataset.date); };
|
|
240
|
+
this._onToday = () => { const t = this._todayIso(); this._goMonth(t.slice(0, 7), t); };
|
|
241
|
+
this._onPrev = () => this._goMonth(stepMonth(this._month, -1));
|
|
242
|
+
this._onNext = () => this._goMonth(stepMonth(this._month, 1));
|
|
243
|
+
this._onKey = (e) => {
|
|
244
|
+
// Enter and Space are the day button's own click.
|
|
245
|
+
const day = e.target.closest?.('.cal-day');
|
|
246
|
+
if (!day) return;
|
|
247
|
+
const date = parseDay(day.dataset.date), ws = this._weekStart, wd = (date.getDay() - ws + 7) % 7;
|
|
248
|
+
let target = null;
|
|
249
|
+
if (STEPS[e.key]) target = addDays(date, STEPS[e.key]);
|
|
250
|
+
else if (e.key === 'Home') target = addDays(date, -wd);
|
|
251
|
+
else if (e.key === 'End') target = addDays(date, 6 - wd);
|
|
252
|
+
else if (e.key === 'PageUp' || e.key === 'PageDown') {
|
|
253
|
+
const months = (e.key === 'PageUp' ? -1 : 1) * (e.shiftKey ? 12 : 1);
|
|
254
|
+
const last = new Date(date.getFullYear(), date.getMonth() + months + 1, 0).getDate();
|
|
255
|
+
target = new Date(date.getFullYear(), date.getMonth() + months, Math.min(date.getDate(), last));
|
|
256
|
+
}
|
|
257
|
+
if (!target) return;
|
|
258
|
+
e.preventDefault();
|
|
259
|
+
const iso = isoOf(target);
|
|
260
|
+
if (iso.slice(0, 7) !== this._month) this._goMonth(iso.slice(0, 7), iso);
|
|
261
|
+
else { this._focus = iso; this._draw(); }
|
|
262
|
+
};
|
|
263
|
+
this._onTime = () => {
|
|
264
|
+
this._time = this._timeInput.value;
|
|
265
|
+
this._syncForm();
|
|
266
|
+
if (this._day) this.emit('nk-change', { value: this.value, start: this.start, end: this.end, time: this._time || null });
|
|
267
|
+
};
|
|
268
|
+
this._onClear = () => {
|
|
269
|
+
if (!this.value) return;
|
|
270
|
+
this._day = this._start = this._end = null; this._time = '';
|
|
271
|
+
this._draw();
|
|
272
|
+
this.emit('nk-change', { value: '', start: null, end: null, time: null });
|
|
273
|
+
};
|
|
274
|
+
// Floating: a tap outside closes it and reaches nothing else; Escape closes (captured, before a modal around it).
|
|
275
|
+
this._onOutside = (e) => {
|
|
276
|
+
const path = e.composedPath();
|
|
277
|
+
if (path.includes(this) || (this._anchor && path.includes(this._anchor))) return;
|
|
278
|
+
e.preventDefault(); e.stopPropagation();
|
|
279
|
+
this.close();
|
|
280
|
+
};
|
|
281
|
+
this._onEscape = (e) => { if (e.key === 'Escape' && this.getBoolAttr('floating') && this.getBoolAttr('open')) { e.stopPropagation(); this.close(); } };
|
|
282
|
+
this._grid.addEventListener('click', this._onGrid);
|
|
283
|
+
this._grid.addEventListener('keydown', this._onKey);
|
|
284
|
+
this._todayBtn.addEventListener('click', this._onToday);
|
|
285
|
+
this._prev.addEventListener('click', this._onPrev);
|
|
286
|
+
this._next.addEventListener('click', this._onNext);
|
|
287
|
+
this._timeInput.addEventListener('change', this._onTime);
|
|
288
|
+
this._clear.addEventListener('click', this._onClear);
|
|
289
|
+
document.addEventListener('keydown', this._onEscape, true);
|
|
290
|
+
if (this.getBoolAttr('open')) document.addEventListener('click', this._onOutside, true);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
teardownEvents() {
|
|
294
|
+
this._grid?.removeEventListener('click', this._onGrid);
|
|
295
|
+
this._grid?.removeEventListener('keydown', this._onKey);
|
|
296
|
+
this._todayBtn?.removeEventListener('click', this._onToday);
|
|
297
|
+
this._prev?.removeEventListener('click', this._onPrev);
|
|
298
|
+
this._next?.removeEventListener('click', this._onNext);
|
|
299
|
+
this._timeInput?.removeEventListener('change', this._onTime);
|
|
300
|
+
this._clear?.removeEventListener('click', this._onClear);
|
|
301
|
+
document.removeEventListener('keydown', this._onEscape, true);
|
|
302
|
+
document.removeEventListener('click', this._onOutside, true);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
onAttributeChanged(name, _old, value) {
|
|
306
|
+
if (name === 'value') { this._readValue(value); this._draw(); return; }
|
|
307
|
+
if (name === 'month') { this.month = value; return; }
|
|
308
|
+
if (name === 'open' || name === 'sheet') { this._syncPop(); if (name === 'open') this._syncOpen(); return; }
|
|
309
|
+
if (name === 'align') return;
|
|
310
|
+
if (name === 'floating') { this.teardownEvents(); this._build(); if (this.isConnected) this.setupEvents(); return; }
|
|
311
|
+
if (name === 'range') this._readValue(this.getAttribute('value'));
|
|
312
|
+
if (name === 'days') this._days = null;
|
|
313
|
+
this._draw();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
_syncOpen() {
|
|
317
|
+
const open = this.getBoolAttr('open');
|
|
318
|
+
if (this._anchor) (this._anchor.shadowRoot?.querySelector('button, a[href]') ?? this._anchor).setAttribute('aria-expanded', String(open));
|
|
319
|
+
if (open) {
|
|
320
|
+
this._returnFocus = deepActiveElement();
|
|
321
|
+
if (this._returnFocus?.matches(':focus-visible')) requestAnimationFrame(() => this.focusDay());
|
|
322
|
+
document.addEventListener('click', this._onOutside, true);
|
|
323
|
+
} else {
|
|
324
|
+
document.removeEventListener('click', this._onOutside, true);
|
|
325
|
+
const back = this._returnFocus;
|
|
326
|
+
this._returnFocus = null;
|
|
327
|
+
const active = deepActiveElement();
|
|
328
|
+
if (back?.isConnected && active && this._shadow.contains(active)) back.focus({ preventScroll: true });
|
|
329
|
+
}
|
|
330
|
+
this.emit('nk-toggle', { open });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ββ form ββ
|
|
334
|
+
resetValue() { this._readValue(this._initialValue); this._draw(); }
|
|
335
|
+
restoreValue(state) { this._readValue(state); this._draw(); }
|
|
336
|
+
onFormDisabled(disabled) { this._cal?.toggleAttribute('inert', disabled); }
|
|
337
|
+
|
|
338
|
+
// ββ API ββ
|
|
339
|
+
/** Opens under `anchor` (with `floating`), on the month of the value. */
|
|
340
|
+
show(anchor) {
|
|
341
|
+
if (anchor) { this._anchor = anchor; placeUnder(this, anchor, this.getAttribute('align')); }
|
|
342
|
+
const shown = this.start;
|
|
343
|
+
if (shown && shown.slice(0, 7) !== this._month) { this._month = shown.slice(0, 7); this._focus = null; this._draw(); }
|
|
344
|
+
this.setBoolAttr('open', true);
|
|
345
|
+
}
|
|
346
|
+
close() { this.setBoolAttr('open', false); }
|
|
347
|
+
toggle(anchor) { this.getBoolAttr('open') ? this.close() : this.show(anchor); }
|
|
348
|
+
get open() { return this.getBoolAttr('open'); }
|
|
349
|
+
set open(v) { this.setBoolAttr('open', v); }
|
|
350
|
+
/** Moves focus to the day with the tab stop. */
|
|
351
|
+
focusDay() { this._grid?.querySelector('[tabindex="0"]')?.focus(); }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
customElements.define('nk-calendar', NkCalendar);
|
|
355
|
+
|
|
356
|
+
export { NkCalendar };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NkElement } from './base.js';
|
|
2
2
|
import { d as deepActiveElement } from './shared/focus-BNAChOXO.js';
|
|
3
|
+
import { p as placeUnder } from './shared/floating-B8CwbXnV.js';
|
|
3
4
|
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
4
5
|
|
|
5
6
|
// <nk-menu>
|
|
@@ -21,16 +22,6 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
|
21
22
|
const floatSheet = new CSSStyleSheet();
|
|
22
23
|
floatSheet.replaceSync(`:host([floating]) { display: block; position: fixed; z-index: 60; pointer-events: none; }`);
|
|
23
24
|
|
|
24
|
-
/** The border box of an element, or of the first descendant with one (display: contents hosts). */
|
|
25
|
-
function boxOf(el) {
|
|
26
|
-
if (el.getClientRects().length) return el.getBoundingClientRect();
|
|
27
|
-
for (const child of [...(el.shadowRoot?.children ?? []), ...el.children]) {
|
|
28
|
-
const r = boxOf(child);
|
|
29
|
-
if (r.width || r.height) return r;
|
|
30
|
-
}
|
|
31
|
-
return el.getBoundingClientRect();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
25
|
class NkMenu extends NkElement {
|
|
35
26
|
static get hostStyles() { return floatSheet; }
|
|
36
27
|
static get observedAttributes() { return ['floating', 'sheet', 'open', 'align']; }
|
|
@@ -71,10 +62,7 @@ class NkMenu extends NkElement {
|
|
|
71
62
|
show(anchor) {
|
|
72
63
|
if (anchor) {
|
|
73
64
|
this._anchor = anchor;
|
|
74
|
-
|
|
75
|
-
this.style.top = `${r.bottom + 6}px`;
|
|
76
|
-
if (this.getAttribute('align') === 'start') { this.style.left = `${r.left}px`; this.style.right = ''; }
|
|
77
|
-
else { this.style.right = `${Math.max(8, innerWidth - r.right)}px`; this.style.left = ''; }
|
|
65
|
+
placeUnder(this, anchor, this.getAttribute('align'));
|
|
78
66
|
}
|
|
79
67
|
this.setBoolAttr('open', true);
|
|
80
68
|
}
|
|
@@ -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;
|