@growgroup/visual-editor 0.1.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/LICENSE +21 -0
- package/NOTICE +52 -0
- package/README.md +143 -0
- package/dist/editor.css +2 -0
- package/package.json +94 -0
- package/src/components/DeckSlideRender.tsx +21 -0
- package/src/components/SaveNote.tsx +21 -0
- package/src/components/auth/AuthProvider.tsx +44 -0
- package/src/components/ui/accordion.tsx +57 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +76 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/context-menu.tsx +200 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/scrubbable-label.tsx +95 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/components/viewer/useDeck.ts +58 -0
- package/src/editor/EditorContext.tsx +632 -0
- package/src/editor/EditorToolbar.tsx +446 -0
- package/src/editor/FrontendVisualEditor.tsx +2595 -0
- package/src/editor/autosave.ts +34 -0
- package/src/editor/components/AiPromptPopover.tsx +378 -0
- package/src/editor/components/BreakpointGuides.tsx +323 -0
- package/src/editor/components/BreakpointSelector.tsx +180 -0
- package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
- package/src/editor/components/CssEditorDialog.tsx +168 -0
- package/src/editor/components/EditorCanvas.tsx +987 -0
- package/src/editor/components/EditorContextMenu.tsx +515 -0
- package/src/editor/components/EditorFooter.tsx +81 -0
- package/src/editor/components/EditorHeader.tsx +524 -0
- package/src/editor/components/EditorLayerPanel.tsx +1197 -0
- package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
- package/src/editor/components/EditorSidebar.tsx +226 -0
- package/src/editor/components/FigmaColorPicker.tsx +1577 -0
- package/src/editor/components/HtmlEditorDialog.tsx +71 -0
- package/src/editor/components/HtmlImportDialog.tsx +346 -0
- package/src/editor/components/JsEditorDialog.tsx +175 -0
- package/src/editor/components/LayerTreeItem.tsx +494 -0
- package/src/editor/components/MasterComponentEditor.tsx +846 -0
- package/src/editor/components/MediaLibraryDialog.tsx +447 -0
- package/src/editor/components/PageSettingsDialog.tsx +776 -0
- package/src/editor/components/VariablePicker.tsx +355 -0
- package/src/editor/components/VariablesDialog.tsx +564 -0
- package/src/editor/components/VariantEditor.tsx +687 -0
- package/src/editor/components/VariantMatrix.tsx +507 -0
- package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
- package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
- package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
- package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
- package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
- package/src/editor/components/component-panel/VariantList.tsx +260 -0
- package/src/editor/components/component-panel/index.ts +12 -0
- package/src/editor/components/index.ts +48 -0
- package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
- package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
- package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
- package/src/editor/components/multi-page/PageLabel.tsx +34 -0
- package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
- package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
- package/src/editor/components/multi-page/index.ts +7 -0
- package/src/editor/components/ppt/PptChrome.tsx +2508 -0
- package/src/editor/components/ppt/PptComments.tsx +589 -0
- package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
- package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
- package/src/editor/components/ppt/PptNotes.tsx +246 -0
- package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
- package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
- package/src/editor/components/property-panel/BorderSection.tsx +104 -0
- package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
- package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
- package/src/editor/components/property-panel/EffectSection.tsx +157 -0
- package/src/editor/components/property-panel/FillSection.tsx +69 -0
- package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
- package/src/editor/components/property-panel/ImageSection.tsx +206 -0
- package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
- package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
- package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
- package/src/editor/components/property-panel/LinkSection.tsx +103 -0
- package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
- package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
- package/src/editor/components/property-panel/TypographySection.tsx +230 -0
- package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
- package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
- package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
- package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
- package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
- package/src/editor/components/property-panel/index.ts +24 -0
- package/src/editor/components/property-panel/unit-utils.ts +261 -0
- package/src/editor/components/shell/EditorTopBar.tsx +392 -0
- package/src/editor/components/shell/LeftPanel.tsx +47 -0
- package/src/editor/components/shell/PagesPanel.tsx +118 -0
- package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
- package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
- package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
- package/src/editor/components/variables-panel/index.ts +7 -0
- package/src/editor/constants.ts +468 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
- package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
- package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
- package/src/editor/contexts/EditorRefsContext.tsx +204 -0
- package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
- package/src/editor/contexts/EditorToolContext.tsx +58 -0
- package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
- package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
- package/src/editor/contexts/EditorViewContext.tsx +101 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
- package/src/editor/contexts/index.ts +17 -0
- package/src/editor/editor-skin.css +122 -0
- package/src/editor/hooks/index.ts +38 -0
- package/src/editor/hooks/useAiReplace.ts +397 -0
- package/src/editor/hooks/useAltMeasure.ts +67 -0
- package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
- package/src/editor/hooks/useCanvasControls.ts +1222 -0
- package/src/editor/hooks/useComponentEditMode.ts +658 -0
- package/src/editor/hooks/useComponentInstances.ts +515 -0
- package/src/editor/hooks/useContextMenuHandler.ts +157 -0
- package/src/editor/hooks/useCoordinateTransform.ts +207 -0
- package/src/editor/hooks/useDragResize.ts +1317 -0
- package/src/editor/hooks/useDrawingMode.ts +472 -0
- package/src/editor/hooks/useEditorColors.ts +144 -0
- package/src/editor/hooks/useEditorMessages.ts +108 -0
- package/src/editor/hooks/useElementActions.ts +1754 -0
- package/src/editor/hooks/useElementSelection.ts +1573 -0
- package/src/editor/hooks/useFocusManagement.ts +249 -0
- package/src/editor/hooks/useGoogleFonts.ts +305 -0
- package/src/editor/hooks/useIframeInitializer.ts +584 -0
- package/src/editor/hooks/useIframeSetup.ts +360 -0
- package/src/editor/hooks/useImageUpload.ts +270 -0
- package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
- package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
- package/src/editor/hooks/useMarqueeSelection.ts +388 -0
- package/src/editor/hooks/useMediaLibrary.ts +129 -0
- package/src/editor/hooks/usePageSettingsManager.ts +739 -0
- package/src/editor/hooks/useResizablePanel.ts +163 -0
- package/src/editor/hooks/useRichPaste.ts +255 -0
- package/src/editor/hooks/useTouchGestures.ts +197 -0
- package/src/editor/index.ts +27 -0
- package/src/editor/types/page-settings.ts +513 -0
- package/src/editor/types.ts +382 -0
- package/src/editor/utils/align-elements.ts +169 -0
- package/src/editor/utils/api-json.ts +21 -0
- package/src/editor/utils/aspect-lock.ts +50 -0
- package/src/editor/utils/component-renderer.ts +702 -0
- package/src/editor/utils/component-sync.ts +845 -0
- package/src/editor/utils/crop-mode.ts +265 -0
- package/src/editor/utils/dom-utils.ts +2238 -0
- package/src/editor/utils/element-effects.ts +218 -0
- package/src/editor/utils/eyedropper.ts +35 -0
- package/src/editor/utils/figma-export.ts +560 -0
- package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
- package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
- package/src/editor/utils/figma-paste.ts +2262 -0
- package/src/editor/utils/flex-reorder.ts +231 -0
- package/src/editor/utils/flex-utils.ts +1230 -0
- package/src/editor/utils/geometry.ts +232 -0
- package/src/editor/utils/grid-layout.ts +210 -0
- package/src/editor/utils/html-utils.ts +470 -0
- package/src/editor/utils/index.ts +11 -0
- package/src/editor/utils/ink-style.ts +57 -0
- package/src/editor/utils/inline-format.ts +236 -0
- package/src/editor/utils/measure-distance.ts +120 -0
- package/src/editor/utils/override-apply.ts +526 -0
- package/src/editor/utils/override-detection.ts +613 -0
- package/src/editor/utils/paste-processors.ts +609 -0
- package/src/editor/utils/paste-sanitizer.ts +244 -0
- package/src/editor/utils/restore-flow.ts +188 -0
- package/src/editor/utils/shape-library.ts +171 -0
- package/src/editor/utils/slide-root.ts +29 -0
- package/src/editor/utils/smart-guides.ts +218 -0
- package/src/editor/utils/style-utils.ts +685 -0
- package/src/editor/utils/table-edit.ts +151 -0
- package/src/editor/utils/tailwind-mappings.ts +778 -0
- package/src/editor/utils/tailwind-utils.ts +804 -0
- package/src/editor/utils/text-highlight.ts +216 -0
- package/src/editor/utils/theme-to-variables.ts +341 -0
- package/src/editor/utils/variant-resolver.ts +456 -0
- package/src/editor/utils/viewport-utils.ts +295 -0
- package/src/hooks/useEditorHistory.ts +195 -0
- package/src/hooks/useEditorShortcuts.ts +260 -0
- package/src/index.ts +40 -0
- package/src/io.ts +134 -0
- package/src/lib/agent/slide-agent/types.ts +547 -0
- package/src/lib/agent/website-agent/types.ts +805 -0
- package/src/lib/api/auth-fetch.ts +58 -0
- package/src/lib/deck.ts +69 -0
- package/src/lib/export.ts +47 -0
- package/src/lib/firebase/config.ts +29 -0
- package/src/lib/firebase/css-variables.ts +378 -0
- package/src/lib/firebase/editor-components.ts +677 -0
- package/src/lib/firebase/storage.ts +469 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/editor.css +52 -0
- package/src/styles/skin.css +122 -0
- package/src/types/css-variables.ts +645 -0
- package/src/types/editor-components.ts +944 -0
- package/src/types/editor.ts +420 -0
- package/src/types/page-master.ts +197 -0
- package/src/types/page.ts +419 -0
- package/src/types/slide.ts +402 -0
- package/src/types/website-theme.ts +788 -0
- package/src/types/website.ts +374 -0
- package/src/vendor/firebase-firestore.ts +13 -0
- package/src/vendor/firebase-functions.ts +17 -0
|
@@ -0,0 +1,1230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flexbox drag-to-reorder utilities
|
|
3
|
+
* Concrete5-style element reordering in auto-layout containers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Information about a flex container
|
|
8
|
+
*/
|
|
9
|
+
export interface FlexContainerInfo {
|
|
10
|
+
isFlexContainer: boolean;
|
|
11
|
+
container: HTMLElement;
|
|
12
|
+
direction: 'row' | 'column' | 'row-reverse' | 'column-reverse';
|
|
13
|
+
isReverse: boolean;
|
|
14
|
+
children: HTMLElement[];
|
|
15
|
+
childIndex: number;
|
|
16
|
+
gap: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get information about the flex container of an element
|
|
21
|
+
*/
|
|
22
|
+
export function getFlexContainerInfo(element: HTMLElement): FlexContainerInfo | null {
|
|
23
|
+
const parent = element.parentElement;
|
|
24
|
+
if (!parent) return null;
|
|
25
|
+
|
|
26
|
+
const style = window.getComputedStyle(parent);
|
|
27
|
+
const display = style.display;
|
|
28
|
+
|
|
29
|
+
if (display !== 'flex' && display !== 'inline-flex') {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const direction = (style.flexDirection || 'row') as FlexContainerInfo['direction'];
|
|
34
|
+
const isReverse = direction.includes('reverse');
|
|
35
|
+
|
|
36
|
+
// Get child elements excluding UI elements
|
|
37
|
+
const children = Array.from(parent.children).filter(child => {
|
|
38
|
+
const el = child as HTMLElement;
|
|
39
|
+
return !el.classList.contains('selection-box') &&
|
|
40
|
+
!el.classList.contains('resize-handle') &&
|
|
41
|
+
!el.classList.contains('flex-drop-indicator') &&
|
|
42
|
+
el.tagName !== 'SCRIPT' &&
|
|
43
|
+
el.tagName !== 'STYLE';
|
|
44
|
+
}) as HTMLElement[];
|
|
45
|
+
|
|
46
|
+
const childIndex = children.indexOf(element);
|
|
47
|
+
const gap = parseFloat(style.gap) || 0;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
isFlexContainer: true,
|
|
51
|
+
container: parent,
|
|
52
|
+
direction,
|
|
53
|
+
isReverse,
|
|
54
|
+
children,
|
|
55
|
+
childIndex,
|
|
56
|
+
gap,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if drag should trigger flex reordering
|
|
62
|
+
*/
|
|
63
|
+
export function canReorderInFlex(element: HTMLElement): boolean {
|
|
64
|
+
const info = getFlexContainerInfo(element);
|
|
65
|
+
return info !== null && info.childIndex !== -1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Calculate the target drop index based on cursor position
|
|
70
|
+
*/
|
|
71
|
+
export function calculateDropIndex(
|
|
72
|
+
parent: HTMLElement,
|
|
73
|
+
cursorX: number,
|
|
74
|
+
cursorY: number,
|
|
75
|
+
iframeWindow: Window
|
|
76
|
+
): number {
|
|
77
|
+
const style = iframeWindow.getComputedStyle(parent);
|
|
78
|
+
const direction = style.flexDirection || 'row';
|
|
79
|
+
const isColumn = direction === 'column' || direction === 'column-reverse';
|
|
80
|
+
const isReverse = direction.includes('reverse');
|
|
81
|
+
|
|
82
|
+
// Get child elements excluding UI elements
|
|
83
|
+
const children = Array.from(parent.children).filter(child => {
|
|
84
|
+
const el = child as HTMLElement;
|
|
85
|
+
return !el.classList.contains('selection-box') &&
|
|
86
|
+
!el.classList.contains('resize-handle') &&
|
|
87
|
+
!el.classList.contains('flex-drop-indicator') &&
|
|
88
|
+
el.tagName !== 'SCRIPT' &&
|
|
89
|
+
el.tagName !== 'STYLE';
|
|
90
|
+
}) as HTMLElement[];
|
|
91
|
+
|
|
92
|
+
if (children.length === 0) return 0;
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < children.length; i++) {
|
|
95
|
+
const child = children[i];
|
|
96
|
+
const rect = child.getBoundingClientRect();
|
|
97
|
+
|
|
98
|
+
if (isColumn) {
|
|
99
|
+
// For column layouts, check vertical position
|
|
100
|
+
const childMidY = rect.top + rect.height / 2;
|
|
101
|
+
if (cursorY < childMidY) {
|
|
102
|
+
return isReverse ? children.length - i : i;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
// For row layouts, check horizontal position
|
|
106
|
+
const childMidX = rect.left + rect.width / 2;
|
|
107
|
+
if (cursorX < childMidX) {
|
|
108
|
+
return isReverse ? children.length - i : i;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Cursor is after all children
|
|
114
|
+
return isReverse ? 0 : children.length;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Show the drop indicator at the target position
|
|
119
|
+
*/
|
|
120
|
+
export function showFlexDropIndicator(
|
|
121
|
+
iframeDoc: Document,
|
|
122
|
+
parent: HTMLElement,
|
|
123
|
+
targetIndex: number,
|
|
124
|
+
currentDragElement?: HTMLElement
|
|
125
|
+
): void {
|
|
126
|
+
// Remove existing indicator
|
|
127
|
+
hideFlexDropIndicator(iframeDoc);
|
|
128
|
+
|
|
129
|
+
const style = iframeDoc.defaultView?.getComputedStyle(parent);
|
|
130
|
+
if (!style) return;
|
|
131
|
+
|
|
132
|
+
// Use the same layout detection logic for consistency
|
|
133
|
+
const { isColumn } = isColumnLayout(parent, iframeDoc);
|
|
134
|
+
const gap = parseFloat(style.gap) || 0;
|
|
135
|
+
|
|
136
|
+
// Get child elements excluding UI elements and the element being dragged
|
|
137
|
+
const children = Array.from(parent.children).filter(child => {
|
|
138
|
+
const el = child as HTMLElement;
|
|
139
|
+
return !el.classList.contains('selection-box') &&
|
|
140
|
+
!el.classList.contains('resize-handle') &&
|
|
141
|
+
!el.classList.contains('flex-drop-indicator') &&
|
|
142
|
+
el.tagName !== 'SCRIPT' &&
|
|
143
|
+
el.tagName !== 'STYLE' &&
|
|
144
|
+
el !== currentDragElement;
|
|
145
|
+
}) as HTMLElement[];
|
|
146
|
+
|
|
147
|
+
const indicator = iframeDoc.createElement('div');
|
|
148
|
+
indicator.className = 'flex-drop-indicator';
|
|
149
|
+
indicator.style.cssText = `
|
|
150
|
+
position: absolute;
|
|
151
|
+
pointer-events: none;
|
|
152
|
+
z-index: 10000;
|
|
153
|
+
background: #6366F1;
|
|
154
|
+
border-radius: 1.5px;
|
|
155
|
+
box-shadow: 0 0 6px rgba(99, 102, 241, 0.4);
|
|
156
|
+
opacity: 0.9;
|
|
157
|
+
`;
|
|
158
|
+
|
|
159
|
+
// Add keyframes for nesting animation if not already present
|
|
160
|
+
if (!iframeDoc.getElementById('drop-indicator-styles')) {
|
|
161
|
+
const styleSheet = iframeDoc.createElement('style');
|
|
162
|
+
styleSheet.id = 'drop-indicator-styles';
|
|
163
|
+
styleSheet.textContent = `
|
|
164
|
+
@keyframes nestingPulse {
|
|
165
|
+
0%, 100% { opacity: 0.9; }
|
|
166
|
+
50% { opacity: 0.7; }
|
|
167
|
+
}
|
|
168
|
+
`;
|
|
169
|
+
iframeDoc.head.appendChild(styleSheet);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const parentRect = parent.getBoundingClientRect();
|
|
173
|
+
const scrollLeft = iframeDoc.defaultView?.scrollX || 0;
|
|
174
|
+
const scrollTop = iframeDoc.defaultView?.scrollY || 0;
|
|
175
|
+
|
|
176
|
+
if (children.length === 0 || targetIndex === 0) {
|
|
177
|
+
// Insert at beginning or empty container
|
|
178
|
+
if (isColumn) {
|
|
179
|
+
indicator.style.left = `${parentRect.left + scrollLeft}px`;
|
|
180
|
+
indicator.style.top = `${parentRect.top + scrollTop}px`;
|
|
181
|
+
indicator.style.width = `${parentRect.width}px`;
|
|
182
|
+
indicator.style.height = '3px';
|
|
183
|
+
} else {
|
|
184
|
+
indicator.style.left = `${parentRect.left + scrollLeft}px`;
|
|
185
|
+
indicator.style.top = `${parentRect.top + scrollTop}px`;
|
|
186
|
+
indicator.style.width = '3px';
|
|
187
|
+
indicator.style.height = `${parentRect.height}px`;
|
|
188
|
+
}
|
|
189
|
+
} else if (targetIndex >= children.length) {
|
|
190
|
+
// Insert at end
|
|
191
|
+
const lastChild = children[children.length - 1];
|
|
192
|
+
const lastRect = lastChild.getBoundingClientRect();
|
|
193
|
+
|
|
194
|
+
if (isColumn) {
|
|
195
|
+
indicator.style.left = `${parentRect.left + scrollLeft}px`;
|
|
196
|
+
indicator.style.top = `${lastRect.bottom + gap / 2 + scrollTop}px`;
|
|
197
|
+
indicator.style.width = `${parentRect.width}px`;
|
|
198
|
+
indicator.style.height = '3px';
|
|
199
|
+
} else {
|
|
200
|
+
indicator.style.left = `${lastRect.right + gap / 2 + scrollLeft}px`;
|
|
201
|
+
indicator.style.top = `${parentRect.top + scrollTop}px`;
|
|
202
|
+
indicator.style.width = '3px';
|
|
203
|
+
indicator.style.height = `${parentRect.height}px`;
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
// Insert between elements
|
|
207
|
+
const targetChild = children[targetIndex];
|
|
208
|
+
const targetRect = targetChild.getBoundingClientRect();
|
|
209
|
+
|
|
210
|
+
if (isColumn) {
|
|
211
|
+
indicator.style.left = `${parentRect.left + scrollLeft}px`;
|
|
212
|
+
indicator.style.top = `${targetRect.top - gap / 2 + scrollTop}px`;
|
|
213
|
+
indicator.style.width = `${parentRect.width}px`;
|
|
214
|
+
indicator.style.height = '3px';
|
|
215
|
+
} else {
|
|
216
|
+
indicator.style.left = `${targetRect.left - gap / 2 + scrollLeft}px`;
|
|
217
|
+
indicator.style.top = `${parentRect.top + scrollTop}px`;
|
|
218
|
+
indicator.style.width = '3px';
|
|
219
|
+
indicator.style.height = `${parentRect.height}px`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
iframeDoc.body.appendChild(indicator);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Hide and remove the drop indicator
|
|
228
|
+
*/
|
|
229
|
+
export function hideFlexDropIndicator(iframeDoc: Document): void {
|
|
230
|
+
const indicators = iframeDoc.querySelectorAll('.flex-drop-indicator');
|
|
231
|
+
indicators.forEach(indicator => indicator.remove());
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Perform the actual DOM reordering
|
|
236
|
+
*/
|
|
237
|
+
export function reorderFlexChild(
|
|
238
|
+
parent: HTMLElement,
|
|
239
|
+
element: HTMLElement,
|
|
240
|
+
targetIndex: number
|
|
241
|
+
): boolean {
|
|
242
|
+
// Safety check: element must be a child of parent
|
|
243
|
+
if (element.parentElement !== parent) {
|
|
244
|
+
console.warn('[flex-utils] reorderFlexChild: element is not a child of the specified parent');
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Get child elements excluding UI elements
|
|
249
|
+
const children = Array.from(parent.children).filter(child => {
|
|
250
|
+
const el = child as HTMLElement;
|
|
251
|
+
return !el.classList.contains('selection-box') &&
|
|
252
|
+
!el.classList.contains('resize-handle') &&
|
|
253
|
+
!el.classList.contains('flex-drop-indicator') &&
|
|
254
|
+
!el.classList.contains('drag-ghost') &&
|
|
255
|
+
!el.classList.contains('nesting-drop-indicator') &&
|
|
256
|
+
el.tagName !== 'SCRIPT' &&
|
|
257
|
+
el.tagName !== 'STYLE';
|
|
258
|
+
}) as HTMLElement[];
|
|
259
|
+
|
|
260
|
+
const currentIndex = children.indexOf(element);
|
|
261
|
+
|
|
262
|
+
if (currentIndex === -1) {
|
|
263
|
+
console.warn('[flex-utils] reorderFlexChild: element not found in valid children');
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (currentIndex === targetIndex) {
|
|
268
|
+
return false; // No change needed - element is already at target position
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Clamp target index to valid range
|
|
272
|
+
const clampedTargetIndex = Math.max(0, Math.min(targetIndex, children.length - 1));
|
|
273
|
+
|
|
274
|
+
if (currentIndex === clampedTargetIndex) {
|
|
275
|
+
return false; // No change needed after clamping
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (clampedTargetIndex >= children.length - 1 && currentIndex < children.length - 1) {
|
|
279
|
+
// Move to end (but not if already at end)
|
|
280
|
+
parent.appendChild(element);
|
|
281
|
+
} else {
|
|
282
|
+
// Insert at target position
|
|
283
|
+
const targetElement = children[clampedTargetIndex];
|
|
284
|
+
|
|
285
|
+
// Safety check: don't insert element before/after itself
|
|
286
|
+
if (targetElement === element) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (currentIndex < clampedTargetIndex) {
|
|
291
|
+
// Moving forward: insert after target
|
|
292
|
+
if (targetElement.nextSibling !== element) {
|
|
293
|
+
targetElement.insertAdjacentElement('afterend', element);
|
|
294
|
+
} else {
|
|
295
|
+
return false; // Already in correct position
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
// Moving backward: insert before target
|
|
299
|
+
if (parent.children[clampedTargetIndex] !== element) {
|
|
300
|
+
parent.insertBefore(element, targetElement);
|
|
301
|
+
} else {
|
|
302
|
+
return false; // Already in correct position
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ============================================================
|
|
311
|
+
// Enhanced Auto-Layout Drag Utilities (Ghost Clone Approach)
|
|
312
|
+
// ============================================================
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get valid children of a container (excluding UI elements and placeholders)
|
|
316
|
+
*/
|
|
317
|
+
export function getValidChildren(parent: HTMLElement): HTMLElement[] {
|
|
318
|
+
return Array.from(parent.children).filter(child => {
|
|
319
|
+
const el = child as HTMLElement;
|
|
320
|
+
return !el.classList.contains('selection-box') &&
|
|
321
|
+
!el.classList.contains('resize-handle') &&
|
|
322
|
+
!el.classList.contains('flex-drop-indicator') &&
|
|
323
|
+
!el.classList.contains('drag-ghost') &&
|
|
324
|
+
!el.classList.contains('auto-layout-drop-indicator') &&
|
|
325
|
+
!el.classList.contains('drag-placeholder') &&
|
|
326
|
+
!el.classList.contains('drop-placeholder') &&
|
|
327
|
+
!el.dataset.dragUi &&
|
|
328
|
+
el.tagName !== 'SCRIPT' &&
|
|
329
|
+
el.tagName !== 'STYLE';
|
|
330
|
+
}) as HTMLElement[];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Drag state for auto-layout
|
|
335
|
+
*/
|
|
336
|
+
export interface AutoLayoutDragState {
|
|
337
|
+
element: HTMLElement;
|
|
338
|
+
ghost: HTMLElement;
|
|
339
|
+
originalRect: DOMRect;
|
|
340
|
+
originalParent: HTMLElement;
|
|
341
|
+
originalIndex: number;
|
|
342
|
+
currentDropIndex: number;
|
|
343
|
+
/** Canvas zoom scale (1 = 100%) */
|
|
344
|
+
scale: number;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Create a clean clone of element for drag ghost
|
|
349
|
+
*/
|
|
350
|
+
function createCleanGhost(element: HTMLElement, iframeDoc: Document): HTMLElement {
|
|
351
|
+
// Clone the element
|
|
352
|
+
const ghost = element.cloneNode(true) as HTMLElement;
|
|
353
|
+
|
|
354
|
+
// Remove all UI-related elements from the clone
|
|
355
|
+
const uiSelectors = [
|
|
356
|
+
'.selection-box',
|
|
357
|
+
'.resize-handle',
|
|
358
|
+
'.flex-drop-indicator',
|
|
359
|
+
'.drag-ghost',
|
|
360
|
+
'.auto-layout-drop-indicator',
|
|
361
|
+
'[data-drag-ui]',
|
|
362
|
+
];
|
|
363
|
+
uiSelectors.forEach(selector => {
|
|
364
|
+
ghost.querySelectorAll(selector).forEach(el => el.remove());
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Remove any data attributes that might cause issues
|
|
368
|
+
ghost.removeAttribute('data-selected');
|
|
369
|
+
ghost.classList.remove('selected');
|
|
370
|
+
|
|
371
|
+
return ghost;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Start auto-layout drag - creates ghost clone and dims original element
|
|
376
|
+
* @param element The element being dragged
|
|
377
|
+
* @param iframeDoc The iframe document
|
|
378
|
+
* @param scale Canvas zoom scale (1 = 100%, 1.5 = 150%, etc.)
|
|
379
|
+
*/
|
|
380
|
+
export function startAutoLayoutDrag(
|
|
381
|
+
element: HTMLElement,
|
|
382
|
+
iframeDoc: Document,
|
|
383
|
+
scale: number = 1
|
|
384
|
+
): AutoLayoutDragState {
|
|
385
|
+
const originalRect = element.getBoundingClientRect();
|
|
386
|
+
const originalParent = element.parentElement!;
|
|
387
|
+
|
|
388
|
+
// Get original index among siblings
|
|
389
|
+
const siblings = getValidChildren(originalParent);
|
|
390
|
+
const originalIndex = siblings.indexOf(element);
|
|
391
|
+
|
|
392
|
+
// Create a clean ghost clone (preserves original classes)
|
|
393
|
+
const ghost = createCleanGhost(element, iframeDoc);
|
|
394
|
+
// Add drag-ghost class while preserving original classes for styling
|
|
395
|
+
ghost.classList.add('drag-ghost');
|
|
396
|
+
|
|
397
|
+
// When canvas is zoomed, getBoundingClientRect returns screen coords (post-scale).
|
|
398
|
+
// We need to set ghost to unscaled size and apply scale transform so internal
|
|
399
|
+
// content (fonts, etc.) scales correctly to match the zoomed canvas appearance.
|
|
400
|
+
const unscaledWidth = originalRect.width / scale;
|
|
401
|
+
const unscaledHeight = originalRect.height / scale;
|
|
402
|
+
|
|
403
|
+
// Apply positioning/layering styles with zoom compensation
|
|
404
|
+
// Use transform-origin: 0 0 so scaling happens from top-left corner (matches position)
|
|
405
|
+
ghost.style.position = 'fixed';
|
|
406
|
+
ghost.style.left = `${originalRect.left}px`;
|
|
407
|
+
ghost.style.top = `${originalRect.top}px`;
|
|
408
|
+
ghost.style.width = `${unscaledWidth}px`;
|
|
409
|
+
ghost.style.height = `${unscaledHeight}px`;
|
|
410
|
+
ghost.style.pointerEvents = 'none';
|
|
411
|
+
ghost.style.zIndex = '100000';
|
|
412
|
+
ghost.style.opacity = '0.9';
|
|
413
|
+
ghost.style.boxShadow = '0 2px 8px rgba(0,0,0,0.08)';
|
|
414
|
+
ghost.style.transformOrigin = '0 0';
|
|
415
|
+
ghost.style.transform = `scale(${scale})`;
|
|
416
|
+
ghost.style.margin = '0';
|
|
417
|
+
|
|
418
|
+
iframeDoc.body.appendChild(ghost);
|
|
419
|
+
|
|
420
|
+
// Subtle pickup animation (ghost only) - maintain scale while animating
|
|
421
|
+
requestAnimationFrame(() => {
|
|
422
|
+
ghost.style.transition = 'transform 0.15s ease-out, box-shadow 0.15s ease-out';
|
|
423
|
+
ghost.style.transform = `scale(${scale * 1.01})`;
|
|
424
|
+
ghost.style.boxShadow = '0 8px 24px rgba(0,0,0,0.12)';
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Dim the original element (minimal styles - NO transform/transition to avoid affecting absolute mode)
|
|
428
|
+
element.style.opacity = '0.4';
|
|
429
|
+
element.dataset.dragging = 'true';
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
element,
|
|
433
|
+
ghost,
|
|
434
|
+
originalRect,
|
|
435
|
+
originalParent,
|
|
436
|
+
originalIndex,
|
|
437
|
+
currentDropIndex: originalIndex,
|
|
438
|
+
scale,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Store previous position (kept for potential future use)
|
|
443
|
+
let prevDragX = 0;
|
|
444
|
+
let prevDragY = 0;
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Update ghost position to follow cursor (subtle movement, no rotation)
|
|
448
|
+
*/
|
|
449
|
+
export function updateDragPosition(
|
|
450
|
+
state: AutoLayoutDragState,
|
|
451
|
+
cursorX: number,
|
|
452
|
+
cursorY: number
|
|
453
|
+
): void {
|
|
454
|
+
const { ghost, originalRect, scale } = state;
|
|
455
|
+
|
|
456
|
+
// The ghost's visual size on screen is originalRect.width/height (after scaling)
|
|
457
|
+
// Center the ghost on cursor using screen dimensions
|
|
458
|
+
const targetLeft = cursorX - originalRect.width / 2;
|
|
459
|
+
const targetTop = cursorY - originalRect.height / 2;
|
|
460
|
+
|
|
461
|
+
ghost.style.left = `${targetLeft}px`;
|
|
462
|
+
ghost.style.top = `${targetTop}px`;
|
|
463
|
+
// Keep the subtle scale applied with zoom compensation
|
|
464
|
+
ghost.style.transform = `scale(${scale * 1.01})`;
|
|
465
|
+
|
|
466
|
+
// Store current position for next frame (kept for potential future use)
|
|
467
|
+
prevDragX = cursorX;
|
|
468
|
+
prevDragY = cursorY;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* End auto-layout drag - move element to new position with animation
|
|
473
|
+
*/
|
|
474
|
+
export function endAutoLayoutDrag(
|
|
475
|
+
state: AutoLayoutDragState,
|
|
476
|
+
dropInfo: DropTargetInfo | null,
|
|
477
|
+
iframeDoc: Document
|
|
478
|
+
): boolean {
|
|
479
|
+
const { element, ghost, originalParent, originalIndex, scale } = state;
|
|
480
|
+
|
|
481
|
+
// Debug logging
|
|
482
|
+
console.log('[endAutoLayoutDrag] Starting:', {
|
|
483
|
+
elementTag: element?.tagName,
|
|
484
|
+
elementId: element?.id,
|
|
485
|
+
elementClass: element?.className,
|
|
486
|
+
hasParent: !!element?.parentElement,
|
|
487
|
+
parentTag: element?.parentElement?.tagName,
|
|
488
|
+
parentId: element?.parentElement?.id,
|
|
489
|
+
originalParentTag: originalParent?.tagName,
|
|
490
|
+
originalParentId: originalParent?.id,
|
|
491
|
+
originalIndex,
|
|
492
|
+
dropInfo: dropInfo ? {
|
|
493
|
+
containerTag: dropInfo.container?.tagName,
|
|
494
|
+
containerId: dropInfo.container?.id,
|
|
495
|
+
position: dropInfo.position,
|
|
496
|
+
index: dropInfo.index,
|
|
497
|
+
} : null,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Remove drop indicators
|
|
501
|
+
hideFlexDropIndicator(iframeDoc);
|
|
502
|
+
hideNestingIndicator(iframeDoc);
|
|
503
|
+
|
|
504
|
+
// Reset position tracking
|
|
505
|
+
prevDragX = 0;
|
|
506
|
+
prevDragY = 0;
|
|
507
|
+
|
|
508
|
+
// Subtle fade animation for ghost - maintain zoom scale
|
|
509
|
+
ghost.style.transition = 'transform 0.15s ease-out, opacity 0.15s ease-out';
|
|
510
|
+
ghost.style.transform = `scale(${scale})`;
|
|
511
|
+
ghost.style.opacity = '0.5';
|
|
512
|
+
|
|
513
|
+
// Restore original element immediately (NO transition/transform - keep it simple to avoid affecting absolute mode)
|
|
514
|
+
element.style.opacity = '';
|
|
515
|
+
delete element.dataset.dragging;
|
|
516
|
+
|
|
517
|
+
let moved = false;
|
|
518
|
+
|
|
519
|
+
// Get the current parent of the element (should match originalParent in most cases)
|
|
520
|
+
const currentParent = element.parentElement;
|
|
521
|
+
|
|
522
|
+
console.log('[endAutoLayoutDrag] Before DOM operation:', {
|
|
523
|
+
elementInDOM: element.isConnected,
|
|
524
|
+
currentParentTag: currentParent?.tagName,
|
|
525
|
+
currentParentId: currentParent?.id,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Get artboard for validation
|
|
529
|
+
const artboard = iframeDoc.querySelector('#artboard, .slide-artboard, #slide-artboard, [data-artboard]') as HTMLElement;
|
|
530
|
+
|
|
531
|
+
// Determine if dropInfo points to the same parent as the original
|
|
532
|
+
// Use multiple comparison methods for robustness
|
|
533
|
+
const isSameParent = dropInfo && dropInfo.container && (
|
|
534
|
+
dropInfo.container === originalParent ||
|
|
535
|
+
dropInfo.container === currentParent ||
|
|
536
|
+
(dropInfo.container && originalParent && dropInfo.container.isSameNode(originalParent)) ||
|
|
537
|
+
// Also compare by ID if both have IDs
|
|
538
|
+
(dropInfo.container.id && originalParent?.id && dropInfo.container.id === originalParent.id)
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
// Check if drop container is valid (inside artboard or is artboard)
|
|
542
|
+
const isValidDropLocation = dropInfo && dropInfo.container && (
|
|
543
|
+
dropInfo.container === artboard ||
|
|
544
|
+
(artboard && artboard.contains(dropInfo.container)) ||
|
|
545
|
+
dropInfo.container.id === 'artboard'
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
console.log('[endAutoLayoutDrag] isSameParent:', isSameParent, 'isValidDropLocation:', isValidDropLocation);
|
|
549
|
+
|
|
550
|
+
if (!isValidDropLocation && dropInfo) {
|
|
551
|
+
// Drop location is outside artboard - keep element in original position
|
|
552
|
+
console.warn('[endAutoLayoutDrag] Invalid drop location (outside artboard), keeping element in place');
|
|
553
|
+
// Don't move, element stays where it is
|
|
554
|
+
} else if (dropInfo && isSameParent && originalParent) {
|
|
555
|
+
// Same parent - reorder within the same container
|
|
556
|
+
const targetIndex = dropInfo.index;
|
|
557
|
+
console.log('[endAutoLayoutDrag] Same parent reorder:', { targetIndex, originalIndex });
|
|
558
|
+
if (targetIndex !== originalIndex) {
|
|
559
|
+
moved = reorderFlexChild(originalParent, element, targetIndex);
|
|
560
|
+
console.log('[endAutoLayoutDrag] reorderFlexChild result:', moved);
|
|
561
|
+
}
|
|
562
|
+
// If targetIndex === originalIndex, no move needed (element stays in place)
|
|
563
|
+
} else if (dropInfo && dropInfo.container && isValidDropLocation) {
|
|
564
|
+
// Different parent but valid location - move to new container
|
|
565
|
+
// Safety check: don't move into self or descendants
|
|
566
|
+
console.log('[endAutoLayoutDrag] Different parent move:', {
|
|
567
|
+
containerTag: dropInfo.container.tagName,
|
|
568
|
+
containerId: dropInfo.container.id,
|
|
569
|
+
isElementSelf: dropInfo.container === element,
|
|
570
|
+
isDescendant: element.contains(dropInfo.container),
|
|
571
|
+
});
|
|
572
|
+
if (dropInfo.container !== element && !element.contains(dropInfo.container)) {
|
|
573
|
+
moved = moveElementToContainer(element, dropInfo);
|
|
574
|
+
console.log('[endAutoLayoutDrag] moveElementToContainer result:', moved);
|
|
575
|
+
} else {
|
|
576
|
+
console.warn('[flex-utils] Prevented moving element into itself or descendant');
|
|
577
|
+
}
|
|
578
|
+
} else {
|
|
579
|
+
console.log('[endAutoLayoutDrag] No dropInfo or container, element stays in place');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
console.log('[endAutoLayoutDrag] After DOM operation:', {
|
|
583
|
+
elementInDOM: element.isConnected,
|
|
584
|
+
hasParent: !!element.parentElement,
|
|
585
|
+
parentTag: element.parentElement?.tagName,
|
|
586
|
+
parentId: element.parentElement?.id,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// Safety check: Ensure element still has a valid parent
|
|
590
|
+
// If element was orphaned somehow, re-attach it to the original parent or artboard
|
|
591
|
+
if (!element.parentElement) {
|
|
592
|
+
console.warn('[flex-utils] Element was orphaned during drag, re-attaching to original parent');
|
|
593
|
+
if (originalParent && originalParent.isConnected) {
|
|
594
|
+
originalParent.appendChild(element);
|
|
595
|
+
moved = true; // Consider this a move since we're re-attaching
|
|
596
|
+
} else if (currentParent && currentParent.isConnected) {
|
|
597
|
+
currentParent.appendChild(element);
|
|
598
|
+
moved = true;
|
|
599
|
+
} else {
|
|
600
|
+
// Fallback to artboard
|
|
601
|
+
const artboard = iframeDoc.querySelector('#artboard, .slide-artboard, #slide-artboard, [data-artboard]') as HTMLElement;
|
|
602
|
+
if (artboard) {
|
|
603
|
+
artboard.appendChild(element);
|
|
604
|
+
moved = true;
|
|
605
|
+
} else {
|
|
606
|
+
console.error('[flex-utils] Could not find a valid parent for orphaned element');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Remove ghost after animation completes
|
|
612
|
+
setTimeout(() => {
|
|
613
|
+
ghost.style.opacity = '0';
|
|
614
|
+
ghost.style.transform = 'scale(0.95)';
|
|
615
|
+
setTimeout(() => ghost.remove(), 150);
|
|
616
|
+
}, 50);
|
|
617
|
+
|
|
618
|
+
return moved;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Cancel auto-layout drag - restore everything with animation
|
|
623
|
+
*/
|
|
624
|
+
export function cancelAutoLayoutDrag(
|
|
625
|
+
state: AutoLayoutDragState,
|
|
626
|
+
iframeDoc: Document
|
|
627
|
+
): void {
|
|
628
|
+
const { element, ghost, originalRect, scale } = state;
|
|
629
|
+
|
|
630
|
+
// Reset position tracking
|
|
631
|
+
prevDragX = 0;
|
|
632
|
+
prevDragY = 0;
|
|
633
|
+
|
|
634
|
+
// Remove drop indicators
|
|
635
|
+
hideFlexDropIndicator(iframeDoc);
|
|
636
|
+
hideNestingIndicator(iframeDoc);
|
|
637
|
+
|
|
638
|
+
// Animate ghost back to original position with subtle transition - maintain zoom scale
|
|
639
|
+
ghost.style.transition = 'all 0.2s ease-out';
|
|
640
|
+
ghost.style.left = `${originalRect.left}px`;
|
|
641
|
+
ghost.style.top = `${originalRect.top}px`;
|
|
642
|
+
ghost.style.transform = `scale(${scale})`;
|
|
643
|
+
ghost.style.opacity = '0.5';
|
|
644
|
+
|
|
645
|
+
// Restore original element immediately (NO transition/transform)
|
|
646
|
+
element.style.opacity = '';
|
|
647
|
+
delete element.dataset.dragging;
|
|
648
|
+
|
|
649
|
+
// Remove ghost after animation
|
|
650
|
+
setTimeout(() => {
|
|
651
|
+
ghost.style.opacity = '0';
|
|
652
|
+
setTimeout(() => ghost.remove(), 150);
|
|
653
|
+
}, 200);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Check if element is inside a drag ghost
|
|
658
|
+
*/
|
|
659
|
+
function isInsideDragGhost(element: HTMLElement): boolean {
|
|
660
|
+
let ancestor = element.parentElement;
|
|
661
|
+
while (ancestor) {
|
|
662
|
+
if (ancestor.classList.contains('drag-ghost')) {
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
ancestor = ancestor.parentElement;
|
|
666
|
+
}
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Check if an element is a valid drop container
|
|
672
|
+
* Supports flex, grid, and block-level containers
|
|
673
|
+
*/
|
|
674
|
+
export function isValidDropContainer(element: HTMLElement, iframeDoc?: Document): boolean {
|
|
675
|
+
const tagName = element.tagName.toLowerCase();
|
|
676
|
+
|
|
677
|
+
// Skip if it's a UI element
|
|
678
|
+
if (element.classList.contains('selection-box') ||
|
|
679
|
+
element.classList.contains('resize-handle') ||
|
|
680
|
+
element.classList.contains('drag-ghost') ||
|
|
681
|
+
element.classList.contains('flex-drop-indicator') ||
|
|
682
|
+
element.classList.contains('nesting-drop-indicator') ||
|
|
683
|
+
element.dataset.dragUi) {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Skip if element is inside a drag ghost
|
|
688
|
+
if (isInsideDragGhost(element)) {
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Skip non-element tags
|
|
693
|
+
if (tagName === 'script' || tagName === 'style' || tagName === 'link' ||
|
|
694
|
+
tagName === 'br' || tagName === 'hr' || tagName === 'img' ||
|
|
695
|
+
tagName === 'input' || tagName === 'textarea' || tagName === 'select' ||
|
|
696
|
+
tagName === 'svg' || tagName === 'canvas' || tagName === 'video' || tagName === 'audio') {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Always allow these container tags
|
|
701
|
+
const containerTags = [
|
|
702
|
+
'div', 'section', 'article', 'main', 'aside', 'header', 'footer',
|
|
703
|
+
'nav', 'ul', 'ol', 'li', 'figure', 'figcaption', 'form', 'fieldset',
|
|
704
|
+
'table', 'tbody', 'thead', 'tfoot', 'tr', 'td', 'th',
|
|
705
|
+
'details', 'summary', 'dialog', 'address', 'blockquote', 'pre', 'code'
|
|
706
|
+
];
|
|
707
|
+
|
|
708
|
+
if (containerTags.includes(tagName)) {
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Check if element is a layout container (flex, grid, or block with children)
|
|
713
|
+
const win = element.ownerDocument?.defaultView || iframeDoc?.defaultView;
|
|
714
|
+
if (win) {
|
|
715
|
+
const style = win.getComputedStyle(element);
|
|
716
|
+
const display = style.display;
|
|
717
|
+
|
|
718
|
+
// Flex and grid containers are always valid drop targets
|
|
719
|
+
if (display === 'flex' || display === 'inline-flex' ||
|
|
720
|
+
display === 'grid' || display === 'inline-grid') {
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Block-level elements that can contain children
|
|
725
|
+
if (display === 'block' || display === 'inline-block' || display === 'flow-root') {
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Drop target information
|
|
735
|
+
*/
|
|
736
|
+
export interface DropTargetInfo {
|
|
737
|
+
container: HTMLElement;
|
|
738
|
+
position: 'before' | 'after' | 'inside';
|
|
739
|
+
index: number;
|
|
740
|
+
referenceElement: HTMLElement | null;
|
|
741
|
+
/** True if this is a nesting operation (moving into a container) */
|
|
742
|
+
isNesting?: boolean;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Find the drop target at cursor position
|
|
747
|
+
* Prioritizes sibling reordering over nesting
|
|
748
|
+
*/
|
|
749
|
+
export function findDropTarget(
|
|
750
|
+
iframeDoc: Document,
|
|
751
|
+
cursorX: number,
|
|
752
|
+
cursorY: number,
|
|
753
|
+
draggedElement: HTMLElement,
|
|
754
|
+
_unused?: HTMLElement // kept for backward compatibility
|
|
755
|
+
): DropTargetInfo | null {
|
|
756
|
+
const elementsAtPoint = iframeDoc.elementsFromPoint(cursorX, cursorY);
|
|
757
|
+
const draggedParent = draggedElement.parentElement;
|
|
758
|
+
|
|
759
|
+
// First pass: Look for sibling reordering in the same parent (highest priority)
|
|
760
|
+
for (const el of elementsAtPoint) {
|
|
761
|
+
const element = el as HTMLElement;
|
|
762
|
+
|
|
763
|
+
// Skip invalid elements
|
|
764
|
+
if (isSkippableElement(element, draggedElement)) {
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const parent = element.parentElement;
|
|
769
|
+
|
|
770
|
+
// Prioritize same-parent reordering
|
|
771
|
+
if (parent === draggedParent && parent) {
|
|
772
|
+
const result = calculateSiblingDropPosition(
|
|
773
|
+
element,
|
|
774
|
+
parent,
|
|
775
|
+
cursorX,
|
|
776
|
+
cursorY,
|
|
777
|
+
draggedElement,
|
|
778
|
+
iframeDoc
|
|
779
|
+
);
|
|
780
|
+
if (result) {
|
|
781
|
+
return result;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Second pass: Look for any valid drop target
|
|
787
|
+
for (const el of elementsAtPoint) {
|
|
788
|
+
const element = el as HTMLElement;
|
|
789
|
+
|
|
790
|
+
// Skip invalid elements
|
|
791
|
+
if (isSkippableElement(element, draggedElement)) {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const parent = element.parentElement;
|
|
796
|
+
if (!parent || !isValidDropContainer(parent, iframeDoc)) {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Get valid children (excluding dragged element)
|
|
801
|
+
const children = getValidChildren(parent).filter(c => c !== draggedElement);
|
|
802
|
+
const elementIndex = children.indexOf(element);
|
|
803
|
+
|
|
804
|
+
if (elementIndex === -1) {
|
|
805
|
+
// Element is not a direct child - check for nesting into empty container
|
|
806
|
+
// Only allow nesting if cursor is very centered in the element
|
|
807
|
+
if (isValidDropContainer(element, iframeDoc) && shouldAllowNesting(element, cursorX, cursorY, draggedElement)) {
|
|
808
|
+
const childrenOfElement = getValidChildren(element).filter(c => c !== draggedElement);
|
|
809
|
+
if (childrenOfElement.length === 0) {
|
|
810
|
+
return {
|
|
811
|
+
container: element,
|
|
812
|
+
position: 'inside',
|
|
813
|
+
index: 0,
|
|
814
|
+
referenceElement: null,
|
|
815
|
+
isNesting: true, // Mark as nesting operation
|
|
816
|
+
} as DropTargetInfo;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Calculate sibling drop position
|
|
823
|
+
const result = calculateSiblingDropPosition(
|
|
824
|
+
element,
|
|
825
|
+
parent,
|
|
826
|
+
cursorX,
|
|
827
|
+
cursorY,
|
|
828
|
+
draggedElement,
|
|
829
|
+
iframeDoc
|
|
830
|
+
);
|
|
831
|
+
if (result) {
|
|
832
|
+
return result;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Ultimate fallback: keep element in its original parent with its original position
|
|
837
|
+
// This is now the first fallback to preserve the element's position when dropped outside valid targets
|
|
838
|
+
const originalParent = draggedElement.parentElement;
|
|
839
|
+
if (originalParent) {
|
|
840
|
+
const allChildren = Array.from(originalParent.children);
|
|
841
|
+
const currentIndex = allChildren.indexOf(draggedElement);
|
|
842
|
+
// Return the original parent with the current index to preserve position
|
|
843
|
+
return {
|
|
844
|
+
container: originalParent,
|
|
845
|
+
position: 'inside',
|
|
846
|
+
index: currentIndex >= 0 ? currentIndex : 0,
|
|
847
|
+
referenceElement: null,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Secondary fallback to artboard (only if element has no parent)
|
|
852
|
+
const artboard = iframeDoc.querySelector('#artboard, .slide-artboard, #slide-artboard, [data-artboard]') as HTMLElement;
|
|
853
|
+
if (artboard && artboard !== draggedElement && !draggedElement.contains(artboard)) {
|
|
854
|
+
const children = getValidChildren(artboard).filter(c => c !== draggedElement);
|
|
855
|
+
return {
|
|
856
|
+
container: artboard,
|
|
857
|
+
position: 'inside',
|
|
858
|
+
index: children.length,
|
|
859
|
+
referenceElement: null,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Check if element should be skipped during drop target search
|
|
868
|
+
*/
|
|
869
|
+
function isSkippableElement(element: HTMLElement, draggedElement: HTMLElement): boolean {
|
|
870
|
+
// Skip dragged element and its descendants
|
|
871
|
+
if (element === draggedElement || draggedElement.contains(element)) {
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Skip UI elements
|
|
876
|
+
if (element.classList.contains('selection-box') ||
|
|
877
|
+
element.classList.contains('resize-handle') ||
|
|
878
|
+
element.classList.contains('flex-drop-indicator') ||
|
|
879
|
+
element.classList.contains('drag-ghost') ||
|
|
880
|
+
element.classList.contains('nesting-drop-indicator') ||
|
|
881
|
+
element.classList.contains('auto-layout-drop-indicator') ||
|
|
882
|
+
element.dataset.dragging === 'true' ||
|
|
883
|
+
element.dataset.dragUi === 'true') {
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Skip elements inside drag ghost (ghost clone elements)
|
|
888
|
+
// The ghost has class 'drag-ghost', but its children don't
|
|
889
|
+
// So we need to check if any ancestor has the drag-ghost class
|
|
890
|
+
let ancestor = element.parentElement;
|
|
891
|
+
while (ancestor) {
|
|
892
|
+
if (ancestor.classList.contains('drag-ghost')) {
|
|
893
|
+
return true;
|
|
894
|
+
}
|
|
895
|
+
ancestor = ancestor.parentElement;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Check if nesting should be allowed based on cursor position
|
|
903
|
+
* Only allows nesting when cursor is very centered in the element
|
|
904
|
+
*/
|
|
905
|
+
function shouldAllowNesting(
|
|
906
|
+
element: HTMLElement,
|
|
907
|
+
cursorX: number,
|
|
908
|
+
cursorY: number,
|
|
909
|
+
draggedElement: HTMLElement
|
|
910
|
+
): boolean {
|
|
911
|
+
const rect = element.getBoundingClientRect();
|
|
912
|
+
|
|
913
|
+
// Require cursor to be at least 40% into the element from all edges
|
|
914
|
+
const minInset = 0.4;
|
|
915
|
+
const insetX = rect.width * minInset;
|
|
916
|
+
const insetY = rect.height * minInset;
|
|
917
|
+
|
|
918
|
+
const isWellInside =
|
|
919
|
+
cursorX > rect.left + insetX &&
|
|
920
|
+
cursorX < rect.right - insetX &&
|
|
921
|
+
cursorY > rect.top + insetY &&
|
|
922
|
+
cursorY < rect.bottom - insetY;
|
|
923
|
+
|
|
924
|
+
// Also check minimum size - don't nest into small elements
|
|
925
|
+
const minSize = 80;
|
|
926
|
+
const isLargeEnough = rect.width >= minSize && rect.height >= minSize;
|
|
927
|
+
|
|
928
|
+
// Don't nest into elements that are similar size to dragged element
|
|
929
|
+
const draggedRect = draggedElement.getBoundingClientRect();
|
|
930
|
+
const sizeDifferenceThreshold = 1.5;
|
|
931
|
+
const isSignificantlyLarger =
|
|
932
|
+
rect.width > draggedRect.width * sizeDifferenceThreshold ||
|
|
933
|
+
rect.height > draggedRect.height * sizeDifferenceThreshold;
|
|
934
|
+
|
|
935
|
+
return isWellInside && isLargeEnough && isSignificantlyLarger;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Determine if a container's layout direction is column-like
|
|
940
|
+
* Uses multiple detection methods for robustness:
|
|
941
|
+
* 1. Check flex-direction for flex containers
|
|
942
|
+
* 2. Check grid-auto-flow for grid containers
|
|
943
|
+
* 3. Check actual child positions as fallback
|
|
944
|
+
*/
|
|
945
|
+
function isColumnLayout(parent: HTMLElement, iframeDoc: Document): { isColumn: boolean; isReverse: boolean } {
|
|
946
|
+
// Try to get computed style - use parent's ownerDocument for reliability
|
|
947
|
+
const win = parent.ownerDocument?.defaultView || iframeDoc.defaultView;
|
|
948
|
+
const style = win?.getComputedStyle(parent);
|
|
949
|
+
|
|
950
|
+
if (style) {
|
|
951
|
+
const display = style.display;
|
|
952
|
+
|
|
953
|
+
// Flex container
|
|
954
|
+
if (display === 'flex' || display === 'inline-flex') {
|
|
955
|
+
const flexDirection = style.flexDirection || 'row';
|
|
956
|
+
const isColumn = flexDirection === 'column' || flexDirection === 'column-reverse';
|
|
957
|
+
const isReverse = flexDirection.includes('reverse');
|
|
958
|
+
return { isColumn, isReverse };
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Grid container
|
|
962
|
+
if (display === 'grid' || display === 'inline-grid') {
|
|
963
|
+
// Check grid-auto-flow to determine primary direction
|
|
964
|
+
const gridAutoFlow = style.gridAutoFlow || 'row';
|
|
965
|
+
// Also check grid-template-columns/rows to infer layout
|
|
966
|
+
const templateColumns = style.gridTemplateColumns || '';
|
|
967
|
+
const templateRows = style.gridTemplateRows || '';
|
|
968
|
+
|
|
969
|
+
// If only one column is defined, it's effectively a column layout
|
|
970
|
+
const columnCount = templateColumns.split(/\s+/).filter(c => c && c !== 'none').length;
|
|
971
|
+
const rowCount = templateRows.split(/\s+/).filter(r => r && r !== 'none').length;
|
|
972
|
+
|
|
973
|
+
// grid-auto-flow: column means items flow in columns first
|
|
974
|
+
if (gridAutoFlow.includes('column')) {
|
|
975
|
+
return { isColumn: false, isReverse: false };
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// If there's only 1 column defined, items stack vertically
|
|
979
|
+
if (columnCount === 1 || (columnCount === 0 && rowCount > 1)) {
|
|
980
|
+
return { isColumn: true, isReverse: false };
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Default for grid: items flow horizontally first (row)
|
|
984
|
+
return { isColumn: false, isReverse: false };
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// For non-flex/grid containers (block, inline-block, etc.) or when style is unavailable,
|
|
989
|
+
// detect layout direction from actual child positions
|
|
990
|
+
const children = getValidChildren(parent);
|
|
991
|
+
if (children.length >= 2) {
|
|
992
|
+
const first = children[0].getBoundingClientRect();
|
|
993
|
+
const second = children[1].getBoundingClientRect();
|
|
994
|
+
|
|
995
|
+
// If children have similar X positions but different Y, it's column layout
|
|
996
|
+
// If children have similar Y positions but different X, it's row layout
|
|
997
|
+
const xDiff = Math.abs(first.left - second.left);
|
|
998
|
+
const yDiff = Math.abs(first.top - second.top);
|
|
999
|
+
|
|
1000
|
+
// Use a threshold to determine which axis has more variance
|
|
1001
|
+
if (yDiff > xDiff + 10) {
|
|
1002
|
+
// Children are stacked vertically → column layout
|
|
1003
|
+
return { isColumn: true, isReverse: first.top > second.top };
|
|
1004
|
+
} else if (xDiff > yDiff + 10) {
|
|
1005
|
+
// Children are side by side → row layout
|
|
1006
|
+
return { isColumn: false, isReverse: first.left > second.left };
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Default: block elements stack vertically
|
|
1011
|
+
return { isColumn: true, isReverse: false };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Calculate drop position for sibling reordering
|
|
1016
|
+
*/
|
|
1017
|
+
function calculateSiblingDropPosition(
|
|
1018
|
+
element: HTMLElement,
|
|
1019
|
+
parent: HTMLElement,
|
|
1020
|
+
cursorX: number,
|
|
1021
|
+
cursorY: number,
|
|
1022
|
+
draggedElement: HTMLElement,
|
|
1023
|
+
iframeDoc: Document
|
|
1024
|
+
): DropTargetInfo | null {
|
|
1025
|
+
const children = getValidChildren(parent).filter(c => c !== draggedElement);
|
|
1026
|
+
const elementIndex = children.indexOf(element);
|
|
1027
|
+
|
|
1028
|
+
if (elementIndex === -1) {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const rect = element.getBoundingClientRect();
|
|
1033
|
+
const { isColumn, isReverse } = isColumnLayout(parent, iframeDoc);
|
|
1034
|
+
|
|
1035
|
+
// Use edge detection - check if cursor is in the first or second half
|
|
1036
|
+
let position: 'before' | 'after';
|
|
1037
|
+
|
|
1038
|
+
if (isColumn) {
|
|
1039
|
+
const midY = rect.top + rect.height / 2;
|
|
1040
|
+
position = cursorY < midY ? 'before' : 'after';
|
|
1041
|
+
} else {
|
|
1042
|
+
const midX = rect.left + rect.width / 2;
|
|
1043
|
+
position = cursorX < midX ? 'before' : 'after';
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Adjust for reverse direction
|
|
1047
|
+
if (isReverse) {
|
|
1048
|
+
position = position === 'before' ? 'after' : 'before';
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Calculate the actual drop index
|
|
1052
|
+
let index = elementIndex;
|
|
1053
|
+
if (position === 'after') {
|
|
1054
|
+
index = elementIndex + 1;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
return {
|
|
1058
|
+
container: parent,
|
|
1059
|
+
position,
|
|
1060
|
+
index,
|
|
1061
|
+
referenceElement: element,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Show drop indicator at target position
|
|
1067
|
+
* Shows a line for sibling reordering, or a border for nesting
|
|
1068
|
+
*/
|
|
1069
|
+
export function showDropIndicator(
|
|
1070
|
+
iframeDoc: Document,
|
|
1071
|
+
dropInfo: DropTargetInfo,
|
|
1072
|
+
draggedElement: HTMLElement
|
|
1073
|
+
): void {
|
|
1074
|
+
// If nesting, show a container highlight instead of a line
|
|
1075
|
+
if (dropInfo.isNesting || dropInfo.position === 'inside') {
|
|
1076
|
+
hideFlexDropIndicator(iframeDoc);
|
|
1077
|
+
showNestingIndicator(iframeDoc, dropInfo.container);
|
|
1078
|
+
} else {
|
|
1079
|
+
hideNestingIndicator(iframeDoc);
|
|
1080
|
+
showFlexDropIndicator(iframeDoc, dropInfo.container, dropInfo.index, draggedElement);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Show nesting indicator (highlight the container)
|
|
1086
|
+
*/
|
|
1087
|
+
function showNestingIndicator(iframeDoc: Document, container: HTMLElement): void {
|
|
1088
|
+
hideNestingIndicator(iframeDoc);
|
|
1089
|
+
|
|
1090
|
+
const rect = container.getBoundingClientRect();
|
|
1091
|
+
const scrollLeft = iframeDoc.defaultView?.scrollX || 0;
|
|
1092
|
+
const scrollTop = iframeDoc.defaultView?.scrollY || 0;
|
|
1093
|
+
|
|
1094
|
+
const indicator = iframeDoc.createElement('div');
|
|
1095
|
+
indicator.className = 'nesting-drop-indicator';
|
|
1096
|
+
indicator.dataset.dragUi = 'true';
|
|
1097
|
+
indicator.style.cssText = `
|
|
1098
|
+
position: absolute;
|
|
1099
|
+
left: ${rect.left + scrollLeft}px;
|
|
1100
|
+
top: ${rect.top + scrollTop}px;
|
|
1101
|
+
width: ${rect.width}px;
|
|
1102
|
+
height: ${rect.height}px;
|
|
1103
|
+
pointer-events: none;
|
|
1104
|
+
z-index: 9999;
|
|
1105
|
+
border: 2px dashed #10B981;
|
|
1106
|
+
border-radius: 8px;
|
|
1107
|
+
background: rgba(16, 185, 129, 0.05);
|
|
1108
|
+
box-sizing: border-box;
|
|
1109
|
+
animation: nestingPulse 2s ease-in-out infinite;
|
|
1110
|
+
`;
|
|
1111
|
+
|
|
1112
|
+
// Ensure animation styles exist
|
|
1113
|
+
if (!iframeDoc.getElementById('drop-indicator-styles')) {
|
|
1114
|
+
const styleSheet = iframeDoc.createElement('style');
|
|
1115
|
+
styleSheet.id = 'drop-indicator-styles';
|
|
1116
|
+
styleSheet.textContent = `
|
|
1117
|
+
@keyframes nestingPulse {
|
|
1118
|
+
0%, 100% { opacity: 0.9; }
|
|
1119
|
+
50% { opacity: 0.7; }
|
|
1120
|
+
}
|
|
1121
|
+
`;
|
|
1122
|
+
iframeDoc.head.appendChild(styleSheet);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
iframeDoc.body.appendChild(indicator);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Hide nesting indicator
|
|
1130
|
+
*/
|
|
1131
|
+
function hideNestingIndicator(iframeDoc: Document): void {
|
|
1132
|
+
const indicators = iframeDoc.querySelectorAll('.nesting-drop-indicator');
|
|
1133
|
+
indicators.forEach(indicator => indicator.remove());
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Move element to a different container
|
|
1138
|
+
*/
|
|
1139
|
+
function moveElementToContainer(
|
|
1140
|
+
element: HTMLElement,
|
|
1141
|
+
dropInfo: DropTargetInfo
|
|
1142
|
+
): boolean {
|
|
1143
|
+
const { container, position, index, referenceElement } = dropInfo;
|
|
1144
|
+
|
|
1145
|
+
console.log('[moveElementToContainer] Called:', {
|
|
1146
|
+
elementTag: element.tagName,
|
|
1147
|
+
elementId: element.id,
|
|
1148
|
+
containerTag: container?.tagName,
|
|
1149
|
+
containerId: container?.id,
|
|
1150
|
+
position,
|
|
1151
|
+
index,
|
|
1152
|
+
referenceElementTag: referenceElement?.tagName,
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
// Safety check: don't move element into itself
|
|
1156
|
+
if (container === element || element.contains(container)) {
|
|
1157
|
+
console.warn('[flex-utils] Cannot move element into itself or its descendant');
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Check if element is already at the correct position
|
|
1162
|
+
if (element.parentElement === container) {
|
|
1163
|
+
const children = getValidChildren(container).filter(c => c !== element);
|
|
1164
|
+
const currentIndex = Array.from(container.children).indexOf(element);
|
|
1165
|
+
|
|
1166
|
+
// If the element is already in the correct position, skip DOM operation
|
|
1167
|
+
if (position === 'inside' && (index >= children.length || currentIndex === index)) {
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
if (position === 'inside' || !referenceElement) {
|
|
1173
|
+
// Insert at the end or beginning of empty container
|
|
1174
|
+
// Exclude the dragged element from children count
|
|
1175
|
+
const children = getValidChildren(container).filter(c => c !== element);
|
|
1176
|
+
if (index >= children.length) {
|
|
1177
|
+
container.appendChild(element);
|
|
1178
|
+
} else if (children[index] && children[index] !== element) {
|
|
1179
|
+
container.insertBefore(element, children[index]);
|
|
1180
|
+
} else {
|
|
1181
|
+
container.appendChild(element);
|
|
1182
|
+
}
|
|
1183
|
+
} else if (position === 'before') {
|
|
1184
|
+
// Don't insert before itself
|
|
1185
|
+
if (referenceElement !== element) {
|
|
1186
|
+
container.insertBefore(element, referenceElement);
|
|
1187
|
+
}
|
|
1188
|
+
} else {
|
|
1189
|
+
// position === 'after'
|
|
1190
|
+
// Don't insert after itself
|
|
1191
|
+
if (referenceElement !== element) {
|
|
1192
|
+
if (referenceElement.nextSibling && referenceElement.nextSibling !== element) {
|
|
1193
|
+
container.insertBefore(element, referenceElement.nextSibling);
|
|
1194
|
+
} else if (!referenceElement.nextSibling) {
|
|
1195
|
+
container.appendChild(element);
|
|
1196
|
+
}
|
|
1197
|
+
// If nextSibling === element, element is already in the correct position
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
return true;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// ============================================================
|
|
1205
|
+
// Legacy/Compatibility Functions
|
|
1206
|
+
// ============================================================
|
|
1207
|
+
|
|
1208
|
+
export function showDropPlaceholder(): void {
|
|
1209
|
+
// No longer used - we use indicator line instead
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
export function removeDropPlaceholder(iframeDoc: Document): void {
|
|
1213
|
+
const placeholders = iframeDoc.querySelectorAll('.drop-placeholder, .drag-placeholder');
|
|
1214
|
+
placeholders.forEach(p => p.remove());
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
export function createDragGhost(): HTMLElement | null { return null; }
|
|
1218
|
+
export function updateDragGhostPosition(): void {}
|
|
1219
|
+
export function removeDragGhost(iframeDoc: Document): void {
|
|
1220
|
+
const ghosts = iframeDoc.querySelectorAll('.drag-ghost');
|
|
1221
|
+
ghosts.forEach(ghost => ghost.remove());
|
|
1222
|
+
}
|
|
1223
|
+
export function showAutoLayoutDropIndicator(): void {}
|
|
1224
|
+
export function hideAutoLayoutDropIndicator(iframeDoc: Document): void {
|
|
1225
|
+
const indicators = iframeDoc.querySelectorAll('.auto-layout-drop-indicator');
|
|
1226
|
+
indicators.forEach(indicator => indicator.remove());
|
|
1227
|
+
}
|
|
1228
|
+
export function moveElementToParent(element: HTMLElement, dropInfo: DropTargetInfo): boolean {
|
|
1229
|
+
return moveElementToContainer(element, dropInfo);
|
|
1230
|
+
}
|