@jungherz-de/glasskit-elements 1.26.0 → 1.28.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 +47 -1
- package/README.md +49 -6
- package/SKILL.md +72 -10
- package/dist/components/base.js +9 -3
- package/dist/components/glk-modal.js +37 -1
- package/dist/components/glk-segmented.js +58 -4
- package/dist/components/glk-tab-bar.js +81 -14
- package/dist/components/glk-tab-item.js +42 -3
- package/dist/components/shared/glasskit-droplet-3vJVXB3u.js +237 -0
- package/dist/glasskit-elements.esm.js +464 -28
- package/dist/glasskit-elements.js +464 -28
- package/dist/glasskit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/base.js +9 -3
- package/src/components/feedback/glk-modal.js +37 -1
- package/src/components/forms/glk-segmented.js +58 -4
- package/src/components/navigation/glk-tab-bar.js +81 -14
- package/src/components/navigation/glk-tab-item.js +42 -3
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { GlkElement } from './base.js';
|
|
2
|
+
import { a as attachDroplet } from './shared/glasskit-droplet-3vJVXB3u.js';
|
|
2
3
|
import '@jungherz-de/glasskit/glasskit-styles.js';
|
|
3
4
|
|
|
5
|
+
// droplet (since 1.27.0): the active item sits on a drop of glass that glides
|
|
6
|
+
// to the next one and can be dragged — GlassKit's .glass-tab-bar--droplet,
|
|
7
|
+
// moved by its glasskit-droplet.js. The items live in shadow roots of their
|
|
8
|
+
// own, so the bar hands its state down to them (see glk-tab-item _syncBar)
|
|
9
|
+
// and tells the drop when the active item changed.
|
|
10
|
+
|
|
4
11
|
class GlkTabBar extends GlkElement {
|
|
5
12
|
static get observedAttributes() {
|
|
6
|
-
return ['static', 'floating'];
|
|
13
|
+
return ['static', 'floating', 'droplet'];
|
|
7
14
|
}
|
|
8
15
|
|
|
9
16
|
render() {
|
|
@@ -15,6 +22,7 @@ class GlkTabBar extends GlkElement {
|
|
|
15
22
|
if (this.getBoolAttr('floating')) {
|
|
16
23
|
nav.classList.add('glass-tab-bar--floating');
|
|
17
24
|
}
|
|
25
|
+
nav.classList.toggle('glass-tab-bar--droplet', this.getBoolAttr('droplet'));
|
|
18
26
|
|
|
19
27
|
if (this.getBoolAttr('static')) {
|
|
20
28
|
this._applyStatic();
|
|
@@ -27,6 +35,10 @@ class GlkTabBar extends GlkElement {
|
|
|
27
35
|
this._applyStatic();
|
|
28
36
|
} else if (name === 'floating') {
|
|
29
37
|
this._nav.classList.toggle('glass-tab-bar--floating', this.getBoolAttr('floating'));
|
|
38
|
+
this._syncItems();
|
|
39
|
+
} else if (name === 'droplet') {
|
|
40
|
+
this._nav.classList.toggle('glass-tab-bar--droplet', this.getBoolAttr('droplet'));
|
|
41
|
+
this._applyDroplet();
|
|
30
42
|
}
|
|
31
43
|
}
|
|
32
44
|
|
|
@@ -35,24 +47,79 @@ class GlkTabBar extends GlkElement {
|
|
|
35
47
|
this._nav.style.borderRadius = '16px';
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.
|
|
50
|
+
_items() {
|
|
51
|
+
return [...this.children].filter(el => el.localName === 'glk-tab-item');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Hands the bar's floating and droplet state to every item's wrapper. */
|
|
55
|
+
_syncItems() {
|
|
56
|
+
const state = {
|
|
57
|
+
floating: this.getBoolAttr('floating'),
|
|
58
|
+
droplet: this.getBoolAttr('droplet'),
|
|
59
|
+
placed: Boolean(this._nav?.hasAttribute('data-droplet')),
|
|
60
|
+
drag: Boolean(this._nav?.hasAttribute('data-droplet-drag')),
|
|
49
61
|
};
|
|
50
|
-
this.
|
|
62
|
+
for (const item of this._items()) item._syncBar?.(state);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Called by an item that rendered or changed its active state. */
|
|
66
|
+
_itemChanged() {
|
|
67
|
+
this._syncItems();
|
|
68
|
+
this._droplet?.update();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_applyDroplet() {
|
|
72
|
+
const on = this.getBoolAttr('droplet') && this.isConnected;
|
|
73
|
+
if (on && !this._droplet) {
|
|
74
|
+
// The drop measures and marks the items' own buttons; picking one clicks
|
|
75
|
+
// it, which is what a tap does.
|
|
76
|
+
this._droplet = attachDroplet(this._nav, {
|
|
77
|
+
items: () => this._items().map(item => item._btn).filter(Boolean),
|
|
78
|
+
});
|
|
79
|
+
} else if (!on && this._droplet) {
|
|
80
|
+
this._droplet.destroy();
|
|
81
|
+
this._droplet = null;
|
|
82
|
+
}
|
|
83
|
+
this._syncItems();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Called by an item the user selected once its glk-tab-select went through
|
|
88
|
+
* uncancelled; the item is active already. Until 1.27.0 the bar did this
|
|
89
|
+
* while the event was still on its way, so a listener further up could not
|
|
90
|
+
* have kept the current tab.
|
|
91
|
+
*/
|
|
92
|
+
_selected(tab) {
|
|
93
|
+
for (const item of this.querySelectorAll('glk-tab-item')) {
|
|
94
|
+
if (item !== tab) item.removeAttribute('active');
|
|
95
|
+
}
|
|
96
|
+
this.emit('glk-tab-change', { tab });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setupEvents() {
|
|
100
|
+
// The drop's state lives on the nav; the items carry it on their wrappers.
|
|
101
|
+
this._navObserver = new MutationObserver(() => this._syncItems());
|
|
102
|
+
this._navObserver.observe(this._nav, { attributes: true, attributeFilter: ['data-droplet', 'data-droplet-drag'] });
|
|
103
|
+
// Items added or removed.
|
|
104
|
+
this._itemObserver = new MutationObserver(() => this._itemChanged());
|
|
105
|
+
this._itemObserver.observe(this, { childList: true });
|
|
106
|
+
this._applyDroplet();
|
|
51
107
|
}
|
|
52
108
|
|
|
53
109
|
teardownEvents() {
|
|
54
|
-
this.
|
|
110
|
+
this._navObserver?.disconnect();
|
|
111
|
+
this._itemObserver?.disconnect();
|
|
112
|
+
this._droplet?.destroy();
|
|
113
|
+
this._droplet = null;
|
|
55
114
|
}
|
|
115
|
+
|
|
116
|
+
// Properties as well (since 1.28.0): `bar.droplet = true` set an own
|
|
117
|
+
// property before and changed nothing.
|
|
118
|
+
get floating() { return this.getBoolAttr('floating'); }
|
|
119
|
+
set floating(v) { this.setBoolAttr('floating', v); }
|
|
120
|
+
|
|
121
|
+
get droplet() { return this.getBoolAttr('droplet'); }
|
|
122
|
+
set droplet(v) { this.setBoolAttr('droplet', v); }
|
|
56
123
|
}
|
|
57
124
|
|
|
58
125
|
customElements.define('glk-tab-bar', GlkTabBar);
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { GlkElement } from './base.js';
|
|
2
2
|
import '@jungherz-de/glasskit/glasskit-styles.js';
|
|
3
3
|
|
|
4
|
+
// The icon: an inline <svg> child is copied into the shadow root, where
|
|
5
|
+
// GlassKit's rules reach it. Anything else — a font icon such as Material
|
|
6
|
+
// Icons, an <img>, an element of the page's own — goes into slot="icon"
|
|
7
|
+
// (since 1.28.0) and takes the state colour from GlassKit's icon box: active,
|
|
8
|
+
// hover, the lens of the drop and the drag. With a slotted icon no SVG is
|
|
9
|
+
// copied.
|
|
10
|
+
//
|
|
11
|
+
// glk-tab-select (cancelable since 1.28.0) comes before `active` changes:
|
|
12
|
+
// preventDefault() keeps the current tab — for an item that opens a drawer
|
|
13
|
+
// or a link — and the bar then neither moves `active` nor its drop, nor
|
|
14
|
+
// emits glk-tab-change. A drag of the drop that ends on the item clicks it,
|
|
15
|
+
// so it takes the same path.
|
|
16
|
+
|
|
4
17
|
class GlkTabItem extends GlkElement {
|
|
5
18
|
static get observedAttributes() {
|
|
6
19
|
return ['label', 'active', 'badge'];
|
|
@@ -12,8 +25,10 @@ class GlkTabItem extends GlkElement {
|
|
|
12
25
|
this._btn = this.createElement('button', ['glass-tab-bar__item']);
|
|
13
26
|
if (this.getBoolAttr('active')) this._btn.classList.add('is-active');
|
|
14
27
|
|
|
15
|
-
// Icon — clone SVG from light DOM into shadow DOM so GlassKit CSS applies
|
|
28
|
+
// Icon — clone SVG from light DOM into shadow DOM so GlassKit CSS applies,
|
|
29
|
+
// or show what is slotted as icon
|
|
16
30
|
this._iconEl = this.createElement('span', ['glass-tab-bar__icon']);
|
|
31
|
+
this._iconEl.appendChild(this.createElement('slot', [], { name: 'icon' }));
|
|
17
32
|
|
|
18
33
|
// Label
|
|
19
34
|
this._labelEl = this.createElement('span', ['glass-tab-bar__label']);
|
|
@@ -37,7 +52,8 @@ class GlkTabItem extends GlkElement {
|
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
projectLightDom() {
|
|
40
|
-
|
|
55
|
+
// A slotted icon wins; only without one is an <svg> copied.
|
|
56
|
+
const svg = this.querySelector(':scope > [slot="icon"]') ? null : this.querySelector('svg');
|
|
41
57
|
const signature = svg ? svg.outerHTML : '';
|
|
42
58
|
if (signature === this._iconSignature) return;
|
|
43
59
|
this._iconSignature = signature;
|
|
@@ -63,10 +79,32 @@ class GlkTabItem extends GlkElement {
|
|
|
63
79
|
|
|
64
80
|
setupEvents() {
|
|
65
81
|
this._onClick = () => {
|
|
82
|
+
if (!this.emit('glk-tab-select', null, { cancelable: true })) return;
|
|
66
83
|
this.setAttribute('active', '');
|
|
67
|
-
this.
|
|
84
|
+
this.closest('glk-tab-bar')?._selected?.(this);
|
|
68
85
|
};
|
|
69
86
|
this._btn.addEventListener('click', this._onClick);
|
|
87
|
+
this._tellBar();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The bar's look reaches the item through its wrapper. GlassKit's rules for
|
|
92
|
+
* floating and droplet items are descendant selectors of the bar's classes,
|
|
93
|
+
* and this button sits in a shadow root of its own, so <glk-tab-bar> hands
|
|
94
|
+
* its state down and the wrapper carries it. Before 1.27.0 the items of a
|
|
95
|
+
* floating bar kept the fixed bar's look: no spotlight, no pill, the dot.
|
|
96
|
+
*/
|
|
97
|
+
_syncBar({ floating, droplet, placed, drag }) {
|
|
98
|
+
if (!this._wrapper) return;
|
|
99
|
+
this._wrapper.classList.toggle('glass-tab-bar--floating', floating);
|
|
100
|
+
this._wrapper.classList.toggle('glass-tab-bar--droplet', droplet);
|
|
101
|
+
this._wrapper.toggleAttribute('data-droplet', placed);
|
|
102
|
+
this._wrapper.toggleAttribute('data-droplet-drag', drag);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Tells the bar that this item rendered or changed its active state. */
|
|
106
|
+
_tellBar() {
|
|
107
|
+
this.closest('glk-tab-bar')?._itemChanged?.();
|
|
70
108
|
}
|
|
71
109
|
|
|
72
110
|
teardownEvents() {
|
|
@@ -78,6 +116,7 @@ class GlkTabItem extends GlkElement {
|
|
|
78
116
|
switch (name) {
|
|
79
117
|
case 'active':
|
|
80
118
|
this._btn.classList.toggle('is-active', this.getBoolAttr('active'));
|
|
119
|
+
this._tellBar();
|
|
81
120
|
break;
|
|
82
121
|
case 'label':
|
|
83
122
|
this._labelEl.textContent = this.getAttribute('label') || '';
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// GlassKit – Droplet
|
|
3
|
+
//
|
|
4
|
+
// The motion behind .glass-tab-bar--droplet and .glass-segmented--droplet.
|
|
5
|
+
// The stylesheet draws the drop of glass; this module puts it on the chosen
|
|
6
|
+
// item, lets it run to a new one, and lets a finger drag it along the row.
|
|
7
|
+
// Optional and dependency-free: without it a --droplet row keeps its usual
|
|
8
|
+
// look, the tab bar its spotlight and the segmented row its raised option.
|
|
9
|
+
//
|
|
10
|
+
// import { attachDroplets } from '@jungherz-de/glasskit/glasskit-droplet.js';
|
|
11
|
+
// attachDroplets(); // every --droplet row in the page
|
|
12
|
+
//
|
|
13
|
+
// import { attachDroplet } from '@jungherz-de/glasskit/glasskit-droplet.js';
|
|
14
|
+
// const drop = attachDroplet(row); // one row; drop.update(), drop.destroy()
|
|
15
|
+
//
|
|
16
|
+
// The chosen item is the tab with .is-active or the option with
|
|
17
|
+
// aria-pressed="true". The page keeps setting them its own way and the drop
|
|
18
|
+
// follows. Dragged onto another item, the drop clicks it, so the page's own
|
|
19
|
+
// click handler makes the choice. In a row that scrolls sideways a swipe
|
|
20
|
+
// still scrolls: only the drop itself is dragged, and the row scrolls along
|
|
21
|
+
// at its edges.
|
|
22
|
+
//
|
|
23
|
+
// It writes what glasskit.css reads: data-droplet on the row, the chosen
|
|
24
|
+
// item's box as --gl-droplet-left, -right, -top and -bottom, data-droplet-dir
|
|
25
|
+
// and data-droplet-lift while the drop travels, data-droplet-drag while a
|
|
26
|
+
// finger holds it, and .is-under-droplet on the item below it.
|
|
27
|
+
//
|
|
28
|
+
// Options of attachDroplet, for rows whose items live elsewhere (GlassKit
|
|
29
|
+
// Elements uses them): items() returns the items, isChosen(item) tells the
|
|
30
|
+
// chosen one, choose(item) picks one, lens(item, on) marks the item below
|
|
31
|
+
// the drop. Without a MutationObserver reaching the items, call update()
|
|
32
|
+
// after the choice changed.
|
|
33
|
+
// ============================================================
|
|
34
|
+
|
|
35
|
+
const ITEMS = '.glass-tab-bar__item, .glass-segmented__item';
|
|
36
|
+
const attached = new WeakMap();
|
|
37
|
+
|
|
38
|
+
function attachDroplet(row, options = {}) {
|
|
39
|
+
if (attached.has(row)) return attached.get(row);
|
|
40
|
+
|
|
41
|
+
const items = options.items || (() => [...row.querySelectorAll(ITEMS)]);
|
|
42
|
+
const isChosen = options.isChosen
|
|
43
|
+
|| (it => it.classList.contains('is-active') || it.getAttribute('aria-pressed') === 'true');
|
|
44
|
+
const choose = options.choose || (it => it.click());
|
|
45
|
+
const mark = options.lens || ((it, on) => it.classList.toggle('is-under-droplet', on));
|
|
46
|
+
const usable = it => !it.disabled && it.getAttribute('aria-disabled') !== 'true';
|
|
47
|
+
const scrolls = () => row.scrollWidth > row.clientWidth + 1;
|
|
48
|
+
const motion = matchMedia('(prefers-reduced-motion: no-preference)');
|
|
49
|
+
const set = (k, px) => row.style.setProperty(`--gl-droplet-${k}`, `${px}px`);
|
|
50
|
+
|
|
51
|
+
let chosen = null, left = 0, placed = null, liftTimer = 0, lensFrame = 0, edgeFrame = 0;
|
|
52
|
+
let drag = null, eatClick = false, under = null;
|
|
53
|
+
|
|
54
|
+
// The row on screen and its scale there. Rects are viewport pixels, scaled
|
|
55
|
+
// by every transform on an ancestor — an app that zooms in on start, a
|
|
56
|
+
// popover that opens from scale(0.96) — while the drop's lengths are the
|
|
57
|
+
// row's own. So distances on screen are divided by the scale: the drop
|
|
58
|
+
// sits right at rest and while such a transform runs, and nothing needs
|
|
59
|
+
// measuring again when it ends (before 1.28.0 it stayed beside the item).
|
|
60
|
+
// The layout size is the computed one under GlassKit's border-box,
|
|
61
|
+
// unscaled and nearly unrounded, else the offset size, rounded to whole
|
|
62
|
+
// pixels. A difference within that rounding is no transform: the rects
|
|
63
|
+
// are then taken as they are, as before.
|
|
64
|
+
const frame = () => {
|
|
65
|
+
const n = row.getBoundingClientRect(), cs = getComputedStyle(row);
|
|
66
|
+
const exact = cs.boxSizing === 'border-box', tolerance = exact ? 0.05 : 0.5;
|
|
67
|
+
const w = exact ? parseFloat(cs.width) : row.offsetWidth;
|
|
68
|
+
const h = exact ? parseFloat(cs.height) : row.offsetHeight;
|
|
69
|
+
const scale = (onScreen, own) => onScreen > 0 && own > 0 && Math.abs(onScreen - own) > tolerance ? onScreen / own : 1;
|
|
70
|
+
return { n, sx: scale(n.width, w), sy: scale(n.height, h) };
|
|
71
|
+
};
|
|
72
|
+
// An item's box in the row's padding box; in a row that scrolls, in its
|
|
73
|
+
// scrolled content, where the drop scrolls along with the items.
|
|
74
|
+
const boxOf = (it, f = frame()) => {
|
|
75
|
+
const r = it.getBoundingClientRect();
|
|
76
|
+
const l = (r.left - f.n.left) / f.sx - row.clientLeft + row.scrollLeft;
|
|
77
|
+
const t = (r.top - f.n.top) / f.sy - row.clientTop + row.scrollTop;
|
|
78
|
+
const height = r.height / f.sy;
|
|
79
|
+
return { left: l, width: r.width / f.sx, top: t, bottom: row.clientHeight - t - height };
|
|
80
|
+
};
|
|
81
|
+
const put = (l, width, b) => {
|
|
82
|
+
left = l;
|
|
83
|
+
set('left', l); set('right', row.clientWidth - l - width);
|
|
84
|
+
set('top', b.top); set('bottom', b.bottom);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// The usable item nearest a point of the row lights up as under a lens.
|
|
88
|
+
const lens = centre => {
|
|
89
|
+
let below = null, best = Infinity;
|
|
90
|
+
const f = centre == null ? null : frame();
|
|
91
|
+
if (f) for (const it of items()) {
|
|
92
|
+
if (!usable(it)) continue;
|
|
93
|
+
const b = boxOf(it, f), d = Math.abs(b.left + b.width / 2 - centre);
|
|
94
|
+
if (d < best) { best = d; below = it; }
|
|
95
|
+
}
|
|
96
|
+
if (below !== under) {
|
|
97
|
+
if (under) mark(under, false);
|
|
98
|
+
if (below) mark(below, true);
|
|
99
|
+
under = below;
|
|
100
|
+
}
|
|
101
|
+
return below;
|
|
102
|
+
};
|
|
103
|
+
// While the drop travels, the lens follows its centre.
|
|
104
|
+
const follow = () => {
|
|
105
|
+
cancelAnimationFrame(lensFrame);
|
|
106
|
+
const start = performance.now();
|
|
107
|
+
const step = now => {
|
|
108
|
+
if (now - start > 620) { lens(null); return; }
|
|
109
|
+
const cs = getComputedStyle(row, '::before');
|
|
110
|
+
const l = parseFloat(cs.left), r = parseFloat(cs.right);
|
|
111
|
+
lens(l + (row.clientWidth - l - r) / 2);
|
|
112
|
+
lensFrame = requestAnimationFrame(step);
|
|
113
|
+
};
|
|
114
|
+
lensFrame = requestAnimationFrame(step);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const place = (it, travel) => {
|
|
118
|
+
const b = boxOf(it);
|
|
119
|
+
row.dataset.droplet = '';
|
|
120
|
+
if (travel && motion.matches && Math.abs(b.left - left) > 1) {
|
|
121
|
+
row.dataset.dropletDir = b.left > left ? 'right' : 'left';
|
|
122
|
+
row.dataset.dropletLift = '';
|
|
123
|
+
clearTimeout(liftTimer);
|
|
124
|
+
liftTimer = setTimeout(() => { delete row.dataset.dropletLift; }, 280);
|
|
125
|
+
follow();
|
|
126
|
+
} else {
|
|
127
|
+
delete row.dataset.dropletDir;
|
|
128
|
+
}
|
|
129
|
+
put(b.left, b.width, b);
|
|
130
|
+
placed = { ...b, rowWidth: row.clientWidth, rowHeight: row.clientHeight };
|
|
131
|
+
chosen = it;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Follow the choice. Nothing chosen, no drop. A new item is watched for
|
|
135
|
+
// its size too; a size that changed moves the drop without a journey.
|
|
136
|
+
const update = () => {
|
|
137
|
+
for (const it of items()) if (!watched.has(it)) { watched.add(it); ro.observe(it); }
|
|
138
|
+
const it = items().find(isChosen);
|
|
139
|
+
if (!it) {
|
|
140
|
+
if (chosen) { chosen = null; placed = null; delete row.dataset.droplet; }
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (it !== chosen) place(it, chosen !== null && row.dataset.droplet !== undefined);
|
|
144
|
+
};
|
|
145
|
+
const ro = new ResizeObserver(() => {
|
|
146
|
+
if (!chosen || drag?.moved) return;
|
|
147
|
+
const b = { ...boxOf(chosen), rowWidth: row.clientWidth, rowHeight: row.clientHeight };
|
|
148
|
+
if (!placed || Object.keys(b).some(k => Math.abs(b[k] - placed[k]) > 0.5)) place(chosen, false);
|
|
149
|
+
});
|
|
150
|
+
const watched = new Set();
|
|
151
|
+
ro.observe(row);
|
|
152
|
+
const mo = new MutationObserver(update);
|
|
153
|
+
mo.observe(row, { subtree: true, childList: true, attributes: true, attributeFilter: ['class', 'aria-pressed'] });
|
|
154
|
+
|
|
155
|
+
// Dragging: past 8px the drop slides under the finger and grows into a
|
|
156
|
+
// lens; let go, the item below it is chosen and the drop settles on it.
|
|
157
|
+
const at = clientX => {
|
|
158
|
+
const f = frame();
|
|
159
|
+
const x = (clientX - f.n.left) / f.sx - row.clientLeft + row.scrollLeft;
|
|
160
|
+
const boxes = items().map(it => boxOf(it, f));
|
|
161
|
+
const min = Math.min(...boxes.map(b => b.left));
|
|
162
|
+
const max = Math.max(...boxes.map(b => b.left + b.width)) - drag.width;
|
|
163
|
+
return Math.min(max, Math.max(min, x - drag.width / 2));
|
|
164
|
+
};
|
|
165
|
+
const track = () => {
|
|
166
|
+
const l = at(drag.clientX);
|
|
167
|
+
put(l, drag.width, boxOf(chosen));
|
|
168
|
+
drag.below = lens(l + drag.width / 2);
|
|
169
|
+
cancelAnimationFrame(edgeFrame);
|
|
170
|
+
if (!scrolls()) return;
|
|
171
|
+
const n = row.getBoundingClientRect(), zone = 32;
|
|
172
|
+
const v = drag.clientX < n.left + zone ? -1 : drag.clientX > n.right - zone ? 1 : 0;
|
|
173
|
+
if (v) edgeFrame = requestAnimationFrame(() => { row.scrollLeft += v * 6; if (drag?.moved) track(); });
|
|
174
|
+
};
|
|
175
|
+
const down = e => {
|
|
176
|
+
if (!chosen || drag || !usable(chosen) || (e.pointerType === 'mouse' && e.button !== 0)) return;
|
|
177
|
+
if (scrolls() && !e.composedPath().includes(chosen)) return;
|
|
178
|
+
drag = { id: e.pointerId, x: e.clientX, clientX: e.clientX, width: boxOf(chosen).width, moved: false, below: null };
|
|
179
|
+
};
|
|
180
|
+
const move = e => {
|
|
181
|
+
if (!drag || e.pointerId !== drag.id) return;
|
|
182
|
+
drag.clientX = e.clientX;
|
|
183
|
+
if (!drag.moved) {
|
|
184
|
+
if (Math.abs(e.clientX - drag.x) < 8) return;
|
|
185
|
+
drag.moved = true;
|
|
186
|
+
row.setPointerCapture(e.pointerId);
|
|
187
|
+
delete row.dataset.dropletDir;
|
|
188
|
+
row.dataset.dropletDrag = '';
|
|
189
|
+
}
|
|
190
|
+
track();
|
|
191
|
+
};
|
|
192
|
+
const up = e => {
|
|
193
|
+
if (!drag || e.pointerId !== drag.id) return;
|
|
194
|
+
const d = drag;
|
|
195
|
+
drag = null;
|
|
196
|
+
cancelAnimationFrame(edgeFrame);
|
|
197
|
+
if (!d.moved) return; // a tap: the item's own click does it
|
|
198
|
+
delete row.dataset.dropletDrag;
|
|
199
|
+
lens(null);
|
|
200
|
+
const target = d.below || chosen;
|
|
201
|
+
if (e.type === 'pointerup' && target !== chosen) choose(target);
|
|
202
|
+
// Settle where the choice is, unless it already set the drop on its way.
|
|
203
|
+
const next = items().find(isChosen) || chosen;
|
|
204
|
+
if (next !== chosen || row.dataset.dropletDir === undefined) place(next, true);
|
|
205
|
+
eatClick = true; // the click the drag ends in
|
|
206
|
+
setTimeout(() => { eatClick = false; }, 400);
|
|
207
|
+
};
|
|
208
|
+
const click = e => { if (eatClick) { eatClick = false; e.stopPropagation(); e.preventDefault(); } };
|
|
209
|
+
row.addEventListener('pointerdown', down);
|
|
210
|
+
row.addEventListener('pointermove', move);
|
|
211
|
+
row.addEventListener('pointerup', up);
|
|
212
|
+
row.addEventListener('pointercancel', up);
|
|
213
|
+
row.addEventListener('click', click, true);
|
|
214
|
+
|
|
215
|
+
update();
|
|
216
|
+
|
|
217
|
+
const handle = {
|
|
218
|
+
update,
|
|
219
|
+
destroy() {
|
|
220
|
+
mo.disconnect(); ro.disconnect();
|
|
221
|
+
clearTimeout(liftTimer); cancelAnimationFrame(lensFrame); cancelAnimationFrame(edgeFrame);
|
|
222
|
+
row.removeEventListener('pointerdown', down);
|
|
223
|
+
row.removeEventListener('pointermove', move);
|
|
224
|
+
row.removeEventListener('pointerup', up);
|
|
225
|
+
row.removeEventListener('pointercancel', up);
|
|
226
|
+
row.removeEventListener('click', click, true);
|
|
227
|
+
lens(null);
|
|
228
|
+
for (const k of ['droplet', 'dropletDir', 'dropletLift', 'dropletDrag']) delete row.dataset[k];
|
|
229
|
+
for (const k of ['left', 'right', 'top', 'bottom']) row.style.removeProperty(`--gl-droplet-${k}`);
|
|
230
|
+
attached.delete(row);
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
attached.set(row, handle);
|
|
234
|
+
return handle;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export { attachDroplet as a };
|