@jungherz-de/notionkit-elements 1.13.0 → 1.15.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 +33 -0
- package/README.md +11 -11
- package/SKILL.md +110 -51
- package/dist/components/nk-board-view.js +4 -3
- package/dist/components/nk-calendar-view.js +3 -1
- package/dist/components/nk-filter-bar.js +3 -2
- package/dist/components/nk-gallery-view.js +115 -0
- package/dist/components/nk-list-view.js +4 -3
- package/dist/components/nk-table-view.js +2 -1
- package/dist/components/shared/property-cell-BSVcETss.js +204 -0
- package/dist/notionkit-elements.esm.js +208 -22
- package/dist/notionkit-elements.js +211 -21
- package/dist/notionkit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/components/data/nk-board-view.js +3 -3
- package/src/components/data/nk-calendar-view.js +2 -1
- package/src/components/data/nk-filter-bar.js +3 -2
- package/src/components/data/nk-gallery-view.js +111 -0
- package/src/components/data/nk-list-view.js +3 -3
- package/src/index.js +2 -1
- package/src/util/property-cell.js +90 -12
- package/dist/components/shared/property-cell-DKvv6TIG.js +0 -126
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { NkElement } from './base.js';
|
|
2
|
+
import { a as textOf, f as formatDate, r as renderPropertyCell } from './shared/property-cell-BSVcETss.js';
|
|
3
|
+
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
4
|
+
import './shared/avatar-CzPZN3uP.js';
|
|
5
|
+
import './shared/dates-a32DcW1l.js';
|
|
6
|
+
|
|
7
|
+
// <nk-gallery-view name="gallery" label="🖼 Gallery" meta-keys="status,due" new-row></nk-gallery-view>
|
|
8
|
+
// The fifth database view (NotionKit 1.14.0): the rows as cards with a
|
|
9
|
+
// picture on top, in a grid that fills the row – Notion's gallery, for a
|
|
10
|
+
// course catalog or a reading list. `cover-key` names the row field with the
|
|
11
|
+
// picture's URL (default: cover); a row without one shows the cover
|
|
12
|
+
// gradient, and `no-cover` leaves the pictures out. `size` small | medium |
|
|
13
|
+
// large sets the card size, `fit` shows a picture whole instead of cropped.
|
|
14
|
+
// Cards show the title column and the `meta-keys` (default: select and date
|
|
15
|
+
// columns). Standalone: view.columns = […]; view.rows = […]. Inside
|
|
16
|
+
// <nk-database> the database pushes the data. A card fires nk-select
|
|
17
|
+
// { row, id, value } on a click, Enter or Space, like a row of the table;
|
|
18
|
+
// the add card nk-action { action: 'new-row' }.
|
|
19
|
+
// → <div class="nk-gallery" role="list"><div class="nk-card" role="listitem" tabindex="0">
|
|
20
|
+
// <div class="nk-cover"><img src="…" alt=""></div><div class="card-title">🧭 …</div><div class="card-meta">…</div></div>
|
|
21
|
+
// <div class="nk-new-row">+ New page</div></div>
|
|
22
|
+
const SIZES = ['small', 'large'];
|
|
23
|
+
|
|
24
|
+
class NkGalleryView extends NkElement {
|
|
25
|
+
static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'cover-key', 'no-cover', 'size', 'fit', 'title-key', 'meta-keys', 'new-row', 'new-row-label']; }
|
|
26
|
+
|
|
27
|
+
render() {
|
|
28
|
+
this._grid = this.createElement('div', ['nk-gallery'], { role: 'list' });
|
|
29
|
+
this._wrapper.appendChild(this._grid);
|
|
30
|
+
this._columns ??= []; this._rows ??= [];
|
|
31
|
+
this._render();
|
|
32
|
+
this.closest('nk-database')?.requestSync?.();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_titleColumn() {
|
|
36
|
+
const key = this.getAttribute('title-key');
|
|
37
|
+
return this._columns.find(c => c.key === key) || this._columns.find(c => c.title) || this._columns[0] || null;
|
|
38
|
+
}
|
|
39
|
+
_metaColumns() {
|
|
40
|
+
const keys = (this.getAttribute('meta-keys') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
41
|
+
if (keys.length) return keys.map(k => this._columns.find(c => c.key === k)).filter(Boolean);
|
|
42
|
+
return this._columns.filter(c => c.type === 'select' || c.type === 'date');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_render() {
|
|
46
|
+
if (!this._grid) return;
|
|
47
|
+
const size = this.getAttribute('size');
|
|
48
|
+
this._grid.className = ['nk-gallery', SIZES.includes(size) && size, this.getBoolAttr('fit') && 'fit'].filter(Boolean).join(' ');
|
|
49
|
+
const title = this._titleColumn(), meta = this._metaColumns();
|
|
50
|
+
const coverKey = this.getAttribute('cover-key') || 'cover', covers = !this.getBoolAttr('no-cover');
|
|
51
|
+
this._grid.replaceChildren();
|
|
52
|
+
for (const row of this._rows) {
|
|
53
|
+
const card = this.createElement('div', ['nk-card'], { role: 'listitem', tabindex: '0', 'data-id': row.id ?? '' });
|
|
54
|
+
if (covers) {
|
|
55
|
+
const cover = this.createElement('div', ['nk-cover']);
|
|
56
|
+
if (row[coverKey]) cover.appendChild(this.createElement('img', [], { src: row[coverKey], alt: '' }));
|
|
57
|
+
card.appendChild(cover);
|
|
58
|
+
}
|
|
59
|
+
const t = this.createElement('div', ['card-title']);
|
|
60
|
+
t.textContent = `${row.icon ? row.icon + ' ' : ''}${title ? textOf(row[title.key]) : ''}`;
|
|
61
|
+
const m = this.createElement('div', ['card-meta']);
|
|
62
|
+
for (const c of meta) {
|
|
63
|
+
const v = row[c.key];
|
|
64
|
+
if (v === undefined || v === null || v === '') continue;
|
|
65
|
+
const s = document.createElement('span');
|
|
66
|
+
if (c.type === 'progress') s.textContent = `▰ ${v}%`;
|
|
67
|
+
else if (c.type === 'date') s.textContent = `📅 ${formatDate(c, v)}`;
|
|
68
|
+
else s.appendChild(renderPropertyCell(c, v, row));
|
|
69
|
+
m.appendChild(s);
|
|
70
|
+
}
|
|
71
|
+
card.append(t, m);
|
|
72
|
+
this._grid.appendChild(card);
|
|
73
|
+
}
|
|
74
|
+
if (this.getBoolAttr('new-row')) {
|
|
75
|
+
const add = this.createElement('div', ['nk-new-row'], { role: 'button', tabindex: '0', 'data-add': '' });
|
|
76
|
+
add.textContent = this.getAttribute('new-row-label') || '+ New page';
|
|
77
|
+
this._grid.appendChild(add);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setupEvents() {
|
|
82
|
+
this._onClick = (e) => {
|
|
83
|
+
if (e.target.closest('[data-add]')) { this.emit('nk-action', { action: 'new-row' }); return; }
|
|
84
|
+
const card = e.target.closest('.nk-card');
|
|
85
|
+
if (card) { const row = this._rowById(card.dataset.id); this.emit('nk-select', { row, id: row?.id, value: row?.id }); }
|
|
86
|
+
};
|
|
87
|
+
this._onKey = (e) => {
|
|
88
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
89
|
+
if (e.target.matches?.('.nk-card, [data-add]')) { e.preventDefault(); e.target.click(); }
|
|
90
|
+
};
|
|
91
|
+
this._grid.addEventListener('click', this._onClick);
|
|
92
|
+
this._grid.addEventListener('keydown', this._onKey);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
teardownEvents() {
|
|
96
|
+
this._grid?.removeEventListener('click', this._onClick);
|
|
97
|
+
this._grid?.removeEventListener('keydown', this._onKey);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
|
|
101
|
+
onAttributeChanged() { this._render(); }
|
|
102
|
+
|
|
103
|
+
get name() { return this.getAttribute('name') || 'gallery'; }
|
|
104
|
+
get columns() { return this._columns; }
|
|
105
|
+
set columns(v) { this._columns = Array.isArray(v) ? v : []; this._render(); }
|
|
106
|
+
get rows() { return this._rows; }
|
|
107
|
+
set rows(v) { this._rows = Array.isArray(v) ? v : []; this._render(); }
|
|
108
|
+
set data({ columns, rows }) { this.setData(columns, rows); }
|
|
109
|
+
setData(columns, rows) { this._columns = columns || []; this._rows = rows || []; this._render(); }
|
|
110
|
+
refresh() { this._render(); }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
customElements.define('nk-gallery-view', NkGalleryView);
|
|
114
|
+
|
|
115
|
+
export { NkGalleryView };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { NkElement } from './base.js';
|
|
2
|
-
import { r as renderPropertyCell } from './shared/property-cell-
|
|
2
|
+
import { a as textOf, f as formatDate, r as renderPropertyCell } from './shared/property-cell-BSVcETss.js';
|
|
3
3
|
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
4
4
|
import './shared/avatar-CzPZN3uP.js';
|
|
5
|
+
import './shared/dates-a32DcW1l.js';
|
|
5
6
|
|
|
6
7
|
// <nk-list-view name="list" label="☰ List" meta-keys="due,status" new-row></nk-list-view>
|
|
7
8
|
// The third database view: one line per row – icon and title, the `meta-keys`
|
|
@@ -45,12 +46,12 @@ class NkListView extends NkElement {
|
|
|
45
46
|
icon.textContent = row.icon || '';
|
|
46
47
|
if (!row.icon) icon.style.display = 'none';
|
|
47
48
|
const t = this.createElement('span', ['l-title']);
|
|
48
|
-
t.textContent = title ?
|
|
49
|
+
t.textContent = title ? textOf(row[title.key]) : '';
|
|
49
50
|
const m = this.createElement('span', ['l-meta']);
|
|
50
51
|
for (const c of meta) {
|
|
51
52
|
const v = row[c.key];
|
|
52
53
|
if (v === undefined || v === null || v === '') continue;
|
|
53
|
-
m.appendChild(c.type === 'date'
|
|
54
|
+
m.appendChild(c.type === 'date' ? document.createTextNode(formatDate(c, v)) : c.type === 'text' || !c.type ? document.createTextNode(textOf(v)) : renderPropertyCell(c, v, row));
|
|
54
55
|
}
|
|
55
56
|
item.append(icon, t, m);
|
|
56
57
|
this._list.appendChild(item);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { NkElement } from './base.js';
|
|
2
|
-
import { c as compareBy, r as renderPropertyCell } from './shared/property-cell-
|
|
2
|
+
import { c as compareBy, r as renderPropertyCell } from './shared/property-cell-BSVcETss.js';
|
|
3
3
|
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
4
4
|
import './shared/avatar-CzPZN3uP.js';
|
|
5
|
+
import './shared/dates-a32DcW1l.js';
|
|
5
6
|
|
|
6
7
|
// <nk-table-view name="table" label="▦ Table" count new-row sortable wrap></nk-table-view>
|
|
7
8
|
// `wrap` lets cell text break (`.nk-table.wrap`), like Notion's "wrap column".
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { i as initialsOf, p as paintAvatar } from './avatar-CzPZN3uP.js';
|
|
2
|
+
import { r as resolveLocale } from './dates-a32DcW1l.js';
|
|
3
|
+
|
|
4
|
+
// Polymorphic property renderer for database views. Returns plain DOM built
|
|
5
|
+
// from NotionKit classes – deliberately not a custom element: every cell rule
|
|
6
|
+
// starts with `.nk-table`, so a cell inside its own shadow root would never
|
|
7
|
+
// be styled. Exported for consumers who render tables themselves.
|
|
8
|
+
//
|
|
9
|
+
// column: { key, label, type, icon, options: [{ value, label, color }], title }
|
|
10
|
+
// types: text | select | multi-select | date | person | checkbox | url | number | progress | actions
|
|
11
|
+
// url: a string is an address outside, opened in a new tab; { href, label,
|
|
12
|
+
// target } a link of your own – to another page of the app, with its text.
|
|
13
|
+
// actions: buttons for the row from column.actions [{ action, label, icon,
|
|
14
|
+
// danger, disabled, tooltip }]; the row's value – action names or objects –
|
|
15
|
+
// picks which of them it shows, all of them without one.
|
|
16
|
+
// text: a string, or { text, desc, color, tooltip, sort } – one of Notion's
|
|
17
|
+
// nine text colours for a tone, a quiet second line under it, the whole
|
|
18
|
+
// text in a tooltip (served by <nk-tooltip>), a value to sort by.
|
|
19
|
+
// date: the value as given, or with column.format ('short', 'relative' or
|
|
20
|
+
// Intl.DateTimeFormat options) and column.locale formatted from an ISO date
|
|
21
|
+
// or date-time, D.M.YYYY, a Date or a timestamp; { start, end } is a range.
|
|
22
|
+
// Sorting (compareBy) goes by column.sortKey – another field of the row –
|
|
23
|
+
// when there is one, else by a value's `sort`, a date's time, a select's
|
|
24
|
+
// place among its options; empty cells come last in both directions.
|
|
25
|
+
|
|
26
|
+
const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
|
|
27
|
+
const COLORS = ['gray', 'brown', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink', 'red'];
|
|
28
|
+
|
|
29
|
+
function optionFor(column, value) {
|
|
30
|
+
const opts = column.options || [];
|
|
31
|
+
return opts.find(o => o.value === value) || opts.find(o => o.label === value) || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function tagFor(column, value) {
|
|
35
|
+
const opt = optionFor(column, value);
|
|
36
|
+
const color = opt?.color && COLORS.includes(opt.color) ? opt.color : 'gray'; // Notion's default option colour
|
|
37
|
+
return el('span', `nk-tag ${color}`, opt?.label ?? String(value));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The text of a value: a string as it is, a text, link or person object's text, label or name. */
|
|
41
|
+
const textOf = v => (v && typeof v === 'object' ? v.text ?? v.label ?? v.name ?? '' : String(v ?? ''));
|
|
42
|
+
|
|
43
|
+
/** The time of a date value in ms: an ISO date – local midnight – or date-time, D.M.YYYY, a Date, a timestamp, a range's start; NaN when it is none. */
|
|
44
|
+
function timeOf(v) {
|
|
45
|
+
if (v instanceof Date) return v.getTime();
|
|
46
|
+
if (typeof v === 'number') return v;
|
|
47
|
+
if (v && typeof v === 'object') return timeOf(v.start);
|
|
48
|
+
const s = String(v ?? '').trim();
|
|
49
|
+
let m = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
|
50
|
+
if (m) return new Date(+m[3], +m[2] - 1, +m[1]).getTime();
|
|
51
|
+
m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
52
|
+
if (m) return new Date(+m[1], +m[2] - 1, +m[3]).getTime();
|
|
53
|
+
return /^\d{4}-\d{2}-\d{2}T/.test(s) ? Date.parse(s) : NaN;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A date as its column shows it: as given without column.format; 'short' –
|
|
58
|
+
* day and month, the year when it is not this one; 'relative' – minutes or
|
|
59
|
+
* hours ago or ahead within a day, else the short date; or the options of
|
|
60
|
+
* Intl.DateTimeFormat. A range { start, end } stands as start → end.
|
|
61
|
+
*/
|
|
62
|
+
function formatDate(column, value) {
|
|
63
|
+
if (value && typeof value === 'object' && !(value instanceof Date)) return [value.start, value.end].filter(Boolean).map(v => formatDate(column, v)).join(' → ');
|
|
64
|
+
const format = column.format, t = timeOf(value);
|
|
65
|
+
if (!format || Number.isNaN(t)) return String(value ?? '');
|
|
66
|
+
const d = new Date(t), locale = column.locale || resolveLocale();
|
|
67
|
+
if (format === 'relative') {
|
|
68
|
+
const minutes = Math.round((t - Date.now()) / 60000), rel = new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style: 'short' });
|
|
69
|
+
if (Math.abs(minutes) < 60) return rel.format(minutes, 'minute');
|
|
70
|
+
if (Math.abs(minutes) < 24 * 60) return rel.format(Math.round(minutes / 60), 'hour');
|
|
71
|
+
}
|
|
72
|
+
if (format === 'short' || format === 'relative') {
|
|
73
|
+
const year = d.getFullYear() === new Date().getFullYear() ? {} : { year: 'numeric' };
|
|
74
|
+
return d.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', ...year });
|
|
75
|
+
}
|
|
76
|
+
return typeof format === 'object' ? new Intl.DateTimeFormat(locale, format).format(d) : String(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderPropertyCell(column, value, row = {}) {
|
|
80
|
+
const type = column.type || 'text';
|
|
81
|
+
if (type === 'actions') return actions(column, value);
|
|
82
|
+
if (value === undefined || value === null || value === '') {
|
|
83
|
+
if (type === 'checkbox') return checkbox(column, false, row);
|
|
84
|
+
if (type === 'progress') return progress(0);
|
|
85
|
+
return el('span', type === 'person' ? 'person-cell' : null, type === 'text' && column.title ? '' : '—');
|
|
86
|
+
}
|
|
87
|
+
switch (type) {
|
|
88
|
+
case 'select': return tagFor(column, value);
|
|
89
|
+
case 'multi-select': {
|
|
90
|
+
const wrap = el('span');
|
|
91
|
+
wrap.style.display = 'inline-flex'; wrap.style.gap = '4px'; wrap.style.flexWrap = 'wrap';
|
|
92
|
+
for (const v of Array.isArray(value) ? value : String(value).split(',').map(s => s.trim()).filter(Boolean)) wrap.appendChild(tagFor(column, v));
|
|
93
|
+
return wrap;
|
|
94
|
+
}
|
|
95
|
+
case 'date': return el('span', 'date-cell', formatDate(column, value));
|
|
96
|
+
case 'person': {
|
|
97
|
+
const cell = el('span', 'person-cell');
|
|
98
|
+
const p = typeof value === 'string' ? { name: value } : value;
|
|
99
|
+
// color: one of Notion's nine names, or any CSS background; none – the avatar gradient.
|
|
100
|
+
const avatar = el('span', 'nk-avatar small', p.initials || p.avatar || initialsOf(p.name));
|
|
101
|
+
paintAvatar(avatar, p.color);
|
|
102
|
+
cell.append(avatar, document.createTextNode(' ' + (p.name || '')));
|
|
103
|
+
return cell;
|
|
104
|
+
}
|
|
105
|
+
case 'checkbox': return checkbox(column, !!value, row);
|
|
106
|
+
case 'url': {
|
|
107
|
+
const link = typeof value === 'object' ? value : { href: String(value), label: String(value).replace(/^https?:\/\//, ''), target: '_blank' };
|
|
108
|
+
const a = el('a', null, link.label ?? link.href);
|
|
109
|
+
a.href = link.href ?? '';
|
|
110
|
+
if (link.target) a.target = link.target;
|
|
111
|
+
if (link.target === '_blank') a.rel = 'noopener';
|
|
112
|
+
return a;
|
|
113
|
+
}
|
|
114
|
+
// column.locale / column.format: Intl.NumberFormat locale and options, e.g. { minimumFractionDigits: 1 }.
|
|
115
|
+
case 'number': return el('span', null, typeof value === 'number' ? value.toLocaleString(column.locale, column.format) : String(value));
|
|
116
|
+
case 'progress': return progress(Number(value) || 0);
|
|
117
|
+
default: {
|
|
118
|
+
const o = typeof value === 'object' ? value : { text: value };
|
|
119
|
+
let t;
|
|
120
|
+
if (column.title) {
|
|
121
|
+
t = el('span', 'row-title');
|
|
122
|
+
if (row.icon) t.append(document.createTextNode(row.icon + ' '));
|
|
123
|
+
t.append(document.createTextNode(String(o.text ?? '')));
|
|
124
|
+
} else if (typeof value === 'object') {
|
|
125
|
+
t = el('span', o.color && COLORS.includes(o.color) ? `td-text ${o.color}` : 'td-text', String(o.text ?? ''));
|
|
126
|
+
} else return el('span', null, String(value));
|
|
127
|
+
if (o.tooltip) t.dataset.tooltip = o.tooltip;
|
|
128
|
+
if (!o.desc) return t;
|
|
129
|
+
const frag = document.createDocumentFragment();
|
|
130
|
+
frag.append(t, el('span', 'td-desc', String(o.desc)));
|
|
131
|
+
return frag;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function actions(column, value) {
|
|
137
|
+
const all = column.actions || [];
|
|
138
|
+
const list = value == null ? all
|
|
139
|
+
: (Array.isArray(value) ? value : [value]).map(v => typeof v === 'string' ? all.find(a => a.action === v) ?? { action: v, label: v } : v);
|
|
140
|
+
const wrap = el('span', 'row-actions');
|
|
141
|
+
for (const a of list) {
|
|
142
|
+
const b = el('button', `nk-btn ${a.danger ? 'danger' : 'secondary'} small`, [a.icon, a.label ?? a.action].filter(Boolean).join(' '));
|
|
143
|
+
b.type = 'button';
|
|
144
|
+
b.dataset.action = a.action;
|
|
145
|
+
b.disabled = !!a.disabled;
|
|
146
|
+
if (a.tooltip) b.dataset.tooltip = a.tooltip;
|
|
147
|
+
wrap.appendChild(b);
|
|
148
|
+
}
|
|
149
|
+
return wrap;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function checkbox(column, checked, row) {
|
|
153
|
+
const label = el('label', 'nk-check');
|
|
154
|
+
const input = el('input');
|
|
155
|
+
input.type = 'checkbox'; input.checked = checked;
|
|
156
|
+
input.dataset.key = column.key;
|
|
157
|
+
if (row.id != null) input.dataset.rowId = row.id;
|
|
158
|
+
label.appendChild(input);
|
|
159
|
+
return label;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function progress(value) {
|
|
163
|
+
const frag = document.createDocumentFragment();
|
|
164
|
+
const bar = el('span', 'nk-progress');
|
|
165
|
+
const fill = el('i'); fill.style.width = `${Math.max(0, Math.min(100, value))}%`;
|
|
166
|
+
bar.appendChild(fill);
|
|
167
|
+
frag.append(bar, el('span', 'nk-progress-label', `${Math.round(value)}%`));
|
|
168
|
+
return frag;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A select's place among its options: the order Notion sorts by. */
|
|
172
|
+
function rankOf(column, value) {
|
|
173
|
+
const i = (column.options || []).findIndex(o => o.value === value || o.label === value);
|
|
174
|
+
return i < 0 ? (column.options || []).length : i;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** What a column sorts a row by – see the head of this file. */
|
|
178
|
+
function sortValue(column, row) {
|
|
179
|
+
if (column.sortKey) return row[column.sortKey];
|
|
180
|
+
const v = row[column.key];
|
|
181
|
+
if (v === undefined || v === null || v === '') return null;
|
|
182
|
+
if (typeof v === 'object' && !Array.isArray(v) && 'sort' in v) return v.sort;
|
|
183
|
+
switch (column.type || 'text') {
|
|
184
|
+
case 'number': case 'progress': return Number(v);
|
|
185
|
+
case 'checkbox': return v ? 1 : 0;
|
|
186
|
+
case 'date': { const t = timeOf(v); return Number.isNaN(t) ? String(v) : t; }
|
|
187
|
+
case 'select': return rankOf(column, v);
|
|
188
|
+
case 'multi-select': { const first = (Array.isArray(v) ? v : String(v).split(','))[0]; return first == null ? null : rankOf(column, String(first).trim()); }
|
|
189
|
+
default: return textOf(v);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Sort comparator for a column; empty cells come last whichever the direction. */
|
|
194
|
+
function compareBy(column, dir = 1) {
|
|
195
|
+
const empty = v => v === null || v === undefined || v === '' || Number.isNaN(v);
|
|
196
|
+
return (a, b) => {
|
|
197
|
+
const x = sortValue(column, a), y = sortValue(column, b);
|
|
198
|
+
if (empty(x) || empty(y)) return empty(x) === empty(y) ? 0 : empty(x) ? 1 : -1;
|
|
199
|
+
if (typeof x === 'number' && typeof y === 'number') return (x - y) * dir;
|
|
200
|
+
return String(x).localeCompare(String(y), undefined, { numeric: true }) * dir;
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export { textOf as a, compareBy as c, formatDate as f, renderPropertyCell as r, tagFor as t };
|