Skip to content

Commit

Permalink
Dragging layers to reorder (#414)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kitenite authored Sep 28, 2024
1 parent c9888fa commit b446a32
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 77 deletions.
3 changes: 2 additions & 1 deletion app/electron/preload/webview/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { processDom } from './dom';
import { getElementAtLoc, getElementWithSelector } from './elements';
import { isElementInserted } from './elements/helpers';
import { getInsertedElements, getInsertLocation } from './elements/insert';
import { getMovedElements } from './elements/move';
import { getElementIndex, getMovedElements } from './elements/move';
import { drag, endDrag, startDrag } from './elements/move/drag';
import { getRemoveActionFromSelector } from './elements/remove';
import {
Expand All @@ -30,6 +30,7 @@ export function setApi() {
drag: drag,
endDrag: endDrag,
getMovedElements: getMovedElements,
getElementIndex: getElementIndex,

// Edit text
startEditingText: startEditingText,
Expand Down
22 changes: 21 additions & 1 deletion app/electron/preload/webview/elements/move/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getDomElement } from '../helpers';
import { moveElToIndex } from './helpers';
import { EditorAttributes } from '/common/constants';
import { getUniqueSelector } from '/common/helpers';
import { getUniqueSelector, isValidHtmlElement } from '/common/helpers';
import { InsertPos } from '/common/models';
import { DomElement } from '/common/models/element';
import { ActionMoveLocation, DomActionType, MovedElement } from '/common/models/element/domAction';
Expand All @@ -12,6 +12,10 @@ export function moveElement(selector: string, newIndex: number): DomElement | un
console.error(`Move element not found: ${selector}`);
return;
}
const originalIndex = getElementIndex(selector);
if (el.getAttribute(EditorAttributes.DATA_ONLOOK_ORIGINAL_INDEX) === null) {
el.setAttribute(EditorAttributes.DATA_ONLOOK_ORIGINAL_INDEX, originalIndex.toString());
}
const movedEl = moveElToIndex(el, newIndex);
if (!movedEl) {
console.error(`Failed to move element: ${selector}`);
Expand All @@ -32,6 +36,11 @@ export function getMovedElements(): MovedElement[] {
const isElementInserted = el.hasAttribute(EditorAttributes.DATA_ONLOOK_INSERTED);
return !isParentInserted && !isElementInserted;
})
.filter((el) => {
const originalIndex = el.getAttribute(EditorAttributes.DATA_ONLOOK_ORIGINAL_INDEX);
const currentIndex = getElementIndex(getUniqueSelector(el as HTMLElement));
return originalIndex !== currentIndex.toString();
})
.map((el) => getMovedElement(el as HTMLElement))
.sort((a, b) => a.timestamp - b.timestamp);
return movedEls;
Expand Down Expand Up @@ -69,3 +78,14 @@ export function clearMovedElements() {
el.removeAttribute(EditorAttributes.DATA_ONLOOK_TIMESTAMP);
}
}

export function getElementIndex(selector: string): number {
const el = document.querySelector(selector) as HTMLElement | null;
if (!el) {
console.error(`Element not found: ${selector}`);
return -1;
}
const htmlElments = Array.from(el.parentElement?.children || []).filter(isValidHtmlElement);
const index = htmlElments.indexOf(el);
return index;
}
16 changes: 11 additions & 5 deletions app/src/lib/editor/engine/move/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { nanoid } from 'nanoid';
import React from 'react';
import { ActionManager } from '../action';
import { HistoryManager } from '../history';
import { OverlayManager } from '../overlay';
import { MoveElementAction } from '/common/actions';
Expand Down Expand Up @@ -69,24 +70,29 @@ export class MoveManager {

const { newIndex, newSelector } = endRes;
if (newIndex !== this.originalIndex) {
this.pushMoveAction(newSelector, this.originalIndex, newIndex, webview.id);
const runAction = this.createMoveAction(
newSelector,
this.originalIndex,
newIndex,
webview.id,
);
this.history.push(runAction);
}
this.clear();
}

pushMoveAction(
createMoveAction(
newSelector: string,
originalIndex: number,
newIndex: number,
webviewId: string,
) {
const action: MoveElementAction = {
): MoveElementAction {
return {
type: 'move-element',
originalIndex,
newIndex,
targets: [{ webviewId, selector: newSelector }],
};
this.history.push(action);
}

clear() {
Expand Down
53 changes: 52 additions & 1 deletion app/src/routes/editor/LayersPanel/LayersTab.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useEditorEngine } from '@/components/Context';
import { observer } from 'mobx-react-lite';
import { useEffect, useRef, useState } from 'react';
import { Tree, TreeApi } from 'react-arborist';
import { NodeApi, Tree, TreeApi } from 'react-arborist';
import useResizeObserver from 'use-resize-observer';
import RightClickMenu from '../RightClickMenu';
import TreeNode from './Tree/TreeNode';
import TreeRow from './Tree/TreeRow';
import { escapeSelector } from '/common/helpers';
import { LayerNode } from '/common/models/element/layers';

const LayersTab = observer(() => {
Expand All @@ -29,6 +30,54 @@ const LayersTab = observer(() => {
}
}

async function handleDragEnd({
dragIds,
parentId,
index,
}: {
dragIds: string[];
parentId: string | null;
index: number;
}) {
const webview = editorEngine.webviews.getWebview(
editorEngine.elements.selected[0].webviewId,
);
if (!webview) {
console.error('No webview found');
return;
}
const originalIndex = (await webview.executeJavaScript(
`window.api?.getElementIndex('${escapeSelector(dragIds[0])}')`,
)) as number | undefined;
if (!originalIndex) {
console.error('No original index found');
return;
}
if (originalIndex === index) {
console.log('No index change');
return;
}
const moveAction = editorEngine.move.createMoveAction(
dragIds[0],
originalIndex,
index,
webview.id,
);
editorEngine.action.run(moveAction);
}

function disableDrop({
parentNode,
dragNodes,
index,
}: {
parentNode: NodeApi<LayerNode> | null;
dragNodes: NodeApi<LayerNode>[];
index: number;
}) {
return !dragNodes.every((node) => node?.parent?.id === parentNode?.id);
}

return (
<div
ref={ref}
Expand All @@ -48,6 +97,8 @@ const LayersTab = observer(() => {
height={(height ?? 8) - 16}
width={width ?? 365}
renderRow={TreeRow as any}
onMove={handleDragEnd}
disableDrop={disableDrop}
>
{(props) => <TreeNode {...props} treeHovered={treeHovered} />}
</Tree>
Expand Down
141 changes: 72 additions & 69 deletions app/src/routes/editor/LayersPanel/Tree/TreeNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ const TreeNode = observer(
node,
style,
treeHovered,
dragHandle,
}: {
node: NodeApi<LayerNode>;
style: React.CSSProperties;
treeHovered: boolean;
dragHandle?: React.RefObject<HTMLDivElement> | any;
}) => {
const editorEngine = useEditorEngine();
const [instance, setInstance] = useState<TemplateNode | undefined>(
Expand All @@ -34,7 +36,6 @@ const TreeNode = observer(
editorEngine.elements.selected.some((el) => el.selector === node.data.id),
);
const nodeRef = useRef<HTMLDivElement>(null);

useEffect(() => {
setInstance(editorEngine.ast.getInstance(node.data.id));
}, [editorEngine.ast.templateNodeMap]);
Expand All @@ -61,7 +62,7 @@ const TreeNode = observer(
function sideOffset() {
const container = document.getElementById('layer-tab-id');
const containerRect = container?.getBoundingClientRect();
const nodeRect = nodeRef.current?.getBoundingClientRect();
const nodeRect = nodeRef?.current?.getBoundingClientRect();
if (!containerRect || !nodeRect) {
return 0;
}
Expand Down Expand Up @@ -103,77 +104,79 @@ const TreeNode = observer(
return (
<Tooltip>
<TooltipTrigger asChild>
<div
ref={nodeRef}
style={style}
onClick={() => handleSelectNode()}
onMouseOver={() => handleHoverNode()}
className={twMerge(
clsx(
'flex flex-row items-center h-6 cursor-pointer rounded w-fit min-w-full',
{
'bg-bg': hovered,
'bg-stone-800': selected,
'text-purple-100': instance && selected,
'text-purple-300': instance && !selected,
'text-purple-200': instance && !selected && hovered,
'bg-purple-700/50': instance && selected,
'bg-purple-900/60': instance && !selected && hovered,
'text-active': !instance && selected,
'text-hover': !instance && !selected && hovered,
'text-text': !instance && !selected && !hovered,
},
),
)}
>
<span className="w-4 h-4">
{!node.isLeaf && (
<div
className="w-4 h-4 flex items-center justify-center"
onClick={() => node.toggle()}
>
{treeHovered && (
<motion.div
initial={false}
animate={{ rotate: node.isOpen ? 90 : 0 }}
>
<ChevronRightIcon className="h-2.5 w-2.5" />
</motion.div>
<div ref={nodeRef}>
<div
ref={dragHandle}
style={style}
onClick={() => handleSelectNode()}
onMouseOver={() => handleHoverNode()}
className={twMerge(
clsx(
'flex flex-row items-center h-6 cursor-pointer rounded w-fit min-w-full',
{
'bg-bg': hovered,
'bg-stone-800': selected,
'text-purple-100': instance && selected,
'text-purple-300': instance && !selected,
'text-purple-200': instance && !selected && hovered,
'bg-purple-700/50': instance && selected,
'bg-purple-900/60': instance && !selected && hovered,
'text-active': !instance && selected,
'text-hover': !instance && !selected && hovered,
'text-text': !instance && !selected && !hovered,
},
),
)}
>
<span className="w-4 h-4">
{!node.isLeaf && (
<div
className="w-4 h-4 flex items-center justify-center"
onClick={() => node.toggle()}
>
{treeHovered && (
<motion.div
initial={false}
animate={{ rotate: node.isOpen ? 90 : 0 }}
>
<ChevronRightIcon className="h-2.5 w-2.5" />
</motion.div>
)}
</div>
)}
</span>
{instance ? (
<Component1Icon
className={clsx(
'w-3 h-3 ml-1 mr-2',
hovered && !selected
? 'text-purple-200'
: selected
? 'text-purple-100'
: 'text-purple-300',
)}
</div>
/>
) : (
<NodeIcon iconClass="w-3 h-3 ml-1 mr-2" node={node.data} />
)}
</span>
{instance ? (
<Component1Icon
<span
className={clsx(
'w-3 h-3 ml-1 mr-2',
hovered && !selected
? 'text-purple-200'
: selected
? 'text-purple-100'
: 'text-purple-300',
'truncate space',
instance
? selected
? 'text-purple-100'
: hovered
? 'text-purple-200'
: 'text-purple-300'
: '',
)}
/>
) : (
<NodeIcon iconClass="w-3 h-3 ml-1 mr-2" node={node.data} />
)}
<span
className={clsx(
'truncate',
instance
? selected
? 'text-purple-100'
: hovered
? 'text-purple-200'
: 'text-purple-300'
: '',
)}
>
{instance?.component
? instance.component
: node.data.tagName.toLowerCase()}
{node.data.textContent}
</span>
>
{instance?.component
? instance.component
: node.data.tagName.toLowerCase()}
{' ' + node.data.textContent}
</span>
</div>
</div>
</TooltipTrigger>
{node.data.textContent !== '' && (
Expand Down

0 comments on commit b446a32

Please sign in to comment.