@txstate-mws/svelte-components 1.6.14 → 1.6.15

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.
@@ -1,5 +1,50 @@
1
1
  import { watchForMutations } from '../util/index.js';
2
2
  import { equal } from 'txstate-utils';
3
+ /**
4
+ * All active glue instances share one scroll listener and one requestAnimationFrame
5
+ * per frame. Each frame runs every registered measure() (the layout reads) and only
6
+ * then runs the write thunks they return, so for a page with thousands of glued
7
+ * elements the browser reads layout once per frame and never interleaves reads with
8
+ * writes. Multiple scroll events that arrive before a frame paints coalesce into a
9
+ * single measure pass.
10
+ */
11
+ const glueInstances = new Set();
12
+ let frameRequested = false;
13
+ let scrollBound = false;
14
+ function runGlueFrame() {
15
+ frameRequested = false;
16
+ const writes = [];
17
+ for (const measure of glueInstances) {
18
+ const write = measure();
19
+ if (write)
20
+ writes.push(write);
21
+ }
22
+ for (const write of writes)
23
+ write();
24
+ }
25
+ function scheduleGlueFrame() {
26
+ if (frameRequested)
27
+ return;
28
+ frameRequested = true;
29
+ requestAnimationFrame(runGlueFrame);
30
+ }
31
+ function onGlueScroll() {
32
+ scheduleGlueFrame();
33
+ }
34
+ function registerGlue(measure) {
35
+ glueInstances.add(measure);
36
+ if (!scrollBound) {
37
+ document.addEventListener('scroll', onGlueScroll, { capture: true, passive: true });
38
+ scrollBound = true;
39
+ }
40
+ }
41
+ function unregisterGlue(measure) {
42
+ glueInstances.delete(measure);
43
+ if (glueInstances.size === 0 && scrollBound) {
44
+ document.removeEventListener('scroll', onGlueScroll, { capture: true });
45
+ scrollBound = false;
46
+ }
47
+ }
3
48
  /**
4
49
  * Make this element `position: fixed` and move it around to align with the target element.
5
50
  */
@@ -21,20 +66,33 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
21
66
  }
22
67
  }
23
68
  let lastrect;
24
- let timer;
25
- function reposition() {
26
- const fixedParent = fixedContainingBlock(el);
69
+ // The containing block (nearest ancestor that establishes a containing block for
70
+ // position: fixed) is determined by the ancestors' computed styles. Scrolling can
71
+ // never change it, but it CAN change when an ancestor gains/loses a transform,
72
+ // filter, will-change, contain, etc. Walking the ancestor chain and calling
73
+ // getComputedStyle on each node is expensive, so cache it across the high-frequency
74
+ // scroll events and recompute only when something might have changed: a DOM/style
75
+ // mutation, a resize, or a target change (see invalidateAndSchedule / update).
76
+ let fixedParent;
77
+ let fixedParentComputed = false;
78
+ // Read-only pass: gather geometry and build the write thunk. Runs inside the shared
79
+ // frame's measure phase, so it must not write to the DOM.
80
+ function measure() {
81
+ if (!fixedParentComputed) {
82
+ fixedParent = fixedContainingBlock(el);
83
+ fixedParentComputed = true;
84
+ }
27
85
  let fixedRect = { left: 0, top: 0, right: 0, bottom: 0 };
28
86
  if (fixedParent) {
29
87
  const tmpRect = fixedParent.getBoundingClientRect();
30
88
  fixedRect = { left: tmpRect.left, right: document.documentElement.clientWidth - tmpRect.right, top: tmpRect.top, bottom: document.documentElement.clientHeight - tmpRect.bottom };
31
89
  }
32
90
  if (!target)
33
- return;
91
+ return undefined;
34
92
  const tmpRect = target.getBoundingClientRect();
35
93
  const rect = { left: tmpRect.left - fixedRect.left, right: document.documentElement.clientWidth - tmpRect.right - fixedRect.right, top: tmpRect.top - fixedRect.top, bottom: document.documentElement.clientHeight - tmpRect.bottom - fixedRect.bottom, width: tmpRect.width, height: tmpRect.height };
36
94
  if (equal(rect, lastrect))
37
- return;
95
+ return undefined;
38
96
  lastrect = rect;
39
97
  let autoalign = align;
40
98
  const leftright = rect.right > rect.left ? 'left' : 'right';
@@ -84,81 +142,75 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
84
142
  const elOffsetHeight = el.offsetHeight;
85
143
  const parentClientHeight = parent?.clientHeight ?? 0;
86
144
  const parentOverflowY = parent?.style.overflowY;
145
+ let position;
87
146
  if (autoalign === 'bottomleft') {
88
- cancelAnimationFrame(timer);
89
- timer = requestAnimationFrame(() => {
147
+ position = () => {
90
148
  el.style.top = `${rect.top + (cover ? 0 : targetHeight) + gap}px`;
91
149
  el.style.left = `${rect.left}px`;
92
150
  el.style.removeProperty('bottom');
93
151
  el.style.removeProperty('right');
94
152
  adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
95
- });
153
+ };
96
154
  valign = 'bottom';
97
155
  halign = 'left';
98
156
  }
99
157
  else if (autoalign === 'bottomright') {
100
- cancelAnimationFrame(timer);
101
- timer = requestAnimationFrame(() => {
158
+ position = () => {
102
159
  el.style.top = `${rect.top + (cover ? 0 : targetHeight) + gap}px`;
103
160
  el.style.removeProperty('left');
104
161
  el.style.removeProperty('bottom');
105
162
  el.style.right = `${rect.right}px`;
106
163
  adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
107
- });
164
+ };
108
165
  valign = 'bottom';
109
166
  halign = 'right';
110
167
  }
111
168
  else if (autoalign === 'topleft') {
112
- cancelAnimationFrame(timer);
113
- timer = requestAnimationFrame(() => {
169
+ position = () => {
114
170
  el.style.removeProperty('top');
115
171
  el.style.left = `${rect.left}px`;
116
172
  el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight) + gap}px`;
117
173
  el.style.removeProperty('right');
118
174
  adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
119
- });
175
+ };
120
176
  valign = 'top';
121
177
  halign = 'left';
122
178
  }
123
179
  else if (autoalign === 'topright') {
124
- cancelAnimationFrame(timer);
125
- timer = requestAnimationFrame(() => {
180
+ position = () => {
126
181
  el.style.removeProperty('top');
127
182
  el.style.removeProperty('left');
128
183
  el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight) + gap}px`;
129
184
  el.style.right = `${rect.right}px`;
130
185
  adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
131
- });
186
+ };
132
187
  valign = 'top';
133
188
  halign = 'right';
134
189
  }
135
190
  else if (autoalign === 'top') {
136
- cancelAnimationFrame(timer);
137
- timer = requestAnimationFrame(() => {
191
+ position = () => {
138
192
  el.style.removeProperty('top');
139
193
  el.style.left = `${rect.left + rect.width / 2 - elWidth / 2}px`;
140
194
  el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight) + gap}px`;
141
195
  el.style.removeProperty('right');
142
196
  adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
143
- });
197
+ };
144
198
  valign = 'top';
145
199
  halign = 'center';
146
200
  }
147
201
  else if (autoalign === 'bottom') {
148
- cancelAnimationFrame(timer);
149
- timer = requestAnimationFrame(() => {
202
+ position = () => {
150
203
  el.style.top = `${rect.top + (cover ? 0 : targetHeight) + gap}px`;
151
204
  el.style.left = `${rect.left + rect.width / 2 - elWidth / 2}px`;
152
205
  el.style.removeProperty('bottom');
153
206
  el.style.removeProperty('right');
154
207
  adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
155
- });
208
+ };
156
209
  valign = 'bottom';
157
210
  halign = 'center';
158
211
  }
159
212
  else if (autoalign === 'left') {
160
- cancelAnimationFrame(timer);
161
- timer = requestAnimationFrame(() => {
213
+ position = () => {
162
214
  el.style.top = `${rect.top + rect.height / 2 - elHeight / 2}px`;
163
215
  el.style.removeProperty('bottom');
164
216
  if (cover) {
@@ -170,13 +222,12 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
170
222
  el.style.right = `${rect.right + rect.width + gap}px`;
171
223
  }
172
224
  adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
173
- });
225
+ };
174
226
  valign = 'center';
175
227
  halign = 'left';
176
228
  }
177
229
  else if (autoalign === 'right') {
178
- cancelAnimationFrame(timer);
179
- timer = requestAnimationFrame(() => {
230
+ position = () => {
180
231
  el.style.top = `${rect.top + rect.height / 2 - elHeight / 2}px`;
181
232
  el.style.removeProperty('bottom');
182
233
  if (cover) {
@@ -188,26 +239,35 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
188
239
  el.style.removeProperty('right');
189
240
  }
190
241
  adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
191
- });
242
+ };
192
243
  valign = 'center';
193
244
  halign = 'right';
194
245
  }
195
246
  else if (autoalign === 'middle') {
196
- cancelAnimationFrame(timer);
197
- timer = requestAnimationFrame(() => {
247
+ position = () => {
198
248
  el.style.top = `${rect.top + rect.height / 2 - elHeight / 2}px`;
199
249
  el.style.left = `${rect.left + rect.width / 2 - elWidth / 2}px`;
200
250
  el.style.removeProperty('bottom');
201
251
  el.style.removeProperty('right');
202
252
  adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
203
- });
253
+ };
204
254
  valign = 'center';
205
255
  halign = 'center';
206
256
  }
207
- store?.update(v => ({ ...v, valign, halign }));
257
+ const fvalign = valign;
258
+ const fhalign = halign;
259
+ return () => {
260
+ position?.();
261
+ store?.update(v => ({ ...v, valign: fvalign, halign: fhalign }));
262
+ };
263
+ }
264
+ function invalidateAndSchedule() {
265
+ // A mutation or resize may have changed which ancestor is the containing block.
266
+ fixedParentComputed = false;
267
+ scheduleGlueFrame();
208
268
  }
209
- const { destroy: watchDestroy } = watchForMutations(reposition);
210
- document.addEventListener('scroll', reposition, { capture: true });
269
+ const { destroy: watchDestroy } = watchForMutations(invalidateAndSchedule);
270
+ registerGlue(measure);
211
271
  return {
212
272
  update({ target: utarget, align: ualign = 'auto', cover: ucover = false, gap: ugap = 0, store: ustore }) {
213
273
  align = ualign;
@@ -223,13 +283,14 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
223
283
  el.style.removeProperty('position');
224
284
  else {
225
285
  el.style.position = 'fixed';
226
- reposition();
286
+ fixedParentComputed = false;
227
287
  }
288
+ scheduleGlueFrame();
228
289
  }
229
290
  },
230
291
  destroy() {
292
+ unregisterGlue(measure);
231
293
  watchDestroy();
232
- document.removeEventListener('scroll', reposition);
233
294
  if (adjustparentheight && parent) {
234
295
  if (formerMinHeight)
235
296
  parent.style.minHeight = formerMinHeight;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@txstate-mws/svelte-components",
3
- "version": "1.6.14",
3
+ "version": "1.6.15",
4
4
  "description": "Svelte components that are generically useful.",
5
5
  "scripts": {
6
6
  "prepublishOnly": "svelte-package",
@@ -29,7 +29,7 @@
29
29
  "@sveltejs/package": "^2.0.2",
30
30
  "eslint-config-love": "^154.0.0",
31
31
  "eslint-plugin-svelte": "^3.0.0",
32
- "svelte": "^5.0.0",
32
+ "svelte": "^5.56.3",
33
33
  "svelte-check": "^4.0.0",
34
34
  "tslib": "^2.3.1",
35
35
  "typescript": "^6.0.3"