@ashraf_mizo/htmlcanvas 1.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/bin/cli.js +28 -0
- package/editor/alignment.js +211 -0
- package/editor/assets.js +724 -0
- package/editor/clipboard.js +177 -0
- package/editor/coords.js +121 -0
- package/editor/crop.js +325 -0
- package/editor/cssVars.js +134 -0
- package/editor/domModel.js +161 -0
- package/editor/editor.css +1996 -0
- package/editor/editor.js +833 -0
- package/editor/guides.js +513 -0
- package/editor/history.js +135 -0
- package/editor/index.html +540 -0
- package/editor/layers.js +389 -0
- package/editor/logo-final.svg +21 -0
- package/editor/logo-toolbar.svg +21 -0
- package/editor/manipulation.js +864 -0
- package/editor/multiSelect.js +436 -0
- package/editor/properties.js +1583 -0
- package/editor/selection.js +432 -0
- package/editor/serializer.js +160 -0
- package/editor/shortcuts.js +143 -0
- package/editor/slidePanel.js +361 -0
- package/editor/slides.js +101 -0
- package/editor/snap.js +98 -0
- package/editor/textEdit.js +538 -0
- package/editor/zoom.js +96 -0
- package/package.json +28 -0
- package/server.js +588 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// editor/multiSelect.js — Multi-element selection, marquee drag, group/ungroup
|
|
2
|
+
//
|
|
3
|
+
// Exports:
|
|
4
|
+
// initMultiSelect(iframe, iframeDoc, canvasArea) — attach marquee + shift-click listeners
|
|
5
|
+
// addToSelection(element) — add element to multi-selection set
|
|
6
|
+
// removeFromSelection(element) — remove element from set
|
|
7
|
+
// clearMultiSelection() — empty the set
|
|
8
|
+
// getSelectedElements() — returns array of selected elements
|
|
9
|
+
// getSelectedHcIds() — returns array of selected hc-id strings
|
|
10
|
+
// isMultiSelectActive() — true if 2+ elements selected
|
|
11
|
+
// makeGroupCommand(elements, hcIds, iframeDoc) — Command to wrap in data-hc-group
|
|
12
|
+
// makeUngroupCommand(wrapper, iframeDoc) — Command to unwrap group
|
|
13
|
+
|
|
14
|
+
// ── Module state ─────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
let _selectedElements = new Set();
|
|
17
|
+
let _selectedHcIds = new Set();
|
|
18
|
+
let _iframe = null;
|
|
19
|
+
let _iframeDoc = null;
|
|
20
|
+
let _canvasArea = null;
|
|
21
|
+
let _marqueeDiv = null;
|
|
22
|
+
let _marqueeStart = null;
|
|
23
|
+
|
|
24
|
+
// Stored handler refs for clean removal on re-init
|
|
25
|
+
let _onZoomForMulti = null;
|
|
26
|
+
let _onScrollForMulti = null;
|
|
27
|
+
|
|
28
|
+
// ── DOM refs (lazy — only accessed after DOM is ready) ────────────────────────
|
|
29
|
+
|
|
30
|
+
function getMultiOverlay() {
|
|
31
|
+
if (typeof document === 'undefined') return null;
|
|
32
|
+
return document.getElementById('multi-selection-overlay');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Selection API ─────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Add an element to the multi-selection set.
|
|
39
|
+
* Dispatches hc:multi-selection-changed.
|
|
40
|
+
* @param {Element} element
|
|
41
|
+
*/
|
|
42
|
+
export function addToSelection(element) {
|
|
43
|
+
if (!element) return;
|
|
44
|
+
const hcId = element.getAttribute ? element.getAttribute('data-hc-id') : null;
|
|
45
|
+
_selectedElements.add(element);
|
|
46
|
+
if (hcId) _selectedHcIds.add(hcId);
|
|
47
|
+
_dispatchMultiSelectionChanged();
|
|
48
|
+
_updateMultiOverlay();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Remove an element from the multi-selection set.
|
|
53
|
+
* Dispatches hc:multi-selection-changed.
|
|
54
|
+
* @param {Element} element
|
|
55
|
+
*/
|
|
56
|
+
export function removeFromSelection(element) {
|
|
57
|
+
if (!element) return;
|
|
58
|
+
const hcId = element.getAttribute ? element.getAttribute('data-hc-id') : null;
|
|
59
|
+
_selectedElements.delete(element);
|
|
60
|
+
if (hcId) _selectedHcIds.delete(hcId);
|
|
61
|
+
_dispatchMultiSelectionChanged();
|
|
62
|
+
_updateMultiOverlay();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Clear all multi-selection state.
|
|
67
|
+
* Dispatches hc:multi-selection-changed with empty arrays.
|
|
68
|
+
*/
|
|
69
|
+
export function clearMultiSelection() {
|
|
70
|
+
_selectedElements.clear();
|
|
71
|
+
_selectedHcIds.clear();
|
|
72
|
+
_dispatchMultiSelectionChanged();
|
|
73
|
+
_hideMultiOverlay();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Returns the array of all currently selected elements.
|
|
78
|
+
* @returns {Element[]}
|
|
79
|
+
*/
|
|
80
|
+
export function getSelectedElements() {
|
|
81
|
+
return [..._selectedElements];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Returns the array of all currently selected hc-id strings.
|
|
86
|
+
* @returns {string[]}
|
|
87
|
+
*/
|
|
88
|
+
export function getSelectedHcIds() {
|
|
89
|
+
return [..._selectedHcIds];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Returns true if 2 or more elements are selected.
|
|
94
|
+
* @returns {boolean}
|
|
95
|
+
*/
|
|
96
|
+
export function isMultiSelectActive() {
|
|
97
|
+
return _selectedElements.size > 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Event dispatch ────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
function _dispatchMultiSelectionChanged() {
|
|
103
|
+
// Guard: only dispatch in browser context
|
|
104
|
+
if (typeof window === 'undefined') return;
|
|
105
|
+
window.dispatchEvent(new CustomEvent('hc:multi-selection-changed', {
|
|
106
|
+
detail: {
|
|
107
|
+
elements: [..._selectedElements],
|
|
108
|
+
hcIds: [..._selectedHcIds],
|
|
109
|
+
}
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Multi-selection overlay ───────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Update the position of the multi-selection bounding box overlay.
|
|
117
|
+
* Uses getElementCanvasRect for each element, then computes the union rect.
|
|
118
|
+
*/
|
|
119
|
+
function _updateMultiOverlay() {
|
|
120
|
+
// Only works in browser with iframe refs
|
|
121
|
+
if (!_iframe || !_canvasArea || _selectedElements.size < 2) {
|
|
122
|
+
_hideMultiOverlay();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const overlay = getMultiOverlay();
|
|
127
|
+
if (!overlay) return;
|
|
128
|
+
|
|
129
|
+
// Import getElementCanvasRect dynamically to avoid circular deps in tests
|
|
130
|
+
// (this path is only reached when _iframe is set = browser only)
|
|
131
|
+
import('./coords.js').then(({ getElementCanvasRect }) => {
|
|
132
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
133
|
+
|
|
134
|
+
for (const el of _selectedElements) {
|
|
135
|
+
try {
|
|
136
|
+
const r = getElementCanvasRect(el, _iframe, _canvasArea);
|
|
137
|
+
minX = Math.min(minX, r.x !== undefined ? r.x : r.left);
|
|
138
|
+
minY = Math.min(minY, r.y !== undefined ? r.y : r.top);
|
|
139
|
+
maxX = Math.max(maxX, (r.x !== undefined ? r.x : r.left) + r.width);
|
|
140
|
+
maxY = Math.max(maxY, (r.y !== undefined ? r.y : r.top) + r.height);
|
|
141
|
+
} catch { /* element may have been removed */ }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!isFinite(minX)) { _hideMultiOverlay(); return; }
|
|
145
|
+
|
|
146
|
+
overlay.style.left = minX + 'px';
|
|
147
|
+
overlay.style.top = minY + 'px';
|
|
148
|
+
overlay.style.width = (maxX - minX) + 'px';
|
|
149
|
+
overlay.style.height = (maxY - minY) + 'px';
|
|
150
|
+
overlay.style.display = '';
|
|
151
|
+
}).catch(() => {});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function _hideMultiOverlay() {
|
|
155
|
+
const overlay = getMultiOverlay();
|
|
156
|
+
if (overlay) overlay.style.display = 'none';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Marquee drag ──────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Initialises (or re-initialises) the multi-select module for a new iframe.
|
|
163
|
+
* Attaches marquee drag listeners on the canvasArea.
|
|
164
|
+
*
|
|
165
|
+
* @param {HTMLIFrameElement} iframe
|
|
166
|
+
* @param {Document} iframeDoc
|
|
167
|
+
* @param {HTMLElement} canvasArea
|
|
168
|
+
*/
|
|
169
|
+
export function initMultiSelect(iframe, iframeDoc, canvasArea) {
|
|
170
|
+
_iframe = iframe;
|
|
171
|
+
_iframeDoc = iframeDoc;
|
|
172
|
+
_canvasArea = canvasArea;
|
|
173
|
+
|
|
174
|
+
clearMultiSelection();
|
|
175
|
+
|
|
176
|
+
// Remove existing listeners (avoid stacking on reload)
|
|
177
|
+
canvasArea.removeEventListener('pointerdown', _onMarqueeStart);
|
|
178
|
+
if (_onZoomForMulti) window.removeEventListener('hc:zoom-changed', _onZoomForMulti);
|
|
179
|
+
if (_onScrollForMulti && _canvasArea) _canvasArea.removeEventListener('scroll', _onScrollForMulti);
|
|
180
|
+
|
|
181
|
+
// Attach marquee handler
|
|
182
|
+
canvasArea.addEventListener('pointerdown', _onMarqueeStart);
|
|
183
|
+
|
|
184
|
+
// Re-sync overlay on zoom/scroll (stored refs to prevent stacking)
|
|
185
|
+
_onZoomForMulti = () => _updateMultiOverlay();
|
|
186
|
+
_onScrollForMulti = () => _updateMultiOverlay();
|
|
187
|
+
window.addEventListener('hc:zoom-changed', _onZoomForMulti);
|
|
188
|
+
canvasArea.addEventListener('scroll', _onScrollForMulti);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function _onMarqueeStart(e) {
|
|
192
|
+
// Only start marquee if clicking directly on canvas background (not iframe)
|
|
193
|
+
// and not on an existing overlay
|
|
194
|
+
if (e.target !== _canvasArea) return;
|
|
195
|
+
if (e.button !== 0) return;
|
|
196
|
+
|
|
197
|
+
const canvasRect = _canvasArea.getBoundingClientRect();
|
|
198
|
+
const scrollLeft = _canvasArea.scrollLeft;
|
|
199
|
+
const scrollTop = _canvasArea.scrollTop;
|
|
200
|
+
|
|
201
|
+
_marqueeStart = {
|
|
202
|
+
x: e.clientX - canvasRect.left + scrollLeft,
|
|
203
|
+
y: e.clientY - canvasRect.top + scrollTop,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Create marquee div
|
|
207
|
+
_marqueeDiv = document.createElement('div');
|
|
208
|
+
_marqueeDiv.className = 'marquee-rect';
|
|
209
|
+
_marqueeDiv.style.left = _marqueeStart.x + 'px';
|
|
210
|
+
_marqueeDiv.style.top = _marqueeStart.y + 'px';
|
|
211
|
+
_marqueeDiv.style.width = '0px';
|
|
212
|
+
_marqueeDiv.style.height = '0px';
|
|
213
|
+
_canvasArea.appendChild(_marqueeDiv);
|
|
214
|
+
|
|
215
|
+
document.addEventListener('pointermove', _onMarqueeMove);
|
|
216
|
+
document.addEventListener('pointerup', _onMarqueeEnd);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function _onMarqueeMove(e) {
|
|
220
|
+
if (!_marqueeDiv || !_marqueeStart) return;
|
|
221
|
+
|
|
222
|
+
const canvasRect = _canvasArea.getBoundingClientRect();
|
|
223
|
+
const scrollLeft = _canvasArea.scrollLeft;
|
|
224
|
+
const scrollTop = _canvasArea.scrollTop;
|
|
225
|
+
|
|
226
|
+
const curX = e.clientX - canvasRect.left + scrollLeft;
|
|
227
|
+
const curY = e.clientY - canvasRect.top + scrollTop;
|
|
228
|
+
|
|
229
|
+
const left = Math.min(curX, _marqueeStart.x);
|
|
230
|
+
const top = Math.min(curY, _marqueeStart.y);
|
|
231
|
+
const width = Math.abs(curX - _marqueeStart.x);
|
|
232
|
+
const height = Math.abs(curY - _marqueeStart.y);
|
|
233
|
+
|
|
234
|
+
_marqueeDiv.style.left = left + 'px';
|
|
235
|
+
_marqueeDiv.style.top = top + 'px';
|
|
236
|
+
_marqueeDiv.style.width = width + 'px';
|
|
237
|
+
_marqueeDiv.style.height = height + 'px';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function _onMarqueeEnd(e) {
|
|
241
|
+
document.removeEventListener('pointermove', _onMarqueeMove);
|
|
242
|
+
document.removeEventListener('pointerup', _onMarqueeEnd);
|
|
243
|
+
|
|
244
|
+
if (!_marqueeDiv || !_marqueeStart) return;
|
|
245
|
+
|
|
246
|
+
const marqueeRect = {
|
|
247
|
+
left: parseFloat(_marqueeDiv.style.left),
|
|
248
|
+
top: parseFloat(_marqueeDiv.style.top),
|
|
249
|
+
width: parseFloat(_marqueeDiv.style.width),
|
|
250
|
+
height: parseFloat(_marqueeDiv.style.height),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
_marqueeDiv.remove();
|
|
254
|
+
_marqueeDiv = null;
|
|
255
|
+
_marqueeStart = null;
|
|
256
|
+
|
|
257
|
+
// Skip tiny / accidental drags
|
|
258
|
+
if (marqueeRect.width < 4 || marqueeRect.height < 4) return;
|
|
259
|
+
|
|
260
|
+
// Select all elements whose canvas rects intersect the marquee
|
|
261
|
+
if (!_iframeDoc || !_iframe) return;
|
|
262
|
+
|
|
263
|
+
import('./coords.js').then(({ getElementCanvasRect }) => {
|
|
264
|
+
const candidates = _iframeDoc.querySelectorAll('[data-hc-id]');
|
|
265
|
+
for (const el of candidates) {
|
|
266
|
+
try {
|
|
267
|
+
const r = getElementCanvasRect(el, _iframe, _canvasArea);
|
|
268
|
+
const elRect = { left: r.x !== undefined ? r.x : r.left, top: r.y !== undefined ? r.y : r.top, width: r.width, height: r.height };
|
|
269
|
+
if (_rectsIntersect(marqueeRect, elRect)) {
|
|
270
|
+
addToSelection(el);
|
|
271
|
+
}
|
|
272
|
+
} catch { /* skip elements that error */ }
|
|
273
|
+
}
|
|
274
|
+
}).catch(() => {});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function _rectsIntersect(a, b) {
|
|
278
|
+
return !(a.left + a.width < b.left ||
|
|
279
|
+
b.left + b.width < a.left ||
|
|
280
|
+
a.top + a.height < b.top ||
|
|
281
|
+
b.top + b.height < a.top);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Grouping ──────────────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Returns the highest hc-N numeric suffix in iframeDoc.
|
|
288
|
+
*/
|
|
289
|
+
function _getNextHcId(iframeDoc) {
|
|
290
|
+
const allTagged = iframeDoc.querySelectorAll('[data-hc-id]');
|
|
291
|
+
let max = 0;
|
|
292
|
+
for (const el of allTagged) {
|
|
293
|
+
const id = el.getAttribute ? el.getAttribute('data-hc-id') : null;
|
|
294
|
+
if (!id) continue;
|
|
295
|
+
const m = id.match(/^hc-(\d+)$/);
|
|
296
|
+
if (m) max = Math.max(max, parseInt(m[1], 10));
|
|
297
|
+
}
|
|
298
|
+
return max + 1;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Creates a Command that wraps the given elements in a data-hc-group div.
|
|
303
|
+
*
|
|
304
|
+
* @param {Element[]} elements - Live iframe DOM elements to group
|
|
305
|
+
* @param {string[]} hcIds - Their data-hc-id values
|
|
306
|
+
* @param {Document} iframeDoc - iframe contentDocument
|
|
307
|
+
* @returns {{ execute(), undo(), description: string }}
|
|
308
|
+
*/
|
|
309
|
+
export function makeGroupCommand(elements, hcIds, iframeDoc) {
|
|
310
|
+
// Snapshot parent + sibling references before execute
|
|
311
|
+
const snapshots = elements.map(el => ({
|
|
312
|
+
el,
|
|
313
|
+
parent: el.parentElement,
|
|
314
|
+
nextSibling: el.nextSibling,
|
|
315
|
+
}));
|
|
316
|
+
|
|
317
|
+
// Compute bounding box using getBoundingClientRect (works in tests + browser)
|
|
318
|
+
let minLeft = Infinity, minTop = Infinity, maxRight = -Infinity, maxBottom = -Infinity;
|
|
319
|
+
for (const el of elements) {
|
|
320
|
+
const r = el.getBoundingClientRect ? el.getBoundingClientRect() : { left: 0, top: 0, right: 100, bottom: 100 };
|
|
321
|
+
minLeft = Math.min(minLeft, r.left);
|
|
322
|
+
minTop = Math.min(minTop, r.top);
|
|
323
|
+
maxRight = Math.max(maxRight, r.right || (r.left + (r.width || 0)));
|
|
324
|
+
maxBottom = Math.max(maxBottom, r.bottom || (r.top + (r.height || 0)));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Create wrapper element
|
|
328
|
+
const wrapper = iframeDoc.createElement('div');
|
|
329
|
+
wrapper.setAttribute('data-hc-group', '');
|
|
330
|
+
const wrapperHcId = `hc-${_getNextHcId(iframeDoc)}`;
|
|
331
|
+
wrapper.setAttribute('data-hc-id', wrapperHcId);
|
|
332
|
+
wrapper._style = wrapper._style || {};
|
|
333
|
+
wrapper.style.position = 'relative';
|
|
334
|
+
|
|
335
|
+
// Reference parent (the common parent — use the first element's parent)
|
|
336
|
+
const insertParent = snapshots[0].parent;
|
|
337
|
+
const insertBefore = snapshots[0].nextSibling;
|
|
338
|
+
|
|
339
|
+
let _wrapperParent = null;
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
description: 'Group elements',
|
|
343
|
+
|
|
344
|
+
execute() {
|
|
345
|
+
// Insert wrapper before first element's original position
|
|
346
|
+
if (insertParent && insertBefore) {
|
|
347
|
+
insertParent.insertBefore(wrapper, insertBefore);
|
|
348
|
+
} else if (insertParent) {
|
|
349
|
+
insertParent.appendChild(wrapper);
|
|
350
|
+
}
|
|
351
|
+
_wrapperParent = insertParent;
|
|
352
|
+
|
|
353
|
+
// Move all elements into wrapper
|
|
354
|
+
for (const { el } of snapshots) {
|
|
355
|
+
wrapper.appendChild(el);
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
undo() {
|
|
360
|
+
// Move children back to original parents at original positions
|
|
361
|
+
for (const { el, parent, nextSibling } of snapshots) {
|
|
362
|
+
if (parent) {
|
|
363
|
+
if (nextSibling && nextSibling.parentElement === parent) {
|
|
364
|
+
parent.insertBefore(el, nextSibling);
|
|
365
|
+
} else {
|
|
366
|
+
parent.appendChild(el);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Remove wrapper from its parent
|
|
372
|
+
if (wrapper.parentElement) {
|
|
373
|
+
wrapper.parentElement._children = wrapper.parentElement._children
|
|
374
|
+
? wrapper.parentElement._children.filter(c => c !== wrapper)
|
|
375
|
+
: undefined;
|
|
376
|
+
wrapper.remove ? wrapper.remove() : (wrapper.parentElement._children = (wrapper.parentElement._children || []).filter(c => c !== wrapper));
|
|
377
|
+
} else if (_wrapperParent) {
|
|
378
|
+
if (_wrapperParent._children) {
|
|
379
|
+
_wrapperParent._children = _wrapperParent._children.filter(c => c !== wrapper);
|
|
380
|
+
}
|
|
381
|
+
wrapper._parent = null;
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Creates a Command that removes a data-hc-group wrapper, moving its children
|
|
389
|
+
* back to the wrapper's parent.
|
|
390
|
+
*
|
|
391
|
+
* @param {Element} groupWrapper - The wrapper div with data-hc-group
|
|
392
|
+
* @param {Document} iframeDoc - iframe contentDocument
|
|
393
|
+
* @returns {{ execute(), undo(), description: string }}
|
|
394
|
+
*/
|
|
395
|
+
export function makeUngroupCommand(groupWrapper, iframeDoc) {
|
|
396
|
+
const parent = groupWrapper.parentElement || groupWrapper._parent;
|
|
397
|
+
const children = groupWrapper._children
|
|
398
|
+
? [...groupWrapper._children]
|
|
399
|
+
: (groupWrapper.children ? [...groupWrapper.children] : []);
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
description: 'Ungroup elements',
|
|
403
|
+
|
|
404
|
+
execute() {
|
|
405
|
+
// Move children out of wrapper to parent (before wrapper)
|
|
406
|
+
for (const child of children) {
|
|
407
|
+
if (parent) {
|
|
408
|
+
if (groupWrapper.parentElement === parent) {
|
|
409
|
+
parent.insertBefore(child, groupWrapper);
|
|
410
|
+
} else {
|
|
411
|
+
parent.appendChild(child);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Remove wrapper
|
|
417
|
+
if (groupWrapper.remove && groupWrapper.parentElement) {
|
|
418
|
+
groupWrapper.remove();
|
|
419
|
+
} else if (parent && parent._children) {
|
|
420
|
+
parent._children = parent._children.filter(c => c !== groupWrapper);
|
|
421
|
+
groupWrapper._parent = null;
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
undo() {
|
|
426
|
+
// Re-create wrapper structure: insert wrapper back into parent
|
|
427
|
+
if (parent) {
|
|
428
|
+
parent.appendChild(groupWrapper);
|
|
429
|
+
}
|
|
430
|
+
// Move children back into wrapper
|
|
431
|
+
for (const child of children) {
|
|
432
|
+
groupWrapper.appendChild ? groupWrapper.appendChild(child) : groupWrapper._children.push(child);
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|