@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/.github/FUNDING.yml +1 -1
- package/.github/workflows/automated.yml +18 -7
- package/CHANGELOG.md +309 -275
- package/CONTRIBUTING.md +6 -6
- package/README.md +210 -85
- package/demo/index.html +474 -46
- package/docs/index.html +7 -7
- package/package.json +21 -16
- package/src/autoScroll.js +213 -0
- package/src/draggable/engine.js +698 -0
- package/src/draggable.js +64 -0
- package/src/eventOperator.js +15 -0
- package/src/helpers/events.js +34 -0
- package/src/index.css +19 -0
- package/src/index.js +232 -71
- package/src/position.js +443 -482
- package/src/snappable.js +572 -0
- package/test/auto-scroll.browser.cjs +307 -0
- package/webpack.config.js +1 -0
package/src/snappable.js
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import observer from "@cocreate/observer";
|
|
2
|
+
import { queryElements } from "@cocreate/utils";
|
|
3
|
+
import { normalizeEvent } from "./helpers/events.js";
|
|
4
|
+
import Position from "./draggable/engine.js";
|
|
5
|
+
import { initElement } from "./draggable.js";
|
|
6
|
+
|
|
7
|
+
const scopeSelector = [
|
|
8
|
+
"[droppable-query][draggable-query]",
|
|
9
|
+
"[droppable-query][clonable-query]",
|
|
10
|
+
"[snappable-query][draggable-query]",
|
|
11
|
+
"[snappable-query][clonable-query]"
|
|
12
|
+
].join(", ");
|
|
13
|
+
const hoveredDocuments = new WeakSet();
|
|
14
|
+
|
|
15
|
+
function getEditorDetails(element) {
|
|
16
|
+
const current = element?.editor;
|
|
17
|
+
return typeof current?.getStart === "function" ? current : element?.editorDetails;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getEditorSourceRange(element, editorElement, source) {
|
|
21
|
+
const details = getEditorDetails(element);
|
|
22
|
+
if (typeof details?.getOuterRange === "function") {
|
|
23
|
+
if (details.element !== editorElement) return null;
|
|
24
|
+
const range = details.getOuterRange();
|
|
25
|
+
return range && Number.isInteger(range.start) && Number.isInteger(range.end) &&
|
|
26
|
+
range.start >= 0 && range.end >= range.start && range.end <= source.length ? range : null;
|
|
27
|
+
}
|
|
28
|
+
if (details?.editorElement !== editorElement ||
|
|
29
|
+
![details.start, details.contentStart, details.contentEnd, details.end].every(Number.isInteger) ||
|
|
30
|
+
details.start < 0 || details.start > details.contentStart ||
|
|
31
|
+
details.contentStart > details.contentEnd || details.contentEnd > details.end ||
|
|
32
|
+
details.end > source.length)
|
|
33
|
+
return null;
|
|
34
|
+
return details;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function insertIntoEditor({ instance, target, position, value, hasValue, mode }) {
|
|
38
|
+
const details = getEditorDetails(target);
|
|
39
|
+
const editorElement = details?.element || details?.editorElement;
|
|
40
|
+
if (!editorElement || typeof editorElement.editor?.updateText !== "function" ||
|
|
41
|
+
(typeof editorElement.editor?.getSource !== "function" && typeof editorElement.value !== "string")) return false;
|
|
42
|
+
|
|
43
|
+
const source = typeof editorElement.editor.getSource === "function"
|
|
44
|
+
? editorElement.editor.getSource() : editorElement.value;
|
|
45
|
+
if (typeof source !== "string") return false;
|
|
46
|
+
const insertionPoint = details.getInsertionPoint?.(position);
|
|
47
|
+
const outerRange = details.getOuterRange?.();
|
|
48
|
+
const contentRange = details.getContentRange?.();
|
|
49
|
+
const targetBoundary = insertionPoint ? insertionPoint.editor.getStart() + insertionPoint.offset : ({
|
|
50
|
+
beforebegin: outerRange?.start ?? details.start,
|
|
51
|
+
afterbegin: contentRange?.start ?? details.contentStart,
|
|
52
|
+
beforeend: contentRange?.end ?? details.contentEnd,
|
|
53
|
+
afterend: outerRange?.end ?? details.end
|
|
54
|
+
})[position];
|
|
55
|
+
const start = targetBoundary;
|
|
56
|
+
if (!Number.isInteger(start) || start < 0 || start > source.length) return false;
|
|
57
|
+
|
|
58
|
+
const sourceDetails = mode === "draggable" ? getEditorDetails(instance.el) : null;
|
|
59
|
+
const sourceEditor = sourceDetails?.element || sourceDetails?.editorElement;
|
|
60
|
+
const sourceText = typeof sourceEditor?.editor?.getSource === "function"
|
|
61
|
+
? sourceEditor.editor.getSource() : typeof sourceEditor?.value === "string" ? sourceEditor.value : null;
|
|
62
|
+
const sourceRange = sourceText === null ? null : getEditorSourceRange(instance.el, sourceEditor, sourceText);
|
|
63
|
+
if (sourceEditor && !sourceRange) return false;
|
|
64
|
+
const movingSource = mode === "draggable" && !hasValue && sourceEditor === editorElement && !!sourceRange;
|
|
65
|
+
const insertedText = movingSource
|
|
66
|
+
? sourceText.slice(sourceRange.start, sourceRange.end)
|
|
67
|
+
: typeof value === "string" ? value : value?.outerHTML;
|
|
68
|
+
if (typeof insertedText !== "string") return false;
|
|
69
|
+
|
|
70
|
+
if (sourceEditor === editorElement && sourceRange) {
|
|
71
|
+
if (movingSource && start >= sourceRange.start && start <= sourceRange.end) return true;
|
|
72
|
+
let editStart = start;
|
|
73
|
+
let length = 0;
|
|
74
|
+
let text = insertedText;
|
|
75
|
+
if (mode === "draggable") {
|
|
76
|
+
if (start <= sourceRange.start) {
|
|
77
|
+
length = sourceRange.end - start;
|
|
78
|
+
text += sourceText.slice(start, sourceRange.start);
|
|
79
|
+
} else if (start >= sourceRange.end) {
|
|
80
|
+
editStart = sourceRange.start;
|
|
81
|
+
length = start - sourceRange.start;
|
|
82
|
+
text = sourceText.slice(sourceRange.end, start) + text;
|
|
83
|
+
} else {
|
|
84
|
+
editStart = sourceRange.start;
|
|
85
|
+
length = sourceRange.end - sourceRange.start;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await editorElement.editor.updateText({ start: editStart, length, value: text });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (insertionPoint) {
|
|
93
|
+
await insertionPoint.editor.updateText({ start: insertionPoint.offset, length: 0, value: insertedText });
|
|
94
|
+
} else await editorElement.editor.updateText({ start, length: 0, value: insertedText });
|
|
95
|
+
if (mode === "draggable") {
|
|
96
|
+
if (sourceEditor && sourceRange)
|
|
97
|
+
await sourceEditor.editor.updateText({
|
|
98
|
+
start: sourceRange.start,
|
|
99
|
+
length: sourceRange.end - sourceRange.start,
|
|
100
|
+
value: ""
|
|
101
|
+
});
|
|
102
|
+
else instance.el.remove();
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function trackHoveredDocument(doc) {
|
|
108
|
+
if (!doc || hoveredDocuments.has(doc)) return;
|
|
109
|
+
hoveredDocuments.add(doc);
|
|
110
|
+
const update = event => {
|
|
111
|
+
doc.hoveredElement = event.target;
|
|
112
|
+
if (doc.documentElement) doc.documentElement.hoveredElement = event.target;
|
|
113
|
+
const frame = doc.defaultView?.frameElement;
|
|
114
|
+
if (frame) frame.hoveredElement = event.target;
|
|
115
|
+
};
|
|
116
|
+
doc.addEventListener("mouseover", update);
|
|
117
|
+
doc.addEventListener("mousemove", update);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function mapFramePoint(frame, x, y) {
|
|
121
|
+
const rect = frame.getBoundingClientRect();
|
|
122
|
+
const scaleX = frame.offsetWidth ? rect.width / frame.offsetWidth : 1;
|
|
123
|
+
const scaleY = frame.offsetHeight ? rect.height / frame.offsetHeight : 1;
|
|
124
|
+
return {
|
|
125
|
+
x: rect.left + (frame.clientLeft + x) * scaleX,
|
|
126
|
+
y: rect.top + (frame.clientTop + y) * scaleY
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const snappableMethods = {
|
|
131
|
+
_shouldDelegateDrag() {
|
|
132
|
+
const scope = this.el.closest(scopeSelector);
|
|
133
|
+
const draggableQuery = scope?.getAttribute("draggable-query");
|
|
134
|
+
const clonableQuery = scope?.getAttribute("clonable-query");
|
|
135
|
+
return !!((draggableQuery && queryElements({ element: scope, selector: draggableQuery }).includes(this.el)) ||
|
|
136
|
+
(clonableQuery && queryElements({ element: scope, selector: clonableQuery }).includes(this.el)));
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
_initSnappableDrag() {
|
|
140
|
+
const dropScope = this.dragScope?.hasAttribute("droppable-query")
|
|
141
|
+
? this.dragScope
|
|
142
|
+
: this.el.closest("[droppable-query]");
|
|
143
|
+
const snapScope = this.dragScope?.hasAttribute("snappable-query")
|
|
144
|
+
? this.dragScope
|
|
145
|
+
: this.el.closest("[snappable-query]");
|
|
146
|
+
this.dropQuerySelector = dropScope?.getAttribute("droppable-query") || null;
|
|
147
|
+
this.snapQuerySelector = snapScope?.getAttribute("snappable-query") || null;
|
|
148
|
+
this.referenceQuerySelector = this.dropQuerySelector || this.snapQuerySelector;
|
|
149
|
+
this.referenceQueryElement = this.dropQuerySelector ? dropScope : snapScope;
|
|
150
|
+
this.referenceQueryTargets = null;
|
|
151
|
+
this.referenceTargetsSelector = null;
|
|
152
|
+
this.reference = null;
|
|
153
|
+
this._setActiveDroppable(null);
|
|
154
|
+
this._removePositionMarker();
|
|
155
|
+
this._initReferenceTracking();
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
_updateSnappableReference(x, y, doc, hoveredElement) {
|
|
159
|
+
if (!this._usesPositionReference()) return;
|
|
160
|
+
this._prepareReferenceTargets();
|
|
161
|
+
this._initReferenceTracking();
|
|
162
|
+
this._updateReference(x, y, doc, hoveredElement);
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
_finishSnappableReference(event, reference) {
|
|
166
|
+
if (event && this._usesPositionReference())
|
|
167
|
+
this._updateReference(
|
|
168
|
+
reference?.x ?? event.clientX,
|
|
169
|
+
reference?.y ?? event.clientY,
|
|
170
|
+
reference?.document ?? event.document ?? this.el.ownerDocument,
|
|
171
|
+
reference?.element
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
_completeSnappableDrop(cancelled) {
|
|
176
|
+
if (this.dropQuerySelector) {
|
|
177
|
+
if (!cancelled && this.reference) {
|
|
178
|
+
this._applyDrop().then(inserted => {
|
|
179
|
+
if (!inserted) this._restoreOriginalStyle();
|
|
180
|
+
}).catch(error => {
|
|
181
|
+
console.error("Position drop failed:", error);
|
|
182
|
+
this._restoreOriginalStyle();
|
|
183
|
+
});
|
|
184
|
+
} else {
|
|
185
|
+
this._restoreOriginalStyle();
|
|
186
|
+
}
|
|
187
|
+
} else if (this.snapQuerySelector && !cancelled && this.reference) {
|
|
188
|
+
this._applyDrop().catch(error => console.error("Position snap failed:", error));
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
_clearSnappableState() {
|
|
193
|
+
this._removePositionMarker();
|
|
194
|
+
this._setActiveDroppable(null);
|
|
195
|
+
this._removeFrameOverlays();
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
_createFrameOverlays() {
|
|
199
|
+
if (!this.dropQuerySelector || this.frameOverlays?.length) return;
|
|
200
|
+
let topDocument;
|
|
201
|
+
try { topDocument = this.el.ownerDocument.defaultView.top.document; } catch {}
|
|
202
|
+
if (!topDocument) return;
|
|
203
|
+
|
|
204
|
+
this.frameOverlays = [];
|
|
205
|
+
for (const frame of topDocument.querySelectorAll("iframe")) {
|
|
206
|
+
try {
|
|
207
|
+
if (!frame.contentDocument) continue;
|
|
208
|
+
const rect = frame.getBoundingClientRect();
|
|
209
|
+
const overlay = topDocument.createElement("div");
|
|
210
|
+
overlay.setAttribute("aria-hidden", "true");
|
|
211
|
+
overlay.style.cssText = `position:fixed;left:${rect.left}px;top:${rect.top}px;width:${rect.width}px;height:${rect.height}px;z-index:2147483646;background:transparent;border:0;pointer-events:auto;cursor:grabbing`;
|
|
212
|
+
overlay.__positionFrame = frame;
|
|
213
|
+
(topDocument.body || topDocument.documentElement).appendChild(overlay);
|
|
214
|
+
this.frameOverlays.push(overlay);
|
|
215
|
+
} catch {}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
_getFrameReference(event, pointer = event) {
|
|
220
|
+
const frame = event?.target?.__positionFrame;
|
|
221
|
+
if (!frame || !pointer || !Number.isFinite(pointer.clientX) || !Number.isFinite(pointer.clientY)) return null;
|
|
222
|
+
try {
|
|
223
|
+
const doc = frame.contentDocument;
|
|
224
|
+
if (!doc) return null;
|
|
225
|
+
const rect = frame.getBoundingClientRect();
|
|
226
|
+
const scaleX = frame.offsetWidth ? rect.width / frame.offsetWidth : 1;
|
|
227
|
+
const scaleY = frame.offsetHeight ? rect.height / frame.offsetHeight : 1;
|
|
228
|
+
const x = (pointer.clientX - rect.left) / scaleX - frame.clientLeft;
|
|
229
|
+
const y = (pointer.clientY - rect.top) / scaleY - frame.clientTop;
|
|
230
|
+
return { x, y, document: doc, element: doc.elementsFromPoint(x, y)[0] || null };
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
_removeFrameOverlays() {
|
|
237
|
+
for (const overlay of this.frameOverlays || []) overlay.remove();
|
|
238
|
+
this.frameOverlays = null;
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
_initReferenceTracking() {
|
|
242
|
+
if (!this._usesPositionReference() || this.referenceTrackingInitialized) return;
|
|
243
|
+
this.referenceTrackingInitialized = true;
|
|
244
|
+
trackHoveredDocument(this.el.ownerDocument);
|
|
245
|
+
const watchFrame = frame => {
|
|
246
|
+
try {
|
|
247
|
+
if (frame.contentDocument?.readyState === "complete")
|
|
248
|
+
this._watchReferenceFrame(frame, frame.contentDocument);
|
|
249
|
+
else frame.addEventListener("load", () => {
|
|
250
|
+
try { this._watchReferenceFrame(frame, frame.contentDocument); } catch {}
|
|
251
|
+
}, { once: true });
|
|
252
|
+
} catch {}
|
|
253
|
+
};
|
|
254
|
+
this.el.ownerDocument.querySelectorAll("iframe").forEach(watchFrame);
|
|
255
|
+
const enclosingFrame = this.el.ownerDocument.defaultView?.frameElement;
|
|
256
|
+
if (enclosingFrame) watchFrame(enclosingFrame);
|
|
257
|
+
const MutationObserver = this.el.ownerDocument.defaultView.MutationObserver;
|
|
258
|
+
this.referenceObserver = new MutationObserver(mutations => {
|
|
259
|
+
for (const mutation of mutations) for (const node of mutation.addedNodes) {
|
|
260
|
+
if (node.nodeType !== 1) continue;
|
|
261
|
+
if (node.matches("iframe")) watchFrame(node);
|
|
262
|
+
node.querySelectorAll?.("iframe").forEach(watchFrame);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
this.referenceObserver.observe(this.el.ownerDocument.documentElement, {
|
|
266
|
+
childList: true,
|
|
267
|
+
subtree: true
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
_usesPositionReference() {
|
|
272
|
+
return this.el.hasAttribute("position-reference") || !!this.referenceQuerySelector;
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
_prepareReferenceTargets() {
|
|
276
|
+
const selector = this.referenceQuerySelector;
|
|
277
|
+
if (selector == null) {
|
|
278
|
+
this.referenceQueryTargets = null;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (this.referenceTargetsSelector === selector && this.referenceQueryTargets) return;
|
|
282
|
+
this.referenceTargetsSelector = selector;
|
|
283
|
+
this.referenceQueryTargets = new Set(queryElements({
|
|
284
|
+
element: this.referenceQueryElement || this.dragScope || this.el,
|
|
285
|
+
selector
|
|
286
|
+
}));
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
_watchReferenceFrame(frame, frameDocument) {
|
|
290
|
+
if (!frameDocument) return;
|
|
291
|
+
if (frame.positionReferenceDocument !== frameDocument) {
|
|
292
|
+
frame.positionReferenceDocument = frameDocument;
|
|
293
|
+
trackHoveredDocument(frameDocument);
|
|
294
|
+
const update = event => {
|
|
295
|
+
const pointer = event.changedTouches?.[0] || event;
|
|
296
|
+
frame.dispatchEvent(new CustomEvent("position-reference-move", {
|
|
297
|
+
detail: { type: event.type, element: event.target, x: pointer.clientX, y: pointer.clientY, document: frameDocument, originalEvent: event }
|
|
298
|
+
}));
|
|
299
|
+
};
|
|
300
|
+
frameDocument.addEventListener("mouseover", update);
|
|
301
|
+
frameDocument.addEventListener("mousemove", update);
|
|
302
|
+
frameDocument.addEventListener("mouseup", update);
|
|
303
|
+
frameDocument.addEventListener("touchmove", update, { passive: false });
|
|
304
|
+
frameDocument.addEventListener("touchend", update);
|
|
305
|
+
frameDocument.addEventListener("touchcancel", update);
|
|
306
|
+
}
|
|
307
|
+
if (!frame.positionReferenceInstances) frame.positionReferenceInstances = new WeakSet();
|
|
308
|
+
if (frame.positionReferenceInstances.has(this)) return;
|
|
309
|
+
frame.positionReferenceInstances.add(this);
|
|
310
|
+
frame.addEventListener("position-reference-move", event => {
|
|
311
|
+
if (!this.clickedInfo?.isMoving) {
|
|
312
|
+
if (["mouseup", "touchend", "touchcancel"].includes(event.detail.type))
|
|
313
|
+
this._cancelPendingDragStart();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (event.detail.type === "touchmove") event.detail.originalEvent?.preventDefault();
|
|
317
|
+
const point = mapFramePoint(frame, event.detail.x, event.detail.y);
|
|
318
|
+
const reference = {
|
|
319
|
+
x: event.detail.x,
|
|
320
|
+
y: event.detail.y,
|
|
321
|
+
document: event.detail.document,
|
|
322
|
+
element: event.detail.element
|
|
323
|
+
};
|
|
324
|
+
if (event.detail.type === "mouseup" || event.detail.type === "touchend" || event.detail.type === "touchcancel") {
|
|
325
|
+
this._onUp({ clientX: point.x, clientY: point.y }, event.detail.type === "touchcancel", reference);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
this._onMove({ clientX: point.x, clientY: point.y }, reference);
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
_setActiveDroppable(container) {
|
|
333
|
+
if (this.activeDroppable === container) return;
|
|
334
|
+
this.activeDroppable?.removeAttribute("droppable");
|
|
335
|
+
this.activeDroppable = container || null;
|
|
336
|
+
this.activeDroppable?.setAttribute("droppable", "");
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
_updateReference(x, y, doc, hoveredElement) {
|
|
340
|
+
let candidate = hoveredElement || doc.elementsFromPoint(x, y).find(element =>
|
|
341
|
+
element !== this.el && !this.el.contains(element));
|
|
342
|
+
if (candidate === this.el || this.el.contains(candidate)) candidate = null;
|
|
343
|
+
const hasContainerQuery = !!this.referenceQuerySelector;
|
|
344
|
+
let element = candidate;
|
|
345
|
+
let container = null;
|
|
346
|
+
let isEmptyContainer = false;
|
|
347
|
+
if (candidate && hasContainerQuery) {
|
|
348
|
+
this._prepareReferenceTargets();
|
|
349
|
+
container = candidate;
|
|
350
|
+
while (container && !this.referenceQueryTargets.has(container)) container = container.parentElement;
|
|
351
|
+
this._setActiveDroppable(container);
|
|
352
|
+
|
|
353
|
+
if (!container) {
|
|
354
|
+
element = null;
|
|
355
|
+
} else if ([...container.children].filter(child =>
|
|
356
|
+
!child.hasAttribute("position-marker") && child !== this.el && !this.el.contains(child)
|
|
357
|
+
).length === 0) {
|
|
358
|
+
element = container;
|
|
359
|
+
isEmptyContainer = true;
|
|
360
|
+
} else {
|
|
361
|
+
element = candidate === container ? null : candidate;
|
|
362
|
+
while (element && element.parentElement !== container) element = element.parentElement;
|
|
363
|
+
if (element?.hasAttribute("position-marker")) element = null;
|
|
364
|
+
if (!element) {
|
|
365
|
+
let closestDistance = Infinity;
|
|
366
|
+
for (const child of container.children) {
|
|
367
|
+
if (child.hasAttribute("position-marker") || child === this.el || this.el.contains(child)) continue;
|
|
368
|
+
const rect = child.getBoundingClientRect();
|
|
369
|
+
const dx = Math.max(rect.left - x, 0, x - rect.right);
|
|
370
|
+
const dy = Math.max(rect.top - y, 0, y - rect.bottom);
|
|
371
|
+
const distance = dx * dx + dy * dy;
|
|
372
|
+
if (distance < closestDistance) {
|
|
373
|
+
closestDistance = distance;
|
|
374
|
+
element = child;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
this._setActiveDroppable(null);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!element) {
|
|
384
|
+
this._removePositionMarker();
|
|
385
|
+
if (this.reference) {
|
|
386
|
+
this.reference = null;
|
|
387
|
+
this.createEvent("element-position-reference");
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const rect = element.getBoundingClientRect();
|
|
393
|
+
const vertical = rect.height >= rect.width;
|
|
394
|
+
const offset = vertical ? y - rect.top : x - rect.left;
|
|
395
|
+
const size = vertical ? rect.height : rect.width;
|
|
396
|
+
let position;
|
|
397
|
+
if (isEmptyContainer) position = "afterbegin";
|
|
398
|
+
else if (hasContainerQuery) position = offset < size / 2 ? "beforebegin" : "afterend";
|
|
399
|
+
else {
|
|
400
|
+
const edge = Math.min(size * 0.2, 24);
|
|
401
|
+
if (offset < edge) position = "beforebegin";
|
|
402
|
+
else if (offset > size - edge) position = "afterend";
|
|
403
|
+
else position = offset < size / 2 ? "afterbegin" : "beforeend";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
this.reference = { element, container: hasContainerQuery ? container : null, position, x, y, document: doc };
|
|
407
|
+
this._updatePositionMarker(this.reference);
|
|
408
|
+
this.createEvent("element-position-reference");
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
_updatePositionMarker(reference) {
|
|
412
|
+
const doc = reference.document || this.el.ownerDocument;
|
|
413
|
+
if (!this.positionMarker || this.positionMarker.ownerDocument !== doc) {
|
|
414
|
+
this._removePositionMarker();
|
|
415
|
+
const marker = this.el.cloneNode(true);
|
|
416
|
+
const activationAttributes = ["draggable", "draggable-query", "clonable-query", "droppable-query", "snappable-query", "draggable-value", "clonable-value", "position-query", "position-element", "position-reference", "snapped", "position-marker"];
|
|
417
|
+
for (const element of [marker, ...marker.querySelectorAll("*")]) {
|
|
418
|
+
element.removeAttribute("id");
|
|
419
|
+
element.removeAttribute("style");
|
|
420
|
+
for (const attribute of activationAttributes) element.removeAttribute(attribute);
|
|
421
|
+
element.classList.remove("position-moving", "position-moving-preview", "position-cloning", "selectedPosition");
|
|
422
|
+
}
|
|
423
|
+
marker.setAttribute("position-marker", "");
|
|
424
|
+
marker.setAttribute("aria-hidden", "true");
|
|
425
|
+
marker.setAttribute("inert", "");
|
|
426
|
+
this.positionMarker = marker;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const marker = this.positionMarker;
|
|
430
|
+
const alreadyPlaced = reference.position === "beforebegin"
|
|
431
|
+
? marker.nextElementSibling === reference.element
|
|
432
|
+
: reference.position === "afterend"
|
|
433
|
+
? marker.previousElementSibling === reference.element
|
|
434
|
+
: reference.position === "afterbegin"
|
|
435
|
+
? marker.parentElement === reference.element && marker === reference.element.firstElementChild
|
|
436
|
+
: marker.parentElement === reference.element && marker === reference.element.lastElementChild;
|
|
437
|
+
if (!alreadyPlaced) reference.element.insertAdjacentElement(reference.position, marker);
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
_removePositionMarker() {
|
|
441
|
+
this.positionMarker?.remove();
|
|
442
|
+
this.positionMarker = null;
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
async _applyDrop() {
|
|
446
|
+
const { element, position } = this.reference || {};
|
|
447
|
+
if (!element || !position || !this.referenceQuerySelector) return false;
|
|
448
|
+
if (element === this.el || this.el.contains(element)) return false;
|
|
449
|
+
const mode = this.dragType === "clonable" ? "clonable" : "draggable";
|
|
450
|
+
const valueAttribute = `${mode}-value`;
|
|
451
|
+
const hasValue = this.el.hasAttribute(valueAttribute);
|
|
452
|
+
const value = hasValue ? this.el.getAttribute(valueAttribute)
|
|
453
|
+
: mode === "clonable" ? this.el.cloneNode(true) : this.el;
|
|
454
|
+
if (typeof value !== "string" && value?.nodeType !== 1) return false;
|
|
455
|
+
const insertValue = mode === "clonable" && value === this.el ? this.el.cloneNode(true) : value;
|
|
456
|
+
this._restoreOriginalStyle();
|
|
457
|
+
if (getEditorDetails(element)) {
|
|
458
|
+
const inserted = await insertIntoEditor({
|
|
459
|
+
instance: this,
|
|
460
|
+
target: element,
|
|
461
|
+
position,
|
|
462
|
+
value,
|
|
463
|
+
hasValue,
|
|
464
|
+
mode
|
|
465
|
+
});
|
|
466
|
+
if (!inserted) return false;
|
|
467
|
+
this.createEvent("element-position-inserted", {
|
|
468
|
+
type: mode,
|
|
469
|
+
value: insertValue,
|
|
470
|
+
inserted: true,
|
|
471
|
+
reference: this.reference,
|
|
472
|
+
editorElement: getEditorDetails(element).element || getEditorDetails(element).editorElement
|
|
473
|
+
});
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
let inserted;
|
|
477
|
+
try {
|
|
478
|
+
if (typeof value === "string") {
|
|
479
|
+
element.insertAdjacentHTML(position, value);
|
|
480
|
+
inserted = true;
|
|
481
|
+
} else inserted = element.insertAdjacentElement(position, insertValue);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.error("Position drop failed:", error);
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
if (!inserted) return false;
|
|
487
|
+
if (this.snapQuerySelector && mode === "draggable" && insertValue === this.el) {
|
|
488
|
+
this.el.removeAttribute("style");
|
|
489
|
+
this.el.setAttribute("snapped", "");
|
|
490
|
+
}
|
|
491
|
+
if (mode === "draggable" && hasValue && value !== this.el) this.el.remove();
|
|
492
|
+
this.createEvent("element-position-inserted", { type: mode, value: insertValue, inserted, reference: this.reference });
|
|
493
|
+
return true;
|
|
494
|
+
},
|
|
495
|
+
|
|
496
|
+
_restoreOriginalStyle() {
|
|
497
|
+
if (this.originalStyle?.present) this.el.setAttribute("style", this.originalStyle.value);
|
|
498
|
+
else this.el.removeAttribute("style");
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
Object.assign(Position.prototype, snappableMethods);
|
|
503
|
+
|
|
504
|
+
function initScopes(scopes) {
|
|
505
|
+
for (const scope of scopes) {
|
|
506
|
+
const matchesScope = scope.matches(scopeSelector);
|
|
507
|
+
if (scope.positionDragDelegation && matchesScope) continue;
|
|
508
|
+
if (scope.positionDragDelegation) {
|
|
509
|
+
scope.removeEventListener("mousedown", scope.positionDragDelegation);
|
|
510
|
+
scope.removeEventListener("touchstart", scope.positionDragDelegation);
|
|
511
|
+
scope.positionDragDelegation = null;
|
|
512
|
+
}
|
|
513
|
+
if (!matchesScope) continue;
|
|
514
|
+
|
|
515
|
+
const startDrag = event => {
|
|
516
|
+
if (event.__positionHandled) return;
|
|
517
|
+
const pointerEvent = normalizeEvent(event);
|
|
518
|
+
const draggableQuery = scope.getAttribute("draggable-query");
|
|
519
|
+
const clonableQuery = scope.getAttribute("clonable-query");
|
|
520
|
+
const draggableMatches = new Set(draggableQuery ? queryElements({ element: scope, selector: draggableQuery }) : []);
|
|
521
|
+
const clonableMatches = new Set(clonableQuery ? queryElements({ element: scope, selector: clonableQuery }) : []);
|
|
522
|
+
let element = pointerEvent.target?.nodeType === 1 ? pointerEvent.target : pointerEvent.target?.parentElement;
|
|
523
|
+
while (element && !draggableMatches.has(element) && !clonableMatches.has(element))
|
|
524
|
+
element = element.parentElement;
|
|
525
|
+
if (!element || (!draggableMatches.has(element) && !clonableMatches.has(element))) return;
|
|
526
|
+
const dragType = clonableMatches.has(element) ? "clonable" : "draggable";
|
|
527
|
+
if (!element.position?._positionController)
|
|
528
|
+
initElement(element, { draggable: true, dragScope: scope, dragType, delegated: true });
|
|
529
|
+
Object.assign(element.position.options, {
|
|
530
|
+
draggable: true,
|
|
531
|
+
dragScope: scope,
|
|
532
|
+
delegated: true,
|
|
533
|
+
dragType
|
|
534
|
+
});
|
|
535
|
+
event.__positionDelegated = true;
|
|
536
|
+
event.__positionHandled = true;
|
|
537
|
+
element.position._handlePointerDown(event);
|
|
538
|
+
};
|
|
539
|
+
scope.positionDragDelegation = startDrag;
|
|
540
|
+
scope.addEventListener("mousedown", startDrag);
|
|
541
|
+
scope.addEventListener("touchstart", startDrag, { passive: false });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function refresh(node) {
|
|
546
|
+
if (node.nodeType !== 1) return;
|
|
547
|
+
if (node.matches(scopeSelector)) initScopes([node]);
|
|
548
|
+
initScopes(node.querySelectorAll(scopeSelector));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export function init() {
|
|
552
|
+
initScopes(document.querySelectorAll(scopeSelector));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
observer.init({
|
|
556
|
+
name: "positionSnappableAddedNodes",
|
|
557
|
+
types: ["addedNodes"],
|
|
558
|
+
selector: scopeSelector,
|
|
559
|
+
callback: mutation => refresh(mutation.target)
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
observer.init({
|
|
563
|
+
name: "positionSnappableAttributes",
|
|
564
|
+
types: ["attributes"],
|
|
565
|
+
attributeFilter: ["draggable-query", "clonable-query", "droppable-query", "snappable-query"],
|
|
566
|
+
selector: "[draggable-query], [clonable-query], [droppable-query], [snappable-query]",
|
|
567
|
+
callback: mutation => initScopes([mutation.target])
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
init();
|
|
571
|
+
|
|
572
|
+
export default { init };
|