@cocreate/position 1.16.0 → 2.0.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/src/position.js CHANGED
@@ -1,482 +1,443 @@
1
- function position(el, container, options) {
2
- if (!(el && el.nodeType && el.nodeType === 1)) {
3
- return;
4
- }
5
-
6
- /** define constant **/
7
- this.dragAreaHeight = 50;
8
- this.MARGIN = 5;
9
- this.FULLSCREEN_MARGIN = -60;
10
- this.NO_SNAP = -100;
11
- this.SNAP_MARGIN = -10;
12
- this.RIGHT_SCROL = 5;
13
- this.dragHandle = el.querySelector('[drag-handle]')
14
- this.container = container;
15
- if (!this.container.positionIndex)
16
- this.container.positionIndex = 10
17
-
18
- this.el = el;
19
- this.boundStatus = {};
20
-
21
- this.selectedElement;
22
- // this.selectedClass = "selectedPosition";
23
- /**
24
- * x: mouse x in element
25
- * y: mouse y in element
26
- * cx: x in document
27
- * cy: y in document
28
- * */
29
- this.point = {};
30
- this.rect = {};
31
- this.clickedInfo = null;
32
- this.preSnapped = null;
33
- this.isSnap = false;
34
-
35
- this._createGhost(this.container.getAttribute("data-ghost-class"));
36
- this._initEvent();
37
- this._selectElement()
38
- }
39
-
40
- const INTERACTIVE_SELECTOR = [
41
- "a",
42
- "button",
43
- "input",
44
- "select",
45
- "textarea",
46
- "label",
47
- "summary",
48
- "[contenteditable]:not([contenteditable='false'])",
49
- "[moveable-ignore]"
50
- ].join(",");
51
-
52
- position.prototype = {
53
- constructor: position,
54
-
55
- _initEvent: function () {
56
- let self = this;
57
- this.iframe = this.el.querySelector('iframe')
58
- if (this.iframe) {
59
- this.iframe.addEventListener('load', function () {
60
- const iframeContent = self.iframe.contentDocument;
61
- iframeContent.addEventListener('mousedown', function (e) {
62
- self._selectElement(e.detail.element)
63
- })
64
- });
65
- }
66
-
67
- this.__setRectInfo();
68
-
69
- let positionableEl = this.el
70
- if (this.dragHandle)
71
- positionableEl = this.dragHandle
72
-
73
- this._documentMouseMove = function (e) {
74
- if (!self.clickedInfo?.isMoving) return;
75
- e.preventDefault();
76
- self._onMove(e);
77
- };
78
- this._documentMouseUp = function (e) {
79
- if (!self.clickedInfo?.isMoving) return;
80
- self._onUp(e);
81
- e.preventDefault();
82
- };
83
- this._documentTouchMove = function (e) {
84
- if (!self.clickedInfo?.isMoving || !e.touches.length) return;
85
- e.preventDefault();
86
- self._onMove(e.touches[0]);
87
- };
88
- this._documentTouchEnd = function (e) {
89
- if (!self.clickedInfo?.isMoving) return;
90
- self._onUp(e.changedTouches?.[0]);
91
- };
92
- this._documentBlur = function () {
93
- self._onUp();
94
- };
95
-
96
- positionableEl.addEventListener('touchstart', function (e) {
97
- self._selectElement(e.detail.element)
98
- self._onDown(e.touches[0]);
99
- if (self.clickedInfo?.isMoving) self._bindDocumentDrag();
100
- })
101
-
102
- positionableEl.addEventListener('mousedown', function (e) {
103
- self._selectElement(e.detail.element)
104
- self._onDown(e);
105
- if (self.clickedInfo?.isMoving) self._bindDocumentDrag();
106
- })
107
-
108
- },
109
-
110
- _bindDocumentDrag: function () {
111
- this._unbindDocumentDrag();
112
- document.addEventListener('mousemove', this._documentMouseMove);
113
- document.addEventListener('mouseup', this._documentMouseUp, true);
114
- document.addEventListener('touchmove', this._documentTouchMove, { passive: false });
115
- document.addEventListener('touchend', this._documentTouchEnd, true);
116
- document.addEventListener('touchcancel', this._documentTouchEnd, true);
117
- window.addEventListener('blur', this._documentBlur);
118
- },
119
-
120
- _unbindDocumentDrag: function () {
121
- document.removeEventListener('mousemove', this._documentMouseMove);
122
- document.removeEventListener('mouseup', this._documentMouseUp, true);
123
- document.removeEventListener('touchmove', this._documentTouchMove);
124
- document.removeEventListener('touchend', this._documentTouchEnd, true);
125
- document.removeEventListener('touchcancel', this._documentTouchEnd, true);
126
- window.removeEventListener('blur', this._documentBlur);
127
- },
128
-
129
- _isMovable() {
130
- if (this.dragHandle)
131
- return true
132
- let width = this.rect.width;
133
- return this.point.x > 0 && this.point.x < width && this.point.y > 0 && this.point.y < this.dragAreaHeight;
134
- },
135
-
136
- place: function (anchor = this.el, options = {}) {
137
- if (!anchor?.getBoundingClientRect) return;
138
-
139
- const placement = options.placement ||
140
- anchor.getAttribute("position-placement") ||
141
- this.el.getAttribute("position-placement") ||
142
- "bottom-start";
143
- const offset = Number(options.offset ??
144
- anchor.getAttribute("position-offset") ??
145
- this.el.getAttribute("position-offset") ?? 8);
146
- const padding = Number(options.padding ??
147
- anchor.getAttribute("position-padding") ??
148
- this.el.getAttribute("position-padding") ?? 8);
149
- const collision = (options.collision ||
150
- anchor.getAttribute("position-collision") ||
151
- this.el.getAttribute("position-collision") ||
152
- "flip shift").split(/[\s,]+/);
153
-
154
- const anchorRect = anchor.getBoundingClientRect();
155
- const elementRect = this.el.getBoundingClientRect();
156
- const parentRect = this.container.getBoundingClientRect();
157
- const isFixed = getComputedStyle(this.el).position === "fixed";
158
- const boundary = isFixed
159
- ? { left: 0, top: 0, right: innerWidth, bottom: innerHeight }
160
- : parentRect;
161
- let [side, align = "center"] = placement.split("-");
162
-
163
- const getCoordinates = (currentSide) => {
164
- let left;
165
- let top;
166
- if (currentSide === "top" || currentSide === "bottom") {
167
- top = currentSide === "top"
168
- ? anchorRect.top - elementRect.height - offset
169
- : anchorRect.bottom + offset;
170
- left = align === "start"
171
- ? anchorRect.left
172
- : align === "end"
173
- ? anchorRect.right - elementRect.width
174
- : anchorRect.left + (anchorRect.width - elementRect.width) / 2;
175
- } else {
176
- left = currentSide === "left"
177
- ? anchorRect.left - elementRect.width - offset
178
- : anchorRect.right + offset;
179
- top = align === "start"
180
- ? anchorRect.top
181
- : align === "end"
182
- ? anchorRect.bottom - elementRect.height
183
- : anchorRect.top + (anchorRect.height - elementRect.height) / 2;
184
- }
185
- return { left, top };
186
- };
187
-
188
- let coordinates = getCoordinates(side);
189
- if (collision.includes("flip")) {
190
- const opposite = { top: "bottom", bottom: "top", left: "right", right: "left" };
191
- const overflows =
192
- (side === "top" && coordinates.top < boundary.top + padding) ||
193
- (side === "bottom" && coordinates.top + elementRect.height > boundary.bottom - padding) ||
194
- (side === "left" && coordinates.left < boundary.left + padding) ||
195
- (side === "right" && coordinates.left + elementRect.width > boundary.right - padding);
196
- if (overflows) {
197
- side = opposite[side] || side;
198
- coordinates = getCoordinates(side);
199
- }
200
- }
201
-
202
- if (collision.includes("shift")) {
203
- coordinates.left = Math.min(
204
- Math.max(coordinates.left, boundary.left + padding),
205
- Math.max(boundary.left + padding, boundary.right - elementRect.width - padding)
206
- );
207
- coordinates.top = Math.min(
208
- Math.max(coordinates.top, boundary.top + padding),
209
- Math.max(boundary.top + padding, boundary.bottom - elementRect.height - padding)
210
- );
211
- }
212
-
213
- this.el.style.right = "auto";
214
- this.el.style.bottom = "auto";
215
- this.el.style.left = (isFixed
216
- ? coordinates.left
217
- : coordinates.left - parentRect.left + this.container.scrollLeft) + "px";
218
- this.el.style.top = (isFixed
219
- ? coordinates.top
220
- : coordinates.top - parentRect.top + this.container.scrollTop) + "px";
221
- this.isSnap = false;
222
- this.__setRectInfo();
223
- this.createEvent("element-positioned");
224
- },
225
-
226
- _onDown: function (e) {
227
- // A title-bar style drag handle may contain controls. Those controls
228
- // must retain their native click, focus, and file-picker behavior.
229
- const interactive = e.target?.closest?.(INTERACTIVE_SELECTOR);
230
- if (this.dragHandle && interactive && this.dragHandle.contains(interactive)) {
231
- this.clickedInfo = null;
232
- return;
233
- }
234
-
235
- this.__getBoundStatus(e);
236
-
237
- this.clickedInfo = {
238
- x: this.point.x,
239
- y: this.point.y,
240
- cx: this.point.cx,
241
- cy: this.point.cy,
242
- w: this.rect.width,
243
- h: this.rect.height,
244
- isMoving: this._isMovable(),
245
- boundStatus: this.boundStatus,
246
- isChangeStart: true
247
- }
248
- },
249
-
250
- _onMove: function (e) {
251
- this.__getBoundStatus(e)
252
- this.__animate();
253
- },
254
-
255
- _onUp: function (e) {
256
- this._unbindDocumentDrag();
257
- if (!this.clickedInfo) {
258
- return;
259
- }
260
- if (e) {
261
- this.__getBoundStatus(e);
262
- }
263
- if (this.clickedInfo.isMoving && !this.isParked) {
264
- let p_w = this.el.parentNode.offsetWidth, p_h = this.el.parentNode.offsetHeight;
265
- let snap_info = null;
266
-
267
- if (this.__between(this.rect.top, this.NO_SNAP, this.FULLSCREEN_MARGIN) ||
268
- this.__between(this.rect.left, this.NO_SNAP, this.FULLSCREEN_MARGIN) ||
269
- this.__between(p_w - this.rect.right, this.NO_SNAP, this.FULLSCREEN_MARGIN) ||
270
- this.__between(p_h - this.rect.bottom, this.NO_SNAP, this.FULLSCREEN_MARGIN)) {
271
- snap_info = { x: 0, y: 0, w: '100%', h: '100%' }
272
- } else if (this.__between(this.rect.top, this.NO_SNAP, this.SNAP_MARGIN)) {
273
- snap_info = { x: 0, y: 0, w: '100%', h: '50%' }
274
- } else if (this.__between(this.rect.left, this.NO_SNAP, this.SNAP_MARGIN)) {
275
- snap_info = { x: 0, y: 0, w: '50%', h: '100%' }
276
- } else if (this.__between(p_w - this.rect.right, this.NO_SNAP, this.SNAP_MARGIN)) {
277
- snap_info = { x: '50%', y: 0, w: '50%', h: '100%' }
278
- } else if (this.__between(p_h - this.rect.bottom, this.NO_SNAP, this.SNAP_MARGIN)) {
279
- snap_info = { x: 0, y: '50%', w: '100%', h: '50%' }
280
- }
281
-
282
- if (snap_info && !this.isSnap) {
283
- this.__setBound(this.el, snap_info.x, snap_info.y, snap_info.w, snap_info.h);
284
- this.preSnapped = { x: this.rect.x, y: this.rect.y, width: this.rect.width, height: this.rect.height };
285
- this.isSnap = true;
286
- }
287
-
288
- let ghost_info = {
289
- x: this.rect.x,
290
- y: this.rect.y,
291
- w: this.rect.width,
292
- h: this.rect.height
293
- }
294
-
295
- this._ghostProcess(ghost_info)
296
- }
297
-
298
- if (this.clickedInfo.isMoving) {
299
- this.createEvent('element-moveend');
300
- }
301
-
302
-
303
- this.clickedInfo = null;
304
- },
305
-
306
- __setBound: function (el, x, y, w, h) {
307
- el.style.left = x;
308
- el.style.top = y;
309
- el.style.width = w;
310
- el.style.height = h;
311
- },
312
-
313
- __setRectInfo: function () {
314
- let bound = this.el.getBoundingClientRect();
315
- let parentRect = this.el.parentNode.getBoundingClientRect();
316
- this.rect = {};
317
- this.rect.x = bound.x - parentRect.x;
318
- this.rect.y = bound.y - parentRect.y;
319
- this.rect.width = bound.width;
320
- this.rect.height = bound.height;
321
- this.rect.top = bound.top - parentRect.top;
322
- this.rect.bottom = bound.bottom - parentRect.top;
323
- this.rect.left = bound.left - parentRect.left;
324
- this.rect.right = bound.right - parentRect.left;
325
- },
326
-
327
- __getBoundStatus: function (e) {
328
- let bound = this.el.getBoundingClientRect();
329
- let parentRect = this.el.parentNode.getBoundingClientRect();
330
- let x = e.clientX - bound.left;// - parentRect.left;
331
- let y = e.clientY - bound.top;// - parentRect.top;
332
-
333
- this.__setRectInfo();
334
-
335
- this.point.x = x;
336
- this.point.y = y;
337
- this.point.cx = e.clientX - parentRect.left;
338
- this.point.cy = e.clientY - parentRect.top;
339
-
340
- this.boundStatus = {
341
- isTop: y < this.MARGIN && y > -this.MARGIN,
342
- isLeft: x < this.MARGIN && x > -this.MARGIN,
343
- isRight: x >= bound.width - this.RIGHT_SCROL && x <= bound.width + this.MARGIN + (this.MARGIN - this.RIGHT_SCROL),
344
- isBottom: y >= bound.height - this.MARGIN && y <= bound.height + this.MARGIN
345
- }
346
-
347
- return this.boundStatus;
348
- },
349
-
350
- __between: function (x, min, max) {
351
- return x >= min && x <= max;
352
- },
353
-
354
- __animate: function () {
355
- let c_info = this.clickedInfo;
356
-
357
- let eventName = null;
358
-
359
- if (c_info && c_info.isMoving) {
360
- /**
361
- * Ghost Process
362
- **/
363
-
364
- let p_w = this.el.parentNode.offsetWidth, p_h = this.el.parentNode.offsetHeight;
365
- let ghost_info = null;
366
-
367
- if (this.__between(this.rect.top, this.NO_SNAP, this.FULLSCREEN_MARGIN) ||
368
- this.__between(this.rect.left, this.NO_SNAP, this.FULLSCREEN_MARGIN) ||
369
- this.__between(p_w - this.rect.right, this.NO_SNAP, this.FULLSCREEN_MARGIN) ||
370
- this.__between(p_h - this.rect.bottom, this.NO_SNAP, this.FULLSCREEN_MARGIN)) {
371
- ghost_info = { x: 0, y: 0, w: p_w, h: p_h, type: "show" }
372
- } else if (this.__between(this.rect.top, this.NO_SNAP, this.SNAP_MARGIN)) {
373
- ghost_info = { x: 0, y: 0, w: p_w, h: p_h / 2, type: "show" }
374
- } else if (this.__between(this.rect.left, this.NO_SNAP, this.SNAP_MARGIN)) {
375
- ghost_info = { x: 0, y: 0, w: p_w / 2, h: p_h, type: "show" }
376
- } else if (this.__between(p_w - this.rect.right, this.NO_SNAP, this.SNAP_MARGIN)) {
377
- ghost_info = { x: p_w / 2, y: 0, w: p_w / 2, h: p_h, type: "show" }
378
- } else if (this.__between(p_h - this.rect.bottom, this.NO_SNAP, this.SNAP_MARGIN)) {
379
- ghost_info = { x: 0, y: p_h / 2, w: p_w, h: p_h / 2, type: "show" }
380
- } else {
381
- ghost_info = { x: this.rect.left, y: this.rect.top, w: this.rect.width, h: this.rect.height, type: "hide" }
382
- }
383
-
384
- if (ghost_info && !this.isParked && !this.isSnap) {
385
- this._ghostProcess(ghost_info)
386
- }
387
-
388
- if (this.isSnap) {
389
- this.el.style.left = this.point.cx + 'px';
390
- this.el.style.top = this.point.cy + 'px';
391
- this.el.style.width = this.preSnapped.width + 'px';
392
- this.el.style.height = this.preSnapped.height + 'px';
393
- this.isSnap = false;
394
- } else {
395
- this.el.style.top = (this.point.cy - c_info.y) + 'px';
396
- this.el.style.left = (this.point.cx - c_info.x) + 'px';
397
- }
398
-
399
- eventName = "element-moving";
400
- if (c_info.isChangeStart) {
401
- this.clickedInfo.isChangeStart = false;
402
- eventName = "element-movestart";
403
- }
404
-
405
- this.createEvent(eventName)
406
- return;
407
- }
408
- },
409
-
410
- minMax() {
411
- if (!this.isSnap) {
412
- this.isSnap = true;
413
- this.preSnapped = { x: this.rect.x, y: this.rect.y, width: this.rect.width, height: this.rect.height };
414
- this.__setBound(this.el, 0, 0, "100%", "100%");
415
- } else {
416
- this.isSnap = false;
417
- this.el.style.left = this.preSnapped.x + 'px';
418
- this.el.style.top = this.preSnapped.y + 'px';
419
- this.el.style.width = this.preSnapped.width + 'px';
420
- this.el.style.height = this.preSnapped.height + 'px';
421
- }
422
- },
423
-
424
-
425
- /**
426
- * Position Events
427
- * move: element-moving, element-moveend, element-movestart
428
- **/
429
- createEvent(eventName) {
430
- const event = new CustomEvent(eventName, { detail: { element: this.el, instance: this } });
431
- this.el.dispatchEvent(event);
432
- },
433
-
434
- _createGhost: function (ghostClass) {
435
- let node = document.createElement("div");
436
- if (!ghostClass) {
437
- ghostClass = "snap-ghost";
438
- }
439
-
440
- node.classList.add(ghostClass);
441
- this.container.appendChild(node);
442
- this.ghostEl = node;
443
- },
444
-
445
- _ghostProcess: function (info) {
446
- this.ghostEl.style.left = info.x + 'px';
447
- this.ghostEl.style.top = info.y + 'px';
448
- this.ghostEl.style.width = info.w + 'px';
449
- this.ghostEl.style.height = info.h + 'px';
450
-
451
- if (info.type === "show") {
452
- if (this.ghostEl.style.display === 'none') {
453
- this.ghostEl.style.display = 'block'
454
- }
455
- this.ghostEl.style.opacity = 0.2;
456
- } else {
457
- this.ghostEl.style.opacity = 0;
458
- if (this.ghostEl.style.display !== 'none') {
459
- this.ghostEl.style.display = 'none'
460
- }
461
- }
462
- return;
463
- },
464
-
465
- _selectElement: function (el) {
466
- this._releaseSelect();
467
- this.container['selectedPositionElement'] = this.el
468
- if (this.el) {
469
- this.createEvent('elementSelected');
470
- this.el.style.zIndex = this.container.positionIndex += 1;
471
- this.el.classList.add('selectedPosition');
472
- }
473
- },
474
-
475
- _releaseSelect: function () {
476
- let el = this.container['selectedPositionElement']
477
- if (el)
478
- el.classList.remove("selectedPosition");
479
- },
480
- }
481
-
482
- export default position;
1
+ import observer from "@cocreate/observer";
2
+ import { queryElements } from "@cocreate/utils";
3
+ import { relayEvents } from "@cocreate/utils/iframe";
4
+ import { resolveEventOperator } from "./eventOperator.js";
5
+ import { dispatch } from "./helpers/events.js";
6
+
7
+ const placementStates = new WeakMap();
8
+ const triggerElements = new WeakMap();
9
+ const positionEventBindings = new WeakMap();
10
+ const positionFrames = new WeakMap();
11
+ const pendingPositionEvents = new WeakMap();
12
+ const positionConfigSelector = "[position-query], [position-events]";
13
+
14
+ function getQueryAttribute(element, name) {
15
+ return element?.getAttribute?.(name) ?? null;
16
+ }
17
+
18
+ function getPositionEvents(value) {
19
+ const aliases = { hover: ["mouseover", "mousemove", "mouseout"] };
20
+ return [...new Set((value || "").split(",")
21
+ .flatMap(event => aliases[event.trim().toLowerCase()] || [event.trim()])
22
+ .filter(Boolean))];
23
+ }
24
+
25
+ export function initPositionTargets(elements) {
26
+ for (const element of elements) attachPlacement(element);
27
+ }
28
+
29
+ function attachPlacement(element) {
30
+ if (!element?.matches) return;
31
+ const position = element.position || (element.position = {
32
+ el: element,
33
+ container: element.parentElement
34
+ });
35
+ position.el = element;
36
+ position.place = (anchor = element, options = {}) => place(element, anchor, options);
37
+ position.unplace = () => {
38
+ unplace(element);
39
+ element.hidden = true;
40
+ };
41
+ }
42
+
43
+ function getEventSnapshot(event, trigger) {
44
+ const detail = event?.detail && typeof event.detail === "object" ? event.detail : {};
45
+ const originalEvent = detail.originalEvent || event;
46
+ const target = detail.target || originalEvent?.target || event?.target || null;
47
+ const cursor = detail.cursor || (
48
+ Number.isFinite(originalEvent?.clientX) && Number.isFinite(originalEvent?.clientY)
49
+ ? { x: originalEvent.clientX, y: originalEvent.clientY }
50
+ : null
51
+ );
52
+ const sourceDocument = detail.document ||
53
+ (target?.nodeType === 9 ? target : target?.ownerDocument) ||
54
+ originalEvent?.view?.document || null;
55
+ const relayTarget = detail.originalEvent ? event.target : null;
56
+ const pointer = {};
57
+ for (const key of ["pointerId", "pointerType", "isPrimary", "button", "buttons", "pressure", "tiltX", "tiltY", "twist", "clientX", "clientY", "pageX", "pageY", "screenX", "screenY"]) {
58
+ if (originalEvent?.[key] !== undefined) pointer[key] = originalEvent[key];
59
+ }
60
+ const keyboard = {};
61
+ for (const key of ["key", "code", "repeat", "altKey", "ctrlKey", "metaKey", "shiftKey"]) {
62
+ if (originalEvent?.[key] !== undefined) keyboard[key] = originalEvent[key];
63
+ }
64
+ const input = {};
65
+ for (const key of ["inputType", "data", "dataTransfer"]) {
66
+ if (originalEvent?.[key] !== undefined) input[key] = originalEvent[key];
67
+ }
68
+
69
+ return {
70
+ type: event?.type || originalEvent?.type || "",
71
+ target,
72
+ relatedTarget: detail.relatedTarget || originalEvent?.relatedTarget || null,
73
+ relayTarget,
74
+ frameElement: sourceDocument?.defaultView?.frameElement ||
75
+ (relayTarget?.tagName === "IFRAME" ? relayTarget : null),
76
+ currentTarget: trigger,
77
+ document: sourceDocument,
78
+ timeStamp: originalEvent?.timeStamp ?? event?.timeStamp ?? null,
79
+ cursor,
80
+ pointer,
81
+ keyboard,
82
+ input,
83
+ detail,
84
+ originalEvent
85
+ };
86
+ }
87
+
88
+ function getRectSnapshot(rect) {
89
+ if (!rect) return null;
90
+ return {
91
+ x: rect.left,
92
+ y: rect.top,
93
+ top: rect.top,
94
+ right: rect.right,
95
+ bottom: rect.bottom,
96
+ left: rect.left,
97
+ width: rect.width,
98
+ height: rect.height
99
+ };
100
+ }
101
+
102
+ function updatePositionContext(element, context) {
103
+ attachPlacement(element);
104
+ Object.assign(element.position, {
105
+ element: context.queryElement,
106
+ trigger: context.trigger,
107
+ event: context.event,
108
+ query: context.query,
109
+ queryElements: context.queryElements,
110
+ box: {
111
+ element,
112
+ type: context.boxType,
113
+ rect: context.rect || null
114
+ },
115
+ active: context.active
116
+ });
117
+ }
118
+
119
+ function place(element, anchor, options = {}) {
120
+ if (!anchor?.getBoundingClientRect) return;
121
+ const anchorDocument = anchor.ownerDocument ||
122
+ anchor.startContainer?.ownerDocument || anchor.commonAncestorContainer?.ownerDocument;
123
+ if (!anchorDocument) return;
124
+ const targetDocument = element.ownerDocument;
125
+ const boxType = options.box || getQueryAttribute(element, "position-box") || "element";
126
+ const rect = getPositionBox(anchor, boxType, targetDocument);
127
+ if (!rect) return false;
128
+ element.hidden = false;
129
+
130
+ let state = placementStates.get(element);
131
+ if (!state) placementStates.set(element, state = {});
132
+ state.options = options;
133
+ if (state.anchor !== anchor || state.boxType !== boxType) {
134
+ unplace(element);
135
+ state.anchor = anchor;
136
+ state.boxType = boxType;
137
+ state.documents = getDocumentChain(anchorDocument, targetDocument);
138
+ state.update = () => {
139
+ if (state.frame) return;
140
+ const view = targetDocument.defaultView;
141
+ state.frame = view.requestAnimationFrame(() => {
142
+ state.frame = null;
143
+ const currentAnchor = state.anchor;
144
+ const isConnected = currentAnchor?.isConnected ??
145
+ currentAnchor?.startContainer?.isConnected ??
146
+ currentAnchor?.commonAncestorContainer?.isConnected;
147
+ if (!isConnected || !place(element, currentAnchor, state.options))
148
+ element.hidden = true;
149
+ });
150
+ };
151
+ for (const document of state.documents) {
152
+ document.addEventListener("scroll", state.update, true);
153
+ document.defaultView?.addEventListener("resize", state.update);
154
+ }
155
+ }
156
+
157
+ const view = targetDocument.defaultView;
158
+ const isFixed = view.getComputedStyle(element).position === "fixed";
159
+ const offsetParent = element.offsetParent;
160
+ const parentRect = offsetParent?.getBoundingClientRect();
161
+ element.style.right = "auto";
162
+ element.style.bottom = "auto";
163
+ element.style.left = (isFixed || !parentRect
164
+ ? rect.left
165
+ : rect.left - parentRect.left - offsetParent.clientLeft + offsetParent.scrollLeft) + "px";
166
+ element.style.top = (isFixed || !parentRect
167
+ ? rect.top
168
+ : rect.top - parentRect.top - offsetParent.clientTop + offsetParent.scrollTop) + "px";
169
+ element.style.width = `${rect.width}px`;
170
+ element.style.height = `${rect.height}px`;
171
+ if (element.position) {
172
+ element.position.isSnap = false;
173
+ if (options.context) {
174
+ updatePositionContext(element, {
175
+ ...options.context,
176
+ rect: getRectSnapshot(rect),
177
+ active: true
178
+ });
179
+ } else {
180
+ updatePositionContext(element, {
181
+ trigger: null,
182
+ event: null,
183
+ query: { selector: null, elements: [anchor], element: anchor },
184
+ queryElement: anchor,
185
+ queryElements: [anchor],
186
+ boxType,
187
+ rect: getRectSnapshot(rect),
188
+ active: true
189
+ });
190
+ }
191
+ }
192
+ dispatch(element, "element-positioned");
193
+ return true;
194
+ }
195
+
196
+ function mapRectToDocument(rect, sourceDocument, targetDocument) {
197
+ let document = sourceDocument;
198
+ if (!document || !targetDocument) return null;
199
+ while (document !== targetDocument) {
200
+ const frame = document.defaultView?.frameElement;
201
+ if (!frame) return null;
202
+ const frameRect = frame.getBoundingClientRect();
203
+ const scaleX = frame.offsetWidth ? frameRect.width / frame.offsetWidth : 1;
204
+ const scaleY = frame.offsetHeight ? frameRect.height / frame.offsetHeight : 1;
205
+ rect = {
206
+ left: frameRect.left + (frame.clientLeft + rect.left) * scaleX,
207
+ top: frameRect.top + (frame.clientTop + rect.top) * scaleY,
208
+ right: frameRect.left + (frame.clientLeft + rect.right) * scaleX,
209
+ bottom: frameRect.top + (frame.clientTop + rect.bottom) * scaleY,
210
+ width: rect.width * scaleX,
211
+ height: rect.height * scaleY
212
+ };
213
+ document = frame.ownerDocument;
214
+ }
215
+ return rect;
216
+ }
217
+
218
+ function getDocumentChain(sourceDocument, targetDocument) {
219
+ const documents = [];
220
+ let document = sourceDocument;
221
+ while (document) {
222
+ documents.push(document);
223
+ if (document === targetDocument) return documents;
224
+ const frame = document.defaultView?.frameElement;
225
+ if (!frame) break;
226
+ document = frame.ownerDocument;
227
+ }
228
+ return documents;
229
+ }
230
+
231
+ function getRectInDocument(target, targetDocument) {
232
+ const sourceDocument = target.ownerDocument ||
233
+ target.startContainer?.ownerDocument || target.commonAncestorContainer?.ownerDocument;
234
+ return mapRectToDocument(target.getBoundingClientRect(), sourceDocument, targetDocument);
235
+ }
236
+
237
+ function getElementBoxInDocument(element, targetDocument) {
238
+ const rect = element.getBoundingClientRect();
239
+ const style = element.ownerDocument.defaultView.getComputedStyle(element);
240
+ const margin = name => Number.parseFloat(style[name]) || 0;
241
+ const left = rect.left - margin("marginLeft");
242
+ const top = rect.top - margin("marginTop");
243
+ const right = rect.right + margin("marginRight");
244
+ const bottom = rect.bottom + margin("marginBottom");
245
+ return mapRectToDocument({ left, top, right, bottom, width: right - left, height: bottom - top },
246
+ element.ownerDocument, targetDocument);
247
+ }
248
+
249
+ function getTextBoxInDocument(element, targetDocument) {
250
+ const document = element.ownerDocument;
251
+ const walker = document.createTreeWalker(element, document.defaultView.NodeFilter.SHOW_TEXT);
252
+ const rects = [];
253
+ let textNode;
254
+ while ((textNode = walker.nextNode())) {
255
+ if (!textNode.nodeValue) continue;
256
+ const range = document.createRange();
257
+ range.selectNodeContents(textNode);
258
+ for (const rect of range.getClientRects()) {
259
+ if (rect.width || rect.height) rects.push(rect);
260
+ }
261
+ }
262
+ if (!rects.length) return null;
263
+ const left = Math.min(...rects.map(rect => rect.left));
264
+ const top = Math.min(...rects.map(rect => rect.top));
265
+ const right = Math.max(...rects.map(rect => rect.right));
266
+ const bottom = Math.max(...rects.map(rect => rect.bottom));
267
+ return mapRectToDocument({ left, top, right, bottom, width: right - left, height: bottom - top },
268
+ document, targetDocument);
269
+ }
270
+
271
+ function getPositionBox(anchor, type, targetDocument) {
272
+ if (anchor.nodeType === 1) {
273
+ if (String(type).toLowerCase() === "text") return getTextBoxInDocument(anchor, targetDocument);
274
+ return getElementBoxInDocument(anchor, targetDocument);
275
+ }
276
+ return getRectInDocument(anchor, targetDocument);
277
+ }
278
+
279
+ function deepest(elements) {
280
+ return elements.reduce((current, element) =>
281
+ !current || current.contains(element) ? element : current, null);
282
+ }
283
+
284
+ function placeFromTrigger(trigger) {
285
+ const event = pendingPositionEvents.get(trigger) || null;
286
+ pendingPositionEvents.delete(trigger);
287
+ const anchorQuery = getQueryAttribute(trigger, "position-query");
288
+ const elementQuery = getQueryAttribute(trigger, "position-element");
289
+ const previousElements = triggerElements.get(trigger) || new Set();
290
+ if (!elementQuery) {
291
+ for (const element of previousElements) clearPlacement(element);
292
+ triggerElements.delete(trigger);
293
+ return;
294
+ }
295
+ const eventQuery = resolveEventOperator(anchorQuery, event);
296
+ const selectionOperator = eventQuery.handled ? null : anchorQuery?.match(/\$selection/);
297
+ let anchor;
298
+ let matchedElements = [];
299
+ if (eventQuery.handled) {
300
+ matchedElements = eventQuery.value
301
+ ? queryElements({ element: eventQuery.value, selector: eventQuery.selector })
302
+ : [];
303
+ anchor = matchedElements[0];
304
+ } else if (selectionOperator) {
305
+ const prefix = anchorQuery.slice(0, selectionOperator.index).trim();
306
+ const sources = prefix ? queryElements({ element: trigger, selector: prefix }) : [trigger];
307
+ matchedElements = sources;
308
+ for (const source of sources) {
309
+ const sourceDocument = source.nodeType === 9
310
+ ? source
311
+ : source.contentDocument || source.ownerDocument;
312
+ const selection = sourceDocument?.getSelection?.();
313
+ if (selection && !selection.isCollapsed && selection.rangeCount) {
314
+ anchor = selection.getRangeAt(0).cloneRange();
315
+ break;
316
+ }
317
+ }
318
+ } else {
319
+ matchedElements = anchorQuery
320
+ ? queryElements({ element: trigger, selector: anchorQuery })
321
+ : [trigger];
322
+ anchor = anchorQuery?.includes(":hover") ? deepest(matchedElements) : matchedElements[0];
323
+ }
324
+ const elements = queryElements({ element: trigger, selector: elementQuery });
325
+ const currentElements = new Set(elements);
326
+ for (const element of previousElements) {
327
+ if (!currentElements.has(element)) clearPlacement(element);
328
+ }
329
+ triggerElements.set(trigger, currentElements);
330
+ if (!elementQuery) return;
331
+ const options = { box: getQueryAttribute(trigger, "position-box") || "element" };
332
+ const context = {
333
+ trigger,
334
+ event,
335
+ query: { selector: anchorQuery, elements: matchedElements, element: anchor },
336
+ queryElement: anchor,
337
+ queryElements: matchedElements,
338
+ boxType: options.box,
339
+ rect: null,
340
+ active: Boolean(anchor)
341
+ };
342
+ options.context = context;
343
+ for (const element of elements) {
344
+ updatePositionContext(element, context);
345
+ if (!anchor || !element.position.place(anchor, options)) clearPlacement(element);
346
+ }
347
+ }
348
+
349
+ function clearPlacement(element) {
350
+ if (!element) return;
351
+ element.position?.unplace?.();
352
+ element.hidden = true;
353
+ if (element.position) element.position.active = false;
354
+ }
355
+
356
+ function schedulePositionUpdate(element, event) {
357
+ pendingPositionEvents.set(element, event);
358
+ if (positionFrames.has(element)) return;
359
+ const frame = element.ownerDocument.defaultView.requestAnimationFrame(() => {
360
+ positionFrames.delete(element);
361
+ placeFromTrigger(element);
362
+ });
363
+ positionFrames.set(element, frame);
364
+ }
365
+
366
+ function initPositionEvents(element) {
367
+ const value = getQueryAttribute(element, "position-events")?.trim() || "";
368
+ const existing = positionEventBindings.get(element);
369
+ if (!element.hasAttribute("position-query")) {
370
+ existing?.remove();
371
+ positionEventBindings.delete(element);
372
+ return;
373
+ }
374
+ const key = value;
375
+ if (existing?.key === key) return;
376
+ existing?.remove();
377
+ const events = getPositionEvents(value);
378
+ if (!events.length) {
379
+ positionEventBindings.delete(element);
380
+ return;
381
+ }
382
+ const update = event => schedulePositionUpdate(element, getEventSnapshot(event, element));
383
+ for (const event of events) element.addEventListener(event, update, true);
384
+ positionEventBindings.set(element, {
385
+ key,
386
+ remove: () => events.forEach(event => element.removeEventListener(event, update, true))
387
+ });
388
+ }
389
+
390
+ function unplace(element) {
391
+ const state = placementStates.get(element);
392
+ if (!state?.update) return;
393
+ for (const document of state.documents || []) {
394
+ document.removeEventListener("scroll", state.update, true);
395
+ document.defaultView?.removeEventListener("resize", state.update);
396
+ }
397
+ if (state.frame) element.ownerDocument.defaultView?.cancelAnimationFrame(state.frame);
398
+ state.anchor = null;
399
+ state.boxType = null;
400
+ state.documents = null;
401
+ state.update = null;
402
+ state.frame = null;
403
+ }
404
+
405
+ function initTriggers(elements) {
406
+ for (const element of elements) initPositionEvents(element);
407
+ }
408
+
409
+ export function init() {
410
+ refresh(document.documentElement);
411
+ relayEvents(document.querySelectorAll("iframe"));
412
+ }
413
+
414
+ export function refresh(node) {
415
+ if (node?.nodeType !== 1) return;
416
+ initTriggers([node]);
417
+ initTriggers(node.querySelectorAll(positionConfigSelector));
418
+ }
419
+
420
+ observer.init({
421
+ name: "positionIframeEventRelayAddedNodes",
422
+ types: ["addedNodes"],
423
+ selector: "iframe",
424
+ callback: mutation => relayEvents([mutation.target])
425
+ });
426
+
427
+ observer.init({
428
+ name: "positionPlacementAddedNodes",
429
+ types: ["addedNodes"],
430
+ selector: positionConfigSelector,
431
+ callback: mutation => refresh(mutation.target)
432
+ });
433
+
434
+ observer.init({
435
+ name: "positionPlacementAttributes",
436
+ types: ["attributes"],
437
+ attributeFilter: ["position-query", "position-element", "position-events", "position-box"],
438
+ callback: mutation => refresh(mutation.target)
439
+ });
440
+
441
+ init();
442
+
443
+ export default { init, refresh, initPositionTargets };