Skip to content

Commit

Permalink
Allow to drag the parent of the selected element (#267)
Browse files Browse the repository at this point in the history
* Allow to drag the parent of the selected element

Refactor the drag menu option to not have singleton data so we can have as many instances of as we want.
Instead, the element it will drag is passed in from the outside.

* Remove log, position handle better

* Highlight the parent of the selected item

* Dragging the parent works!

* auto format code

---------

Co-authored-by: cibernox <[email protected]>
  • Loading branch information
cibernox and cibernox authored Oct 4, 2024
1 parent ce6a3d2 commit 30875ba
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 60 deletions.
2 changes: 0 additions & 2 deletions assets/svelte/components/PageAstNode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import { updateNodeContent, updateAst } from "$lib/utils/ast-manipulation"
import { elementCanBeDroppedInTarget } from "$lib/utils/drag-helpers"
import type { AstNode } from "$lib/types"
import { initSelectedElementDragMenuPosition } from "./SelectedElementFloatingMenu/DragMenuOption.svelte"
export let node: AstNode
export let nodeId: string
Expand Down Expand Up @@ -76,7 +75,6 @@
function handleClick({ currentTarget }: Event) {
setSelection(nodeId)
setSelectedDom(currentTarget)
initSelectedElementDragMenuPosition(currentTarget)
}
function handleContentEdited({ target }: Event) {
Expand Down
4 changes: 2 additions & 2 deletions assets/svelte/components/PageWrapper.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@
</div>

<style>
:global([data-selected="true"]) {
:global([data-selected="true"], [data-selected-parent="true"]) {
outline-color: #06b6d4;
outline-width: 1px;
outline-style: solid;
}
:global([data-selected="true"].contents > *) {
:global([data-selected="true"].contents > *, [data-selected-parent="true"].contents > *) {
outline-color: #06b6d4;
outline-width: 1px;
outline-style: solid;
Expand Down
3 changes: 3 additions & 0 deletions assets/svelte/components/SelectedElementFloatingMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,7 @@
</div>

<DragMenuOption element={$selectedDomElement} />
{#if $selectedDomElement?.parentElement}
<DragMenuOption element={$selectedDomElement.parentElement} isParent={true} />
{/if}
{/if}
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<script lang="ts" context="module">
import { writable, type Writable } from "svelte/store"
import { page, selectedAstElementId, parentOfSelectedAstElement } from "$lib/stores/page"
import {
page,
selectedAstElementId,
parentOfSelectedAstElement,
parentSelectedAstElementId,
grandParentOfSelectedAstElement,
} from "$lib/stores/page"
import { findHoveredSiblingIndex, getBoundingRect, getDragDirection, type Coords } from "$lib/utils/drag-helpers"
import { live } from "$lib/stores/live"
Expand All @@ -13,41 +19,20 @@
newSiblingRects: LocationInfo[] // LocationInfo[]
}
let currentHandleCoords: Coords
let relativeWrapperRect: DOMRect
const dragHandleStyle: Writable<string> = writable("")
export const isDragging: Writable<boolean> = writable(false)
let dragElementInfo: DragInfo
export function initSelectedElementDragMenuPosition(selectedDomEl, mouseDiff?: Coords) {
let rect = dragElementInfo
? dragElementInfo.originalSiblingRects[dragElementInfo.selectedIndex]
: getBoundingRect(selectedDomEl)
updateHandleCoords(rect, mouseDiff)
let styles = []
if (currentHandleCoords?.y) {
styles.push(`top: ${currentHandleCoords.y}px`)
}
if (currentHandleCoords?.x) {
styles.push(`left: ${currentHandleCoords.x}px`)
function calculateHandleXPosition(rect: LocationInfo, position: "bottom" | "left") {
if (position === "bottom") {
return rect.x + rect.width / 2 - 5
} else {
return rect.x - 25
}
dragHandleStyle.set(styles.join(";"))
}
function calculateHandleXPosition(rect: LocationInfo) {
return rect.x + rect.width / 2 - 5
}
function calculateHandleYPosition(rect: LocationInfo) {
return rect.y + rect.height + 5
}
function updateHandleCoords(currentRect: LocationInfo, movement: Coords = { x: 0, y: 0 }) {
relativeWrapperRect = document
.getElementById("ui-builder-app-container")
.closest(".relative")
.getBoundingClientRect()
currentHandleCoords = {
x: calculateHandleXPosition(currentRect) - relativeWrapperRect.x + movement.x,
y: calculateHandleYPosition(currentRect) - relativeWrapperRect.y + movement.y,
function calculateHandleYPosition(rect: LocationInfo, position: "bottom" | "left") {
if (position === "bottom") {
return rect.y + rect.height + 5
} else {
return rect.y + rect.height / 2 - 5
}
}
</script>
Expand All @@ -60,11 +45,42 @@
let originalSiblings: Element[]
let dragHandleElement: HTMLButtonElement
let dragHandleStyle = ""
let currentHandleCoords: Coords
let relativeWrapperRect: DOMRect
let dragElementInfo: DragInfo
$: canBeDragged = element?.parentElement?.children?.length > 1
$: dragDirection = getDragDirection(element)
$: {
// Update drag menu position when the element store changes
!!element && initSelectedElementDragMenuPosition(element)
!!element && initSelectedElementDragMenuPosition(element, isParent)
}
function updateHandleCoords(currentRect: LocationInfo, isParent: boolean) {
relativeWrapperRect = document
.getElementById("ui-builder-app-container")
.closest(".relative")
.getBoundingClientRect()
const handlePosition = isParent ? "left" : "bottom"
currentHandleCoords = {
x: calculateHandleXPosition(currentRect, handlePosition) - relativeWrapperRect.x,
y: calculateHandleYPosition(currentRect, handlePosition) - relativeWrapperRect.y,
}
}
function initSelectedElementDragMenuPosition(selectedDomEl: Element, isParent: boolean = false) {
let rect = dragElementInfo
? dragElementInfo.originalSiblingRects[dragElementInfo.selectedIndex]
: getBoundingRect(selectedDomEl)
updateHandleCoords(rect, isParent)
let styles = []
if (currentHandleCoords?.y) {
styles.push(`top: ${currentHandleCoords.y}px`)
}
if (currentHandleCoords?.x) {
styles.push(`left: ${currentHandleCoords.x}px`)
}
dragHandleStyle = styles.join(";")
}
function snapshotSelectedElementSiblings() {
Expand Down Expand Up @@ -92,6 +108,7 @@
left,
}
}),
newSiblingRects: null,
}
// If this is expressed as `element.parentElement.style.display = "none"` for some reason svelte
// thinks it has to invalidate the `element` and recompute all state that observes it.
Expand All @@ -111,25 +128,35 @@
}
function applyNewOrder() {
if (newIndex !== null && newIndex !== dragElementInfo.selectedIndex) {
let parent = isParent ? $grandParentOfSelectedAstElement : $parentOfSelectedAstElement
if (newIndex !== null && newIndex !== dragElementInfo.selectedIndex && !!parent) {
// Reordering happened, apply new order
let parent = $parentOfSelectedAstElement
const selectedAstElement = parent.content.splice(dragElementInfo.selectedIndex, 1)[0]
parent.content.splice(newIndex, 0, selectedAstElement)
// Update the selectedAstElementId so the same item remains selected
if (isParent) {
let parts = $selectedAstElementId.split(".")
parts[parts.length - 2] = newIndex.toString()
$selectedAstElementId = parts.join(".")
} else {
let parts = $selectedAstElementId.split(".")
parts[parts.length - 1] = newIndex.toString()
$selectedAstElementId = parts.join(".")
}
// console.log('$page.ast[0]', $page.ast[0]);
$page.ast = [...$page.ast]
let parts = $selectedAstElementId.split(".")
parts[parts.length - 1] = newIndex.toString()
$selectedAstElementId = parts.join(".")
// Update in the server
$live.pushEvent("update_page_ast", { id: $page.id, ast: $page.ast })
}
}
function resetDragElementHandle() {
dragHandleElement.style.transform = null
dragHandleElement.style.setProperty("--tw-translate-y", null)
dragHandleElement.style.setProperty("--tw-translate-x", null)
if (dragHandleElement) {
dragHandleElement.style.transform = null
dragHandleElement.style.setProperty("--tw-translate-y", null)
dragHandleElement.style.setProperty("--tw-translate-x", null)
}
}
async function handleMouseup(e: MouseEvent) {
Expand Down Expand Up @@ -284,7 +311,7 @@
bind:this={dragHandleElement}
on:mousedown={handleMousedown}
class="rounded-full w-6 h-6 flex justify-center items-center absolute bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-blue-200 active:bg-blue-800 transform"
style={$dragHandleStyle}
style={dragHandleStyle}
>
<span
class:hero-arrows-right-left={dragDirection === "horizontal"}
Expand Down
43 changes: 31 additions & 12 deletions assets/svelte/stores/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,54 @@ export const page: Writable<Page> = writable()
export const selectedAstElementId: Writable<string | undefined> = writable()
export const highlightedAstElement: Writable<AstElement | undefined> = writable()
export const slotTargetElement: Writable<AstElement | undefined> = writable()

export const rootAstElement: Readable<AstElement | undefined> = derived([page], ([$page]) => {
// This is a virtual AstElement intended to simulate the page itself to reorder the components at the first level.
if ($page) {
return { tag: "root", attrs: {}, content: $page.ast }
}
})

export const selectedAstElement: Readable<AstElement | undefined> = derived(
[page, selectedAstElementId],
([$page, $selectedAstElementId]) => {
if ($page && $selectedAstElementId) {
if ($selectedAstElementId === "root") return get(rootAstElement)
return findAstElement($page.ast, $selectedAstElementId)
}
},
)

export const parentOfSelectedAstElement: Readable<AstElement | undefined> = derived(
[page, selectedAstElementId],
([$page, $selectedAstElementId]) => {
if ($page && $selectedAstElementId) {
if ($selectedAstElementId === "root") return null
let levels = $selectedAstElementId.split(".")
if (levels.length === 1) return get(rootAstElement)
levels.pop()
return findAstElement($page.ast, levels.join("."))
}
function getParentId(id: string | null) {
if (id === "root") return null
let levels = id.split(".")
if (levels.length === 1) return "root"
levels.pop()
return levels.join(".")
}

export const parentSelectedAstElementId: Readable<string> = derived(
[selectedAstElementId],
([$selectedAstElementId]) => {
return getParentId($selectedAstElementId)
},
)

export const grandParentSelectedAstElementId: Readable<string> = derived(
[parentSelectedAstElementId],
([$parentSelectedAstElementId]) => {
return getParentId($parentSelectedAstElementId)
},
)

export const parentOfSelectedAstElement: Readable<AstElement | undefined> = derived(
[page, parentSelectedAstElementId],
([$page, $parentSelectedAstElementId]) => findAstElement($page.ast, $parentSelectedAstElementId),
)

export const grandParentOfSelectedAstElement: Readable<AstElement | undefined> = derived(
[page, grandParentSelectedAstElementId],
([$page, $grandParentSelectedAstElementId]) => findAstElement($page.ast, $grandParentSelectedAstElementId),
)

export const selectedDomElement: Writable<Element | null> = writable(null)

export function setSelection(selectedId: string) {
Expand All @@ -56,6 +74,7 @@ export function isAstElement(maybeNode: AstNode): maybeNode is AstElement {
}

export function findAstElement(ast: AstNode[], id: string): AstElement {
if (id === "root") return get(rootAstElement)
let indexes = id.split(".").map((s) => parseInt(s, 10))
let node: AstNode = ast[indexes[0]] as AstElement
ast = node.content
Expand Down
4 changes: 2 additions & 2 deletions assets/svelte/utils/drag-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ function detectFlow(rects: DOMRect[]) {
// I'm not sure if there's any imaginative layout in which is not good enough, but just in case
// there's a second logic to check if the parent element uses a horizontal flexbox layout.
export function getDragDirection(element: Element): DragDirection {
let parentEl = element.parentElement
let parentEl = element?.parentElement

if (parentEl === null) {
if (!parentEl) {
return "vertical"
}

Expand Down

0 comments on commit 30875ba

Please sign in to comment.