@cocreate/position 1.16.0 → 2.0.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.
@@ -0,0 +1,213 @@
1
+ import { dispatch } from "./helpers/events.js";
2
+
3
+ const edgeSize = 48;
4
+ const maximumSpeed = 800;
5
+ const scrollOverflow = /^(auto|scroll|overlay)$/;
6
+
7
+ function parentFrame(document) {
8
+ try { return document.defaultView?.frameElement || null; }
9
+ catch { return null; }
10
+ }
11
+
12
+ function toParent(point, frame) {
13
+ const rect = frame.getBoundingClientRect();
14
+ return {
15
+ document: frame.ownerDocument,
16
+ x: rect.left + (frame.clientLeft + point.x) * (frame.offsetWidth ? rect.width / frame.offsetWidth : 1),
17
+ y: rect.top + (frame.clientTop + point.y) * (frame.offsetHeight ? rect.height / frame.offsetHeight : 1)
18
+ };
19
+ }
20
+
21
+ function toChild(point, frame, document) {
22
+ const rect = frame.getBoundingClientRect();
23
+ return {
24
+ document,
25
+ x: (point.x - rect.left) / (frame.offsetWidth ? rect.width / frame.offsetWidth : 1) - frame.clientLeft,
26
+ y: (point.y - rect.top) / (frame.offsetHeight ? rect.height / frame.offsetHeight : 1) - frame.clientTop
27
+ };
28
+ }
29
+
30
+ function viewport(document) {
31
+ return {
32
+ left: 0, top: 0,
33
+ right: document.documentElement.clientWidth,
34
+ bottom: document.documentElement.clientHeight
35
+ };
36
+ }
37
+
38
+ function visibleRect(element, document) {
39
+ const bounds = viewport(document);
40
+ for (let node = element; node && node !== document.scrollingElement; node = node.parentElement) {
41
+ const style = document.defaultView.getComputedStyle(node);
42
+ const rect = node.getBoundingClientRect();
43
+ const scaleX = node.offsetWidth ? rect.width / node.offsetWidth : 1;
44
+ const scaleY = node.offsetHeight ? rect.height / node.offsetHeight : 1;
45
+ if (node === element || style.overflowX !== "visible") {
46
+ bounds.left = Math.max(bounds.left, rect.left + node.clientLeft * scaleX);
47
+ bounds.right = Math.min(bounds.right, rect.left + (node.clientLeft + node.clientWidth) * scaleX);
48
+ }
49
+ if (node === element || style.overflowY !== "visible") {
50
+ bounds.top = Math.max(bounds.top, rect.top + node.clientTop * scaleY);
51
+ bounds.bottom = Math.min(bounds.bottom, rect.top + (node.clientTop + node.clientHeight) * scaleY);
52
+ }
53
+ }
54
+ return bounds;
55
+ }
56
+
57
+ function edgeVelocity(point, rect) {
58
+ if (point.x < rect.left || point.x > rect.right || point.y < rect.top || point.y > rect.bottom)
59
+ return 0;
60
+ const edge = Math.min(edgeSize, (rect.bottom - rect.top) / 3);
61
+ if (edge <= 0) return 0;
62
+ const top = Math.max(0, 1 - (point.y - rect.top) / edge);
63
+ const bottom = Math.max(0, 1 - (rect.bottom - point.y) / edge);
64
+ return maximumSpeed * (bottom * bottom - top * top);
65
+ }
66
+
67
+ /** Scroll the containers under an active drag pointer, including same-origin frames. */
68
+ export function createAutoScroll({ element, isActive, onScroll, onCancel }) {
69
+ const ownerDocument = element.ownerDocument;
70
+ let rootDocument = ownerDocument;
71
+ const ownerFrames = [];
72
+ for (let frame; (frame = parentFrame(rootDocument)); rootDocument = frame.ownerDocument)
73
+ ownerFrames.push(frame);
74
+ const view = rootDocument.defaultView;
75
+ const controller = new AbortController();
76
+ const documents = new Set();
77
+ let pointer = null, animation = null, previousTime = null;
78
+ let scrollTarget = null, remainder = 0, direction = 0, stopped = false;
79
+
80
+ function update(point) {
81
+ if (stopped || !Number.isFinite(point?.x) || !Number.isFinite(point?.y)) return;
82
+ let current = point;
83
+ while (current.document !== rootDocument) {
84
+ const frame = parentFrame(current.document);
85
+ if (!frame) return;
86
+ current = toParent(current, frame);
87
+ }
88
+ pointer = current;
89
+ }
90
+
91
+ function stop() {
92
+ if (stopped) return;
93
+ stopped = true;
94
+ controller.abort();
95
+ if (animation !== null) view.cancelAnimationFrame(animation);
96
+ animation = null;
97
+ pointer = null;
98
+ documents.clear();
99
+ }
100
+
101
+ function cancel() {
102
+ if (stopped) return;
103
+ stop();
104
+ onCancel();
105
+ }
106
+
107
+ function watch(document) {
108
+ if (documents.has(document)) return;
109
+ documents.add(document);
110
+ const options = { capture: true, signal: controller.signal };
111
+ const move = event => {
112
+ const point = event.touches?.[0] || event;
113
+ update({ x: point.clientX, y: point.clientY, document });
114
+ };
115
+ document.addEventListener("mousemove", move, options);
116
+ document.addEventListener("touchmove", move, options);
117
+ for (const event of ["mouseup", "touchend"])
118
+ document.addEventListener(event, stop, options);
119
+ document.addEventListener("touchcancel", cancel, options);
120
+ document.defaultView.addEventListener("blur", cancel, { signal: controller.signal });
121
+ document.addEventListener("visibilitychange", () => {
122
+ if (document.hidden) cancel();
123
+ }, { signal: controller.signal });
124
+ }
125
+
126
+ function targetsAt(point) {
127
+ watch(point.document);
128
+ const targets = [];
129
+ const target = point.document.elementsFromPoint(point.x, point.y).find(node =>
130
+ node !== element && !element.contains(node) &&
131
+ !node.closest(".position-moving-preview, [position-marker]"));
132
+ if (!target) return targets;
133
+ if (target.localName === "iframe") {
134
+ try {
135
+ const document = target.contentDocument;
136
+ if (document?.documentElement)
137
+ targets.push(...targetsAt(toChild(point, target, document)));
138
+ } catch { /* Cross-origin frames keep their own scrolling boundary. */ }
139
+ }
140
+ targets.push({ ...point, element: target });
141
+ return targets;
142
+ }
143
+
144
+ function findScroll(targets) {
145
+ for (const point of targets) {
146
+ const document = point.document;
147
+ const root = document.scrollingElement;
148
+ const candidates = [];
149
+ for (let node = point.element; node; node = node.parentElement) {
150
+ if (node !== root) candidates.push(node);
151
+ }
152
+ if (root) candidates.push(root);
153
+ for (const node of candidates) {
154
+ let overflow = document.defaultView.getComputedStyle(node).overflowY;
155
+ if (node === root && overflow === "visible" && document.body)
156
+ overflow = document.defaultView.getComputedStyle(document.body).overflowY;
157
+ if (node === root ? /^(hidden|clip)$/.test(overflow) : !scrollOverflow.test(overflow)) continue;
158
+ if (node.scrollHeight <= node.clientHeight) continue;
159
+ const velocity = edgeVelocity(point, visibleRect(node, document));
160
+ if (velocity < 0 ? node.scrollTop > 0 : velocity > 0 &&
161
+ node.scrollTop < node.scrollHeight - node.clientHeight - 1)
162
+ return { node, velocity };
163
+ }
164
+ }
165
+ return null;
166
+ }
167
+
168
+ function tick(time) {
169
+ try { scrollFrame(time); }
170
+ catch (error) {
171
+ cancel();
172
+ dispatch(element, "position-error", { error, feature: "auto-scroll" });
173
+ }
174
+ }
175
+
176
+ function scrollFrame(time) {
177
+ animation = null;
178
+ if (stopped || !isActive()) return stop();
179
+ if (!element.isConnected || ownerFrames.some(frame => !frame.isConnected)) return cancel();
180
+ const elapsed = previousTime === null ? 1 / 60 : Math.min(time - previousTime, 32) / 1000;
181
+ previousTime = time;
182
+ if (pointer) {
183
+ const scrolling = findScroll(targetsAt(pointer));
184
+ if (scrolling) {
185
+ const { node, velocity } = scrolling;
186
+ if (scrollTarget !== node || direction !== Math.sign(velocity)) remainder = 0;
187
+ scrollTarget = node;
188
+ direction = Math.sign(velocity);
189
+ remainder += velocity * elapsed;
190
+ const amount = Math.trunc(remainder);
191
+ remainder -= amount;
192
+ const before = node.scrollTop;
193
+ if (amount) node.scrollBy({ top: amount, left: 0, behavior: "instant" });
194
+ if (node.scrollTop !== before) {
195
+ const reference = targetsAt(pointer)[0];
196
+ let ownerPoint = pointer;
197
+ for (const frame of [...ownerFrames].reverse())
198
+ ownerPoint = toChild(ownerPoint, frame, frame.contentDocument);
199
+ if (reference) onScroll({ clientX: ownerPoint.x, clientY: ownerPoint.y }, reference);
200
+ }
201
+ } else {
202
+ scrollTarget = null;
203
+ remainder = 0;
204
+ }
205
+ }
206
+ if (!stopped) animation = view.requestAnimationFrame(tick);
207
+ }
208
+
209
+ watch(rootDocument);
210
+ watch(ownerDocument);
211
+ animation = view.requestAnimationFrame(tick);
212
+ return { update, stop };
213
+ }