Skip to content

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

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

Closed
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should early return for certain conditions.

  1. Point is within element
  2. Point is element
  3. Target element should not have children (img, input, etc)

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 = '';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is distructive. The element loses its original display value.

}

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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kitenite Im facing a problem here, please take a look

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explicitly state the problem? I don't have enough context to understand.

// 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