@jungherz-de/notionkit-elements 1.10.1 → 1.11.1

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