Skip to content

[DRAFT] feat: allow move child element out side parent #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 65 additions & 7 deletions apps/studio/electron/preload/webview/elements/move/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function startDrag(domId: string): number | null {
}
const htmlChildren = Array.from(parent.children).filter(isValidHtmlElement);
const originalIndex = htmlChildren.indexOf(el);

prepareElementForDragging(el);
createStub(el);
const pos = getAbsolutePosition(el);
Expand All @@ -44,14 +45,57 @@ export function drag(domId: string, dx: number, dy: number, x: number, y: number
el.style.width = styles.width + 1;
el.style.height = styles.height + 1;
el.style.position = 'fixed';
el.style.zIndex = '9999';

const targetContainer = findTargetContainerAtPoint(x, y, el);

if (targetContainer) {
moveStub(el, x, y, targetContainer);
} else {
removeStub();
}
}

function findTargetContainerAtPoint(
x: number,
y: number,
draggedElement: HTMLElement,
): HTMLElement | null {
draggedElement.style.display = 'none';

try {
let element = document.elementFromPoint(x, y) as HTMLElement | null;
while (element) {
const styles = window.getComputedStyle(element);
if (
(styles.display === 'flex' ||
styles.display === 'grid' ||
styles.display === 'block') &&
element !== draggedElement &&
!element.hasAttribute(EditorAttributes.DATA_ONLOOK_DRAGGING) &&
!draggedElement.contains(element) &&
!isStubElement(element)
) {
return element;
}
element = element.parentElement;
}
} finally {
draggedElement.style.display = '';
}

return null;
}

moveStub(el, x, y);
function isStubElement(element: HTMLElement): boolean {
return element.id === EditorAttributes.ONLOOK_STUB_ID;
}

export function endDrag(domId: string): {
newIndex: number;
child: DomElement;
parent: DomElement;
oldParent?: DomElement;
} | null {
const el = elementFromDomId(domId);
if (!el) {
Expand All @@ -60,29 +104,43 @@ export function endDrag(domId: string): {
return null;
}

const parent = el.parentElement;
if (!parent) {
const originalParent = el.parentElement;
const stub = document.getElementById(EditorAttributes.ONLOOK_STUB_ID);
const stubParent = stub?.parentElement;

if (!stubParent || !originalParent) {
console.warn('End drag parent not found');
cleanUpElementAfterDragging(el);
removeStub();
return null;
}

const stubIndex = getCurrentStubIndex(parent, el);
const stubIndex = getCurrentStubIndex(stubParent, el);

cleanUpElementAfterDragging(el);
removeStub();

if (stubIndex === -1) {
return null;
}

const elementIndex = Array.from(parent.children).indexOf(el);
const elementIndex = Array.from(originalParent.children).indexOf(el);
if (stubIndex === elementIndex) {
return null;
}

if (stubParent !== originalParent) {
return {
newIndex: stubIndex,
child: getDomElement(el, false),
parent: getDomElement(stubParent, false),
oldParent: getDomElement(originalParent, false),
};
}

return {
newIndex: stubIndex,
child: getDomElement(el, false),
parent: getDomElement(parent, false),
parent: getDomElement(stubParent, false),
};
}

Expand Down
42 changes: 30 additions & 12 deletions apps/studio/electron/preload/webview/elements/move/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import type { DomElement } from '@onlook/models/element';
import { getDomElement } from '../helpers';
import { elementFromDomId, isValidHtmlElement } from '/common/helpers';

export function moveElement(domId: string, newIndex: number): DomElement | undefined {
export function moveElement(
domId: string,
newIndex: number,
targetDomId: string,
oldParentDomId?: string,
): DomElement | undefined {
const el = elementFromDomId(domId) as HTMLElement | null;
if (!el) {
console.warn(`Move element not found: ${domId}`);
return;
}

const movedEl = moveElToIndex(el, newIndex);
const movedEl = moveElToIndex(el, newIndex, targetDomId, oldParentDomId);
if (!movedEl) {
console.warn(`Failed to move element: ${domId}`);
return;
Expand All @@ -31,20 +35,34 @@ export function getElementIndex(domId: string): number {
return index;
}

export function moveElToIndex(el: HTMLElement, newIndex: number): HTMLElement | undefined {
const parent = el.parentElement;
if (!parent) {
console.warn('Parent not found');
export function moveElToIndex(
el: HTMLElement,
newIndex: number,
targetDomId: string,
oldParentDomId?: string,
): HTMLElement | undefined {
const targetEl = elementFromDomId(targetDomId) as HTMLElement | null;
if (!targetEl) {
console.warn(`Target element not found: ${targetDomId}`);
return;
}

parent.removeChild(el);
if (newIndex >= parent.children.length) {
parent.appendChild(el);
const oldParentEl = oldParentDomId
? (elementFromDomId(oldParentDomId) as HTMLElement | null)
: null;

if (oldParentEl) {
oldParentEl.removeChild(el);
} else {
el.parentElement?.removeChild(el);
}

if (newIndex >= targetEl.children.length) {
targetEl.appendChild(el);
return el;
}

const referenceNode = parent.children[newIndex];
parent.insertBefore(el, referenceNode);
const referenceNode = targetEl.children[newIndex];
targetEl.insertBefore(el, referenceNode);
return el;
}
28 changes: 11 additions & 17 deletions apps/studio/electron/preload/webview/elements/move/stub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,25 @@ export function createStub(el: HTMLElement) {
document.body.appendChild(stub);
}

export function moveStub(el: HTMLElement, x: number, y: number) {
export function moveStub(el: HTMLElement, x: number, y: number, targetContainer: HTMLElement) {
const stub = document.getElementById(EditorAttributes.ONLOOK_STUB_ID);
if (!stub) {
return;
}

const parent = el.parentElement;
if (!parent) {
return;
}

let displayDirection = el.getAttribute(EditorAttributes.DATA_ONLOOK_DRAG_DIRECTION);
if (!displayDirection) {
displayDirection = getDisplayDirection(parent);
}
const displayDirection = getDisplayDirection(targetContainer);

// Check if the parent is using grid layout
const parentStyle = window.getComputedStyle(parent);
const isGridLayout = parentStyle.display === 'grid';
// Check if the target container is using grid layout
const containerStyle = window.getComputedStyle(targetContainer);
const isGridLayout = containerStyle.display === 'grid';

const siblings = Array.from(parent.children).filter((child) => child !== el && child !== stub);
const siblings = Array.from(targetContainer.children).filter(
(child) => child !== el && child !== stub,
);

let insertionIndex;
if (isGridLayout) {
insertionIndex = findGridInsertionIndex(parent, siblings, x, y);
insertionIndex = findGridInsertionIndex(targetContainer, siblings, x, y);
} else {
insertionIndex = findFlexBlockInsertionIndex(
siblings,
Expand All @@ -60,9 +54,9 @@ export function moveStub(el: HTMLElement, x: number, y: number) {

// Append element at the insertion index
if (insertionIndex >= siblings.length) {
parent.appendChild(stub);
targetContainer.appendChild(stub);
} else {
parent.insertBefore(stub, siblings[insertionIndex]);
targetContainer.insertBefore(stub, siblings[insertionIndex]);
}

stub.style.display = 'block';
Expand Down
20 changes: 15 additions & 5 deletions apps/studio/electron/preload/webview/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
Change,
GroupContainer,
ImageContentData,
IndexActionLocation,
} from '@onlook/models/actions';
import { WebviewChannels } from '@onlook/models/constants';
import { ipcRenderer } from 'electron';
Expand Down Expand Up @@ -67,13 +68,22 @@ function listenForEditEvents() {
});

ipcRenderer.on(WebviewChannels.MOVE_ELEMENT, (_, data) => {
const { domId, newIndex } = data as {
const { domId, location } = data as {
domId: string;
newIndex: number;
location: IndexActionLocation;
};
const domEl = moveElement(domId, newIndex);
if (domEl) {
publishMoveElement(domEl);
const { index, targetDomId, oldParentDomId } = location;

if (oldParentDomId) {
const domEl = moveElement(domId, index, targetDomId, oldParentDomId);
if (domEl) {
publishMoveElement(domEl);
}
} else {
const domEl = moveElement(domId, index, targetDomId);
if (domEl) {
publishMoveElement(domEl);
}
}
});

Expand Down
2 changes: 1 addition & 1 deletion apps/studio/src/lib/editor/engine/action/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export class ActionManager {
}
sendToWebview(webview, WebviewChannels.MOVE_ELEMENT, {
domId: target.domId,
newIndex: location.index,
location,
});
});
}
Expand Down
52 changes: 44 additions & 8 deletions apps/studio/src/lib/editor/engine/code/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ProjectsManager } from '@/lib/projects';
import { invokeMainChannel, sendAnalytics, sendToWebview } from '@/lib/utils';
import type {
Action,
CodeInsert,
CodeInsertImage,
CodeRemoveImage,
EditTextAction,
Expand Down Expand Up @@ -211,16 +212,51 @@ export class CodeManager {
continue;
}

const movedEl: CodeMove = {
oid: target.oid,
type: CodeActionType.MOVE,
location,
};

const request = await getOrCreateCodeDiffRequest(location.targetOid, oidToCodeChange);
request.structureChanges.push(movedEl);
if (!location.oldParentOid) {
const movedEl: CodeMove = {
oid: target.oid,
type: CodeActionType.MOVE,
location,
};

const request = await getOrCreateCodeDiffRequest(
location.targetOid,
oidToCodeChange,
);
request.structureChanges.push(movedEl);
} else {
// TODO: Handle moving from one parent to another
// 1. Remove from old parent
const removeRequest = await getOrCreateCodeDiffRequest(
location.oldParentOid,
oidToCodeChange,
);
removeRequest.structureChanges.push({
oid: target.oid,
type: CodeActionType.REMOVE,
});

// 2. Add to new parent
// This code still wrong since I can not get full element

const addRequest = await getOrCreateCodeDiffRequest(
location.targetOid,
oidToCodeChange,
);
addRequest.structureChanges.push({
oid: target.oid,
type: CodeActionType.INSERT,
location,
children: [], // This is wrong
tagName: '', // This is wrong
attributes: {}, // This is wrong
textContent: '',
pasteParams: null,
});
}
}

console.log('oidToCodeChange', JSON.stringify(Array.from(oidToCodeChange.values())));
await this.getAndWriteCodeDiff(Array.from(oidToCodeChange.values()));
}

Expand Down
7 changes: 6 additions & 1 deletion apps/studio/src/lib/editor/engine/move/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,21 @@ export class MoveManager {
newIndex: number;
child: DomElement;
parent: DomElement;
oldParent?: DomElement;
} | null = await webview.executeJavaScript(
`window.api?.endDrag('${this.dragTarget.domId}')`,
);

if (res) {
const { newIndex, child, parent } = res;
const { newIndex, child, parent, oldParent } = res;
if (newIndex !== this.originalIndex) {
const moveAction = this.createMoveAction(
webview.id,
child,
parent,
newIndex,
this.originalIndex,
oldParent,
);
this.editorEngine.action.run(moveAction);
}
Expand Down Expand Up @@ -175,6 +177,7 @@ export class MoveManager {
parent: DomElement,
newIndex: number,
originalIndex: number,
oldParent?: DomElement,
): MoveElementAction {
return {
type: 'move-element',
Expand All @@ -184,6 +187,8 @@ export class MoveManager {
targetOid: parent.instanceId || parent.oid,
index: newIndex,
originalIndex: originalIndex,
oldParentDomId: oldParent?.domId,
oldParentOid: oldParent?.instanceId || oldParent?.oid,
},
targets: [
{
Expand Down
2 changes: 2 additions & 0 deletions packages/models/src/actions/location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const BaseActionLocationSchema = z.object({
type: z.enum(['prepend', 'append']),
targetDomId: z.string(),
targetOid: z.string().nullable(),
oldParentDomId: z.string().optional(),
oldParentOid: z.string().optional(),
});

export const IndexActionLocationSchema = BaseActionLocationSchema.extend({
Expand Down