From 4fbe9f13a9a340cf73433ffa3cc791e74bd8dd2c Mon Sep 17 00:00:00 2001 From: Sasha Milenkovic Date: Fri, 28 Jun 2024 07:46:21 -0400 Subject: [PATCH 1/6] internal: implements insertion points correctly for vertically spaced items --- src/index.ts | 10 +- src/plugins/insertion/index.ts | 449 +++++++++++++++++++++++++++ src/plugins/insertion/utils.ts | 0 src/types.ts | 10 + tests/pages/insertion/horizontal.vue | 96 ++++++ tests/pages/insertion/vertical.vue | 83 +++++ tests/pages/multi-drag/index.vue | 105 +++++++ 7 files changed, 751 insertions(+), 2 deletions(-) create mode 100644 src/plugins/insertion/index.ts create mode 100644 src/plugins/insertion/utils.ts create mode 100644 tests/pages/insertion/horizontal.vue create mode 100644 tests/pages/insertion/vertical.vue create mode 100644 tests/pages/multi-drag/index.vue diff --git a/src/index.ts b/src/index.ts index 0c8ecaa2..ad3d2c5b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,6 +43,7 @@ export { animations } from "./plugins/animations"; export { selections } from "./plugins/multiDrag/plugins/selections"; export { swap } from "./plugins/swap"; export { place } from "./plugins/place"; +export { insertion } from "./plugins/insertion"; export * from "./utils"; const scrollConfig: { @@ -454,9 +455,14 @@ export function remapNodes(parent: HTMLElement, force?: boolean) { parents.set(parent, { ...parentData, enabledNodes: enabledNodeRecords }); config.remapFinished(parentData); + + parentData.config.plugins?.forEach((plugin: DNDPlugin) => { + plugin(parent)?.remapFinished?.(); + }); } export function remapFinished() { + console.log("REMAP FINISHED"); if (state) { state.remapJustFinished = true; @@ -507,7 +513,7 @@ export function initDrag(eventData: NodeDragEventData): DragState { return dragState; } -function validateDragHandle(data: NodeEventData): boolean { +export function validateDragHandle(data: NodeEventData): boolean { if (!(data.e instanceof DragEvent) && !(data.e instanceof TouchEvent)) return false; @@ -988,7 +994,7 @@ function touchmove(data: NodeTouchEventData, touchState: TouchState) { } } -function handleScroll() { +export function handleScroll() { for (const direction of Object.keys(scrollConfig)) { const [x, y] = scrollConfig[direction]; diff --git a/src/plugins/insertion/index.ts b/src/plugins/insertion/index.ts new file mode 100644 index 00000000..2babc209 --- /dev/null +++ b/src/plugins/insertion/index.ts @@ -0,0 +1,449 @@ +import type { + NodeDragEventData, + ParentConfig, + DragState, + NodeTouchEventData, + NodeRecord, + TouchOverNodeEvent, + ParentEventData, + TouchOverParentEvent, + ParentRecord, + ParentData, + Node, + NodeData, + SetupNodeData, +} from "../../types"; +import { + state, + parents, + handleEnd as originalHandleEnd, + parentValues, + setParentValues, + nodeEventData, + handleScroll, + nodes, + setupNodeRemap, + remapFinished, +} from "../../index"; +import { eventCoordinates, addEvents, throttle } from "../../utils"; + +export const insertionState = { + draggedOverNodes: Array>(), +}; + +interface InsertionConfig extends ParentConfig {} + +export function insertion( + insertionConfig: Partial> = {} +) { + return (parent: HTMLElement) => { + const parentData = parents.get(parent); + + if (!parentData) return; + + const insertionParentConfig = { + ...parentData.config, + insertionConfig: insertionConfig, + } as InsertionConfig; + + return { + setup() { + insertionParentConfig.handleDragoverNode = + insertionConfig.handleDragoverNode || handleDragoverNode; + + insertionParentConfig.handleDragoverParent = + insertionConfig.handleDragoverParent || handleDragoverParent; + + parentData.config = insertionParentConfig; + + const observer = parentResizeObserver(); + + setPosition(parentData, parent); + + observer.observe(parent); + + const div = document.createElement("div"); + + div.id = "insertion-point"; + + div.style.height = "10px"; + + div.style.position = "absolute"; + + div.style.backgroundColor = "red"; + + div.style.display = "none"; + + div.style.zIndex = "1000"; + + document.body.appendChild(div); + }, + setupNodeRemap(data: SetupNodeData) { + setPosition(data.nodeData, data.node); + + const observer = nodeResizeObserver(); + + observer.observe(data.node); + }, + + remapFinished() { + defineRanges(parentData.enabledNodes); + // setTimeout(() => { + // defineRanges(data.enabledNodes); + // }, 2000); + }, + }; + }; +} + +function getAscendingRange( + node: NodeRecord, + nextNode?: NodeRecord, + isVertical?: boolean +) { + const center = node.data.top + node.data.height / 2; + + if (!nextNode) { + return { + y: [center, center + node.data.height], + x: [node.data.left, node.data.right], + }; + } + + const nextNodeCenter = nextNode.data.top + nextNode.data.height / 2; + + return { + y: [center, center + Math.abs(center - nextNodeCenter) / 2], + x: [node.data.left, node.data.right], + }; +} + +function getDescendingRange( + node: NodeRecord, + prevNode?: NodeRecord, + isVertical?: boolean +) { + const center = node.data.top + node.data.height / 2; + + if (!prevNode) { + return { + y: [center - node.data.height, center], + x: [node.data.left, node.data.right], + }; + } + + return { + y: [ + prevNode.data.bottom + Math.abs(prevNode.data.bottom - node.data.top) / 2, + center, + ], + x: [node.data.left, node.data.right], + }; +} + +function defineRanges(enabledNodes: Array>) { + enabledNodes.forEach((node, index) => { + node.data.range = {}; + if (index !== enabledNodes.length - 1) { + node.data.range.ascending = getAscendingRange( + node, + enabledNodes[index + 1], + node.data.top < enabledNodes[index + 1].data.bottom + ); + } else { + node.data.range.ascending = getAscendingRange(node, undefined); + } + if (index === 0) { + node.data.range.descending = getDescendingRange(node, undefined); + } else { + node.data.range.descending = getDescendingRange( + node, + enabledNodes[index - 1], + node.data.top > enabledNodes[index - 1].data.bottom + ); + } + }); + console.log(enabledNodes); +} + +function setPosition(data: NodeData | ParentData, el: HTMLElement) { + const { top, bottom, left, right, height } = el.getBoundingClientRect(); + + data.top = top; + data.bottom = bottom; + data.left = left; + data.right = right; + data.height = height; +} + +function nodeResizeObserver() { + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + const { target } = entry; + + const nodeData = nodes.get(target as Node); + + if (!nodeData) return; + + setPosition(nodeData, target as Node); + } + }); + + return observer; +} + +function parentResizeObserver() { + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + const { target } = entry; + + if (!(target instanceof HTMLElement)) return; + + const parentData = parents.get(target); + + if (!parentData) return; + + setPosition(parentData, target); + } + }); + + return observer; +} + +export function handleDragoverNode(data: NodeDragEventData) { + if (!state) return; + + data.e.stopPropagation(); + + data.e.preventDefault(); + + return; + + const { x, y } = eventCoordinates(data.e as DragEvent); + + state.coordinates.y = y; + + state.coordinates.x = x; + + handleScroll(); + + const closestChild = findClosest( + state.coordinates.x, + state.coordinates.y, + [data.targetData.node], + true + ); + + console.log("in node", closestChild); + + // const closestChild = findClosest( + // state.coordinates.x, + // state.coordinates.y, + // data.targetData.parent.data.enabledNodes + // ); +} + +export function handleDragoverParent(data: ParentEventData) { + console.log("dragover parent"); + if (!state) return; + + const { x, y } = eventCoordinates(data.e as DragEvent); + + state.coordinates.y = y; + + state.coordinates.x = x; + + handleScroll(); + + const enabledNodes = data.targetData.parent.data.enabledNodes; + + let foundRange: [NodeRecord, string] | null = null; + + for (let x = 0; x < enabledNodes.length; x++) { + if (!state || !enabledNodes[x].data.range) continue; + + if (enabledNodes[x].data.range.ascending) { + if ( + state.coordinates.y > enabledNodes[x].data.range.ascending.y[0] && + state.coordinates.y < enabledNodes[x].data.range.ascending.y[1] && + state.coordinates.x > enabledNodes[x].data.range.ascending.x[0] && + state.coordinates.x < enabledNodes[x].data.range.ascending.x[1] + ) { + foundRange = [enabledNodes[x], "ascending"]; + + break; + } + } + + if (enabledNodes[x].data.range.descending) { + if ( + state.coordinates.y > enabledNodes[x].data.range.descending.y[0] && + state.coordinates.y < enabledNodes[x].data.range.descending.y[1] && + state.coordinates.x > enabledNodes[x].data.range.descending.x[0] && + state.coordinates.x < enabledNodes[x].data.range.descending.x[1] + ) { + foundRange = [enabledNodes[x], "descending"]; + + break; + } + } + } + + if (!foundRange) return; + + console.log("found range", foundRange[0].data.range); + + const div = document.getElementById("insertion-point"); + + if (!div) return; + + div.style.display = "block"; + + const position = foundRange[0].data.range[foundRange[1]]; + + console.log("position[0]", position.y[0]); + console.log("position[1]", position.y[1]); + + const topPosition = + position.y[foundRange[1] === "ascending" ? 1 : 0] - + div.getBoundingClientRect().height / 2; + + console.log("top position", topPosition); + + div.style.top = `${topPosition}px`; + + const leftCoordinate = position.x[0]; + + const rightCoordinate = position.x[1]; + + div.style.left = `${leftCoordinate}px`; + + div.style.right = `${rightCoordinate}px`; + + // console.log("parent closest", closestChild); + + // if (!closestChild) return; + + data.e.stopPropagation(); + + data.e.preventDefault(); + + // transfer(data, state); +} + +// function findClosest(cursorX: number, cursorY: number, nodes: NodeRecord[]) { +// let closestNode = null; +// let minDistance = 100; +// let closestDistance = Infinity; + +// nodes.forEach((node) => { +// const yDistances = [ +// { side: "top", distance: Math.abs(cursorY - node.data.top) }, +// { side: "bottom", distance: Math.abs(cursorY - node.data.bottom) }, +// ]; + +// const xDistances = [ +// { side: "left", distance: Math.abs(cursorX - node.data.left) }, +// { side: "right", distance: Math.abs(cursorX - node.data.right) }, +// ]; + +// const closestY = yDistances.reduce((prev, current) => +// prev.distance < current.distance ? prev : current +// ); + +// const closestX = xDistances.reduce((prev, current) => +// prev.distance < current.distance ? prev : current +// ); + +// const closestSide = +// closestY.distance < closestX.distance ? closestY : closestX; + +// if ( +// (closestSide.distance < closestDistance && +// ["left", "right"].includes(closestSide.side) && +// closestY.distance < minDistance) || +// (["top", "bottom"].includes(closestSide.side) && +// closestX.distance < minDistance) +// ) { +// console.log("getting here"); +// closestDistance = closestSide.distance; +// closestNode = { ...node, closestSide: closestSide.side }; +// } +// }); + +// return closestNode; +// } + +/** + * Returns the node + * + * @param parent + * @returns Node | HTMLElement | null + */ +function placeIndicator( + cursorX: number, + cursorY: number, + nodes: NodeRecord[] +) { + const closestNode = findClosestNode(cursorX, cursorY, nodes); + + if (!closestNode) return; + + console.log(closestNode.el.id); +} + +// function findClosestNode( +// cursorX: number, +// cursorY: number, +// nodes: NodeRecord[] +// ) { +// let closestNode = null; +// let closestDistanceY = Infinity; +// let closestDistanceX = Infinity; + +// nodes.forEach((node, index) => { +// const yDistances = [ +// { side: "top", distance: Math.abs(cursorY - node.data.top) }, +// { side: "bottom", distance: Math.abs(cursorY - node.data.bottom) }, +// ]; + +// const xDistances = [ +// { side: "left", distance: Math.abs(cursorX - node.data.left) }, +// { side: "right", distance: Math.abs(cursorX - node.data.right) }, +// ]; + +// const closestY = yDistances.reduce((prev, current) => +// prev.distance < current.distance ? prev : current +// ); + +// const closestX = xDistances.reduce((prev, current) => +// prev.distance < current.distance ? prev : current +// ); + +// const closestSide = +// closestY.distance < closestX.distance ? closestY : closestX; + +// // console.log("closest side", closestSide, closestY, closestX); + +// if ( +// (["left", "right"].includes(closestSide.side) && +// closestSide.distance <= closestDistanceX && +// closestY.distance <= closestDistanceY) || +// (["top", "bottom"].includes(closestSide.side) && +// closestSide.distance <= closestDistanceY && +// closestX.distance <= closestDistanceX) +// ) { +// closestDistanceY = closestY.distance; +// closestDistanceX = closestX.distance; +// closestNode = { +// ...node, +// index, +// closestSide: closestSide, +// closestY: closestY, +// closestX: closestX, +// }; +// } +// }); + +// return closestNode; +// } diff --git a/src/plugins/insertion/utils.ts b/src/plugins/insertion/utils.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/types.ts b/src/types.ts index 62fc34c1..7dcea624 100644 --- a/src/types.ts +++ b/src/types.ts @@ -204,6 +204,8 @@ export interface ParentData { * The abort controllers for the parent. */ abortControllers: Record; + + [key: string]: any; } /** @@ -227,6 +229,8 @@ export interface NodeData { * The abort controllers for the node. */ abortControllers: Record; + + [key: string]: any; } /** @@ -414,6 +418,10 @@ export interface DNDPluginData { * Called when the parent is dragged over. */ tearDownNodeRemap?: TearDownNode; + /** + * Called when all nodes have finished remapping for a given parent + */ + remapFinished?: RemapFinished; } /** @@ -434,6 +442,8 @@ export type SetupNode = (data: SetupNodeData) => void; export type TearDownNode = (data: TearDownNodeData) => void; +export type RemapFinished = (data: ParentData) => void; + /** * The payload of when the setupNode function is called in a given plugin. */ diff --git a/tests/pages/insertion/horizontal.vue b/tests/pages/insertion/horizontal.vue new file mode 100644 index 00000000..ca68394a --- /dev/null +++ b/tests/pages/insertion/horizontal.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/tests/pages/insertion/vertical.vue b/tests/pages/insertion/vertical.vue new file mode 100644 index 00000000..ac4e2602 --- /dev/null +++ b/tests/pages/insertion/vertical.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/tests/pages/multi-drag/index.vue b/tests/pages/multi-drag/index.vue new file mode 100644 index 00000000..60c519d1 --- /dev/null +++ b/tests/pages/multi-drag/index.vue @@ -0,0 +1,105 @@ + + + + + From 89c80314ae309265c50e549430a44497cac44a1f Mon Sep 17 00:00:00 2001 From: Sasha Milenkovic Date: Fri, 28 Jun 2024 09:27:10 -0400 Subject: [PATCH 2/6] internal: now handles both horizontal and vertical layouts --- src/plugins/insertion/index.ts | 414 +++++++++++++++------------ tests/pages/insertion/horizontal.vue | 26 +- tests/pages/insertion/mixed.vue | 94 ++++++ 3 files changed, 321 insertions(+), 213 deletions(-) create mode 100644 tests/pages/insertion/mixed.vue diff --git a/src/plugins/insertion/index.ts b/src/plugins/insertion/index.ts index 2babc209..26348208 100644 --- a/src/plugins/insertion/index.ts +++ b/src/plugins/insertion/index.ts @@ -25,7 +25,7 @@ import { setupNodeRemap, remapFinished, } from "../../index"; -import { eventCoordinates, addEvents, throttle } from "../../utils"; +import { eventCoordinates, removeClass, addClass, throttle } from "../../utils"; export const insertionState = { draggedOverNodes: Array>(), @@ -54,6 +54,9 @@ export function insertion( insertionParentConfig.handleDragoverParent = insertionConfig.handleDragoverParent || handleDragoverParent; + insertionParentConfig.handleEnd = + insertionConfig.handleEnd || handleEnd; + parentData.config = insertionParentConfig; const observer = parentResizeObserver(); @@ -66,11 +69,9 @@ export function insertion( div.id = "insertion-point"; - div.style.height = "10px"; - div.style.position = "absolute"; - div.style.backgroundColor = "red"; + div.style.backgroundColor = "green"; div.style.display = "none"; @@ -88,25 +89,20 @@ export function insertion( remapFinished() { defineRanges(parentData.enabledNodes); - // setTimeout(() => { - // defineRanges(data.enabledNodes); - // }, 2000); + console.log("enabled nodes", parentData.enabledNodes); }, }; }; } -function getAscendingRange( - node: NodeRecord, - nextNode?: NodeRecord, - isVertical?: boolean -) { +function ascendingVertical(node: NodeRecord, nextNode?: NodeRecord) { const center = node.data.top + node.data.height / 2; if (!nextNode) { return { y: [center, center + node.data.height], x: [node.data.left, node.data.right], + vertical: true, }; } @@ -115,20 +111,38 @@ function getAscendingRange( return { y: [center, center + Math.abs(center - nextNodeCenter) / 2], x: [node.data.left, node.data.right], + vertical: true, }; } -function getDescendingRange( - node: NodeRecord, - prevNode?: NodeRecord, - isVertical?: boolean -) { +function ascendingHorizontal(node: NodeRecord, nextNode?: NodeRecord) { + const center = node.data.left + node.data.width / 2; + + if (!nextNode) { + return { + x: [center, center + node.data.width], + y: [node.data.top, node.data.bottom], + vertical: false, + }; + } + + const nextNodeCenter = nextNode.data.left + nextNode.data.width / 2; + + return { + x: [center, center + Math.abs(center - nextNodeCenter) / 2], + y: [node.data.top, node.data.bottom], + vertical: false, + }; +} + +function descendingVertical(node: NodeRecord, prevNode?: NodeRecord) { const center = node.data.top + node.data.height / 2; if (!prevNode) { return { y: [center - node.data.height, center], x: [node.data.left, node.data.right], + vertical: true, }; } @@ -138,42 +152,88 @@ function getDescendingRange( center, ], x: [node.data.left, node.data.right], + vertical: true, }; } +function descendingHorizontal( + node: NodeRecord, + prevNode?: NodeRecord +) { + const center = node.data.left + node.data.width / 2; + + if (!prevNode) { + return { + x: [center - node.data.width, center], + y: [node.data.top, node.data.bottom], + vertical: false, + }; + } + + return { + x: [ + prevNode.data.right + Math.abs(prevNode.data.right - node.data.left) / 2, + center, + ], + y: [node.data.top, node.data.bottom], + vertical: false, + }; +} + +function getLayoutDirection(el1: NodeRecord, el2: NodeRecord) { + const isAboveOrBelow = + el1.data.top > el2.data.bottom || el1.data.bottom < el2.data.top; + + return isAboveOrBelow ? "column" : "row"; +} + function defineRanges(enabledNodes: Array>) { enabledNodes.forEach((node, index) => { node.data.range = {}; + if (index !== enabledNodes.length - 1) { - node.data.range.ascending = getAscendingRange( - node, - enabledNodes[index + 1], - node.data.top < enabledNodes[index + 1].data.bottom - ); + console.log(getLayoutDirection(node, enabledNodes[index + 1])); + const vertical = + getLayoutDirection(node, enabledNodes[index + 1]) === "column"; + node.data.range.ascending = vertical + ? ascendingVertical(node, enabledNodes[index + 1]) + : ascendingHorizontal(node, enabledNodes[index + 1]); } else { - node.data.range.ascending = getAscendingRange(node, undefined); + const vertical = + getLayoutDirection(node, enabledNodes[index - 1]) === "column"; + node.data.range.ascending = vertical + ? ascendingVertical(node) + : ascendingHorizontal(node); } + if (index === 0) { - node.data.range.descending = getDescendingRange(node, undefined); + const vertical = + getLayoutDirection(node, enabledNodes[index + 1]) === "column"; + + node.data.range.descending = vertical + ? descendingVertical(node) + : descendingHorizontal(node); } else { - node.data.range.descending = getDescendingRange( - node, - enabledNodes[index - 1], - node.data.top > enabledNodes[index - 1].data.bottom - ); + const vertical = + getLayoutDirection(node, enabledNodes[index - 1]) === "column"; + + node.data.range.descending = vertical + ? descendingVertical(node, enabledNodes[index - 1]) + : descendingHorizontal(node, enabledNodes[index - 1]); } }); - console.log(enabledNodes); } function setPosition(data: NodeData | ParentData, el: HTMLElement) { - const { top, bottom, left, right, height } = el.getBoundingClientRect(); + const { top, bottom, left, right, height, width } = + el.getBoundingClientRect(); data.top = top; data.bottom = bottom; data.left = left; data.right = right; data.height = height; + data.width = width; } function nodeResizeObserver() { @@ -213,12 +273,12 @@ function parentResizeObserver() { export function handleDragoverNode(data: NodeDragEventData) { if (!state) return; + if (data.targetData.parent.el !== state.lastParent.el) return; + data.e.stopPropagation(); data.e.preventDefault(); - return; - const { x, y } = eventCoordinates(data.e as DragEvent); state.coordinates.y = y; @@ -227,36 +287,24 @@ export function handleDragoverNode(data: NodeDragEventData) { handleScroll(); - const closestChild = findClosest( - state.coordinates.x, - state.coordinates.y, - [data.targetData.node], - true - ); - - console.log("in node", closestChild); - - // const closestChild = findClosest( - // state.coordinates.x, - // state.coordinates.y, - // data.targetData.parent.data.enabledNodes - // ); -} - -export function handleDragoverParent(data: ParentEventData) { - console.log("dragover parent"); - if (!state) return; + const foundRange = findClosest([data.targetData.node]); - const { x, y } = eventCoordinates(data.e as DragEvent); + if (!foundRange) return; - state.coordinates.y = y; + const position = foundRange[0].data.range[foundRange[1]]; - state.coordinates.x = x; + positionInsertionPoint( + position, + foundRange[1] === "ascending", + foundRange[0] + ); - handleScroll(); + data.e.stopPropagation(); - const enabledNodes = data.targetData.parent.data.enabledNodes; + data.e.preventDefault(); +} +function findClosest(enabledNodes: NodeRecord[]) { let foundRange: [NodeRecord, string] | null = null; for (let x = 0; x < enabledNodes.length; x++) { @@ -271,7 +319,7 @@ export function handleDragoverParent(data: ParentEventData) { ) { foundRange = [enabledNodes[x], "ascending"]; - break; + return foundRange; } } @@ -284,166 +332,150 @@ export function handleDragoverParent(data: ParentEventData) { ) { foundRange = [enabledNodes[x], "descending"]; - break; + return foundRange; } } } +} + +export function handleDragoverParent(data: ParentEventData) { + if (!state) return; + + if (data.targetData.parent.el !== state.lastParent.el) return; + + const { x, y } = eventCoordinates(data.e as DragEvent); + + state.coordinates.y = y; + + state.coordinates.x = x; + + handleScroll(); + + const enabledNodes = data.targetData.parent.data.enabledNodes; + + const foundRange = findClosest(enabledNodes); if (!foundRange) return; - console.log("found range", foundRange[0].data.range); + const position = foundRange[0].data.range[foundRange[1]]; + + positionInsertionPoint( + position, + foundRange[1] === "ascending", + foundRange[0] + ); + + data.e.stopPropagation(); + + data.e.preventDefault(); +} +function positionInsertionPoint( + position: { x: number[]; y: number[]; vertical: boolean }, + ascending: boolean, + node: NodeRecord +) { const div = document.getElementById("insertion-point"); if (!div) return; - div.style.display = "block"; + if (position.vertical) { + const topPosition = + position.y[ascending ? 1 : 0] - div.getBoundingClientRect().height / 2; - const position = foundRange[0].data.range[foundRange[1]]; + div.style.top = `${topPosition}px`; - console.log("position[0]", position.y[0]); - console.log("position[1]", position.y[1]); + const leftCoordinate = position.x[0]; - const topPosition = - position.y[foundRange[1] === "ascending" ? 1 : 0] - - div.getBoundingClientRect().height / 2; + const rightCoordinate = position.x[1]; - console.log("top position", topPosition); + div.style.left = `${leftCoordinate}px`; - div.style.top = `${topPosition}px`; + div.style.right = `${rightCoordinate}px`; - const leftCoordinate = position.x[0]; + div.style.height = "10px"; - const rightCoordinate = position.x[1]; + div.style.width = rightCoordinate - leftCoordinate + "px"; + } else { + const leftPosition = + position.x[ascending ? 1 : 0] - div.getBoundingClientRect().width / 2; - div.style.left = `${leftCoordinate}px`; + div.style.left = `${leftPosition}px`; - div.style.right = `${rightCoordinate}px`; + const topCoordinate = position.y[0]; - // console.log("parent closest", closestChild); + const bottomCoordinate = position.y[1]; - // if (!closestChild) return; + div.style.top = `${topCoordinate}px`; - data.e.stopPropagation(); + div.style.bottom = `${bottomCoordinate}px`; - data.e.preventDefault(); + div.style.width = "10px"; - // transfer(data, state); -} + div.style.height = bottomCoordinate - topCoordinate + "px"; + } -// function findClosest(cursorX: number, cursorY: number, nodes: NodeRecord[]) { -// let closestNode = null; -// let minDistance = 100; -// let closestDistance = Infinity; - -// nodes.forEach((node) => { -// const yDistances = [ -// { side: "top", distance: Math.abs(cursorY - node.data.top) }, -// { side: "bottom", distance: Math.abs(cursorY - node.data.bottom) }, -// ]; - -// const xDistances = [ -// { side: "left", distance: Math.abs(cursorX - node.data.left) }, -// { side: "right", distance: Math.abs(cursorX - node.data.right) }, -// ]; - -// const closestY = yDistances.reduce((prev, current) => -// prev.distance < current.distance ? prev : current -// ); - -// const closestX = xDistances.reduce((prev, current) => -// prev.distance < current.distance ? prev : current -// ); - -// const closestSide = -// closestY.distance < closestX.distance ? closestY : closestX; - -// if ( -// (closestSide.distance < closestDistance && -// ["left", "right"].includes(closestSide.side) && -// closestY.distance < minDistance) || -// (["top", "bottom"].includes(closestSide.side) && -// closestX.distance < minDistance) -// ) { -// console.log("getting here"); -// closestDistance = closestSide.distance; -// closestNode = { ...node, closestSide: closestSide.side }; -// } -// }); - -// return closestNode; -// } - -/** - * Returns the node - * - * @param parent - * @returns Node | HTMLElement | null - */ -function placeIndicator( - cursorX: number, - cursorY: number, - nodes: NodeRecord[] -) { - const closestNode = findClosestNode(cursorX, cursorY, nodes); + insertionState.draggedOverNodes = [node]; + + insertionState.targetIndex = node.data.index; - if (!closestNode) return; + insertionState.ascending = ascending; - console.log(closestNode.el.id); + div.style.display = "block"; } -// function findClosestNode( -// cursorX: number, -// cursorY: number, -// nodes: NodeRecord[] -// ) { -// let closestNode = null; -// let closestDistanceY = Infinity; -// let closestDistanceX = Infinity; - -// nodes.forEach((node, index) => { -// const yDistances = [ -// { side: "top", distance: Math.abs(cursorY - node.data.top) }, -// { side: "bottom", distance: Math.abs(cursorY - node.data.bottom) }, -// ]; - -// const xDistances = [ -// { side: "left", distance: Math.abs(cursorX - node.data.left) }, -// { side: "right", distance: Math.abs(cursorX - node.data.right) }, -// ]; - -// const closestY = yDistances.reduce((prev, current) => -// prev.distance < current.distance ? prev : current -// ); - -// const closestX = xDistances.reduce((prev, current) => -// prev.distance < current.distance ? prev : current -// ); - -// const closestSide = -// closestY.distance < closestX.distance ? closestY : closestX; - -// // console.log("closest side", closestSide, closestY, closestX); - -// if ( -// (["left", "right"].includes(closestSide.side) && -// closestSide.distance <= closestDistanceX && -// closestY.distance <= closestDistanceY) || -// (["top", "bottom"].includes(closestSide.side) && -// closestSide.distance <= closestDistanceY && -// closestX.distance <= closestDistanceX) -// ) { -// closestDistanceY = closestY.distance; -// closestDistanceX = closestX.distance; -// closestNode = { -// ...node, -// index, -// closestSide: closestSide, -// closestY: closestY, -// closestX: closestX, -// }; -// } -// }); - -// return closestNode; -// } +function handleEnd(data: NodeDragEventData | NodeTouchEventData) { + if (!state) return; + + if (state.transferred || state.lastParent.el !== state.initialParent.el) + return; + + const draggedParentValues = parentValues( + state.initialParent.el, + state.initialParent.data + ); + + const draggedValues = state.draggedNodes.map((node) => node.data.value); + + const newParentValues = [ + ...draggedParentValues.filter((x) => !draggedValues.includes(x)), + ]; + + let index = insertionState.draggedOverNodes[0].data.index; + + if ( + insertionState.targetIndex > state.draggedNodes[0].data.index && + !insertionState.ascending + ) { + index--; + } else if ( + insertionState.targetIndex < state.draggedNodes[0].data.index && + insertionState.ascending + ) { + index--; + } + + newParentValues.splice(index, 0, ...draggedValues); + + setParentValues(data.targetData.parent.el, data.targetData.parent.data, [ + ...newParentValues, + ]); + + const dropZoneClass = + "touchedNode" in state + ? data.targetData.parent.data.config.touchDropZoneClass + : data.targetData.parent.data.config.dropZoneClass; + + removeClass( + insertionState.draggedOverNodes.map((node) => node.el), + dropZoneClass + ); + + const div = document.getElementById("insertion-point"); + + if (!div) return; + + div.style.display = "none"; + + originalHandleEnd(data); +} diff --git a/tests/pages/insertion/horizontal.vue b/tests/pages/insertion/horizontal.vue index ca68394a..13915b16 100644 --- a/tests/pages/insertion/horizontal.vue +++ b/tests/pages/insertion/horizontal.vue @@ -3,7 +3,7 @@ import { useDragAndDrop } from "../../../src/vue/index"; import { insertion } from "../../../src/index"; const [parent, values] = useDragAndDrop( - ["Apple", "Banana", "Orange", "Strawberry", "Pineapple", "Grapes"], + ["Apple", "Banana", "Starwberry", "Pineapple"], { plugins: [insertion()], dropZoneClass: "hover", @@ -33,27 +33,13 @@ const [parent, values] = useDragAndDrop( From b2712ceedcfd8097e656a6b7c40eeed311e39478 Mon Sep 17 00:00:00 2001 From: Sasha Milenkovic Date: Fri, 28 Jun 2024 09:55:53 -0400 Subject: [PATCH 3/6] internal: changes way classes are applied in mixed.vue example --- src/plugins/insertion/index.ts | 7 +++++-- tests/pages/insertion/mixed.vue | 16 ++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/plugins/insertion/index.ts b/src/plugins/insertion/index.ts index 26348208..88f8918d 100644 --- a/src/plugins/insertion/index.ts +++ b/src/plugins/insertion/index.ts @@ -192,7 +192,6 @@ function defineRanges(enabledNodes: Array>) { node.data.range = {}; if (index !== enabledNodes.length - 1) { - console.log(getLayoutDirection(node, enabledNodes[index + 1])); const vertical = getLayoutDirection(node, enabledNodes[index + 1]) === "column"; node.data.range.ascending = vertical @@ -443,6 +442,8 @@ function handleEnd(data: NodeDragEventData | NodeTouchEventData) { let index = insertionState.draggedOverNodes[0].data.index; + console.log("original index", index); + if ( insertionState.targetIndex > state.draggedNodes[0].data.index && !insertionState.ascending @@ -452,9 +453,11 @@ function handleEnd(data: NodeDragEventData | NodeTouchEventData) { insertionState.targetIndex < state.draggedNodes[0].data.index && insertionState.ascending ) { - index--; + index++; } + console.log("new indedx", index); + newParentValues.splice(index, 0, ...draggedValues); setParentValues(data.targetData.parent.el, data.targetData.parent.data, [ diff --git a/tests/pages/insertion/mixed.vue b/tests/pages/insertion/mixed.vue index cb8cb2ee..f4bea0fe 100644 --- a/tests/pages/insertion/mixed.vue +++ b/tests/pages/insertion/mixed.vue @@ -19,7 +19,15 @@ const [parent, values] = useDragAndDrop(

Insertion Plugin

    -
    +
    {{ value }}
    @@ -64,11 +72,7 @@ const [parent, values] = useDragAndDrop( margin-bottom: 2em; } -.item:nth-child(1) { - width: 600px; -} - -.item:nth-child(6) { +.large { width: 600px; } From fd5c8f4171fea43a1255e7f0cacf8e227aea72a5 Mon Sep 17 00:00:00 2001 From: Sasha Milenkovic Date: Fri, 28 Jun 2024 13:32:45 -0400 Subject: [PATCH 4/6] feat: --- src/index.ts | 8 +- src/plugins/insertion/index.ts | 234 +++++++++++++++++++++------ tests/pages/insertion/horizontal.vue | 1 + tests/pages/insertion/mixed.vue | 8 +- tests/pages/insertion/vertical.vue | 4 + 5 files changed, 203 insertions(+), 52 deletions(-) diff --git a/src/index.ts b/src/index.ts index ad3d2c5b..57570b4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -482,13 +482,16 @@ export function handleDragstart(data: NodeEventData) { export function dragstartClasses( el: HTMLElement | Node | Element, draggingClass: string | undefined, - dropZoneClass: string | undefined + dropZoneClass: string | undefined, + dragPlaceholderClass: string | undefined ) { addClass([el], draggingClass); setTimeout(() => { removeClass([el], draggingClass); + addClass([el], dragPlaceholderClass); + addClass([el], dropZoneClass); }); } @@ -573,7 +576,8 @@ export function dragstart(data: NodeDragEventData) { dragstartClasses( dragState.draggedNode.el, config.draggingClass, - config.dropZoneClass + config.dropZoneClass, + config.dragPlaceholderClass ); } diff --git a/src/plugins/insertion/index.ts b/src/plugins/insertion/index.ts index 88f8918d..a9dd8fa6 100644 --- a/src/plugins/insertion/index.ts +++ b/src/plugins/insertion/index.ts @@ -1,13 +1,9 @@ import type { NodeDragEventData, ParentConfig, - DragState, NodeTouchEventData, NodeRecord, - TouchOverNodeEvent, ParentEventData, - TouchOverParentEvent, - ParentRecord, ParentData, Node, NodeData, @@ -19,13 +15,11 @@ import { handleEnd as originalHandleEnd, parentValues, setParentValues, - nodeEventData, handleScroll, nodes, - setupNodeRemap, - remapFinished, + dragstart, } from "../../index"; -import { eventCoordinates, removeClass, addClass, throttle } from "../../utils"; +import { eventCoordinates, removeClass } from "../../utils"; export const insertionState = { draggedOverNodes: Array>(), @@ -48,6 +42,9 @@ export function insertion( return { setup() { + insertionParentConfig.handleDragstart = + insertionConfig.handleDragstart || handleDragstart; + insertionParentConfig.handleDragoverNode = insertionConfig.handleDragoverNode || handleDragoverNode; @@ -89,15 +86,32 @@ export function insertion( remapFinished() { defineRanges(parentData.enabledNodes); - console.log("enabled nodes", parentData.enabledNodes); }, }; }; } +function handleDragstart(data: NodeDragEventData) { + if (!(data.e instanceof DragEvent)) return; + + dragstart({ + e: data.e, + targetData: data.targetData, + }); + + setTimeout(() => { + for (const node of data.targetData.parent.data.enabledNodes) { + setPosition(node.data, node.el); + } + defineRanges(data.targetData.parent.data.enabledNodes); + }); +} + function ascendingVertical(node: NodeRecord, nextNode?: NodeRecord) { const center = node.data.top + node.data.height / 2; + console.log("next noide center", node.el.id, nextNode); + if (!nextNode) { return { y: [center, center + node.data.height], @@ -115,7 +129,11 @@ function ascendingVertical(node: NodeRecord, nextNode?: NodeRecord) { }; } -function ascendingHorizontal(node: NodeRecord, nextNode?: NodeRecord) { +function ascendingHorizontal( + node: NodeRecord, + nextNode?: NodeRecord, + lastInRow = false +) { const center = node.data.left + node.data.width / 2; if (!nextNode) { @@ -126,13 +144,21 @@ function ascendingHorizontal(node: NodeRecord, nextNode?: NodeRecord) { }; } - const nextNodeCenter = nextNode.data.left + nextNode.data.width / 2; + if (lastInRow) { + return { + x: [center, center + node.data.width], + y: [node.data.top, node.data.bottom], + vertical: false, + }; + } else { + const nextNodeCenter = nextNode.data.left + nextNode.data.width / 2; - return { - x: [center, center + Math.abs(center - nextNodeCenter) / 2], - y: [node.data.top, node.data.bottom], - vertical: false, - }; + return { + x: [center, center + Math.abs(center - nextNodeCenter) / 2], + y: [node.data.top, node.data.bottom], + vertical: false, + }; + } } function descendingVertical(node: NodeRecord, prevNode?: NodeRecord) { @@ -180,45 +206,151 @@ function descendingHorizontal( }; } -function getLayoutDirection(el1: NodeRecord, el2: NodeRecord) { - const isAboveOrBelow = - el1.data.top > el2.data.bottom || el1.data.bottom < el2.data.top; +function getLayoutDirection( + node: NodeRecord, + nodePrevious?: NodeRecord, + nodeAfter?: NodeRecord +): ["row" | "column"] { + let aboveOrBelowPrevious; + if (nodePrevious) { + aboveOrBelowPrevious = + node.data.top > nodePrevious.data.bottom || + node.data.bottom < nodePrevious.data.top; + } + + let aboveOrBelowAfter; + + if (nodeAfter) { + aboveOrBelowAfter = + node.data.top > nodeAfter.data.bottom || + node.data.bottom < nodeAfter.data.top; + } + + if (aboveOrBelowAfter && !aboveOrBelowPrevious) { + return "row"; + } + + if (aboveOrBelowPrevious && !aboveOrBelowAfter) { + return ["column"]; + } + return "row"; + // if (el1.el.id === 'Watermelon' && el2.el.id === 'Grape') { + + // } + // const isAboveOrBelow = + // el1.data.top > el2.data.bottom || el1.data.bottom < el2.data.top; - return isAboveOrBelow ? "column" : "row"; + // return isAboveOrBelow ? "column" : "row"; } function defineRanges(enabledNodes: Array>) { enabledNodes.forEach((node, index) => { node.data.range = {}; + let aboveOrBelowPrevious = false; + + let aboveOrBelowAfter = false; + + let nextNode = enabledNodes[index + 1]; + + if (enabledNodes[index - 1]) { + aboveOrBelowPrevious = + node.data.top > enabledNodes[index - 1].data.bottom || + node.data.bottom < enabledNodes[index - 1].data.top; + } + + if (enabledNodes[index + 1]) { + aboveOrBelowAfter = + node.data.top > enabledNodes[index + 1].data.bottom || + node.data.bottom < enabledNodes[index + 1].data.top; + } + + if (aboveOrBelowAfter && !aboveOrBelowPrevious) { + node.data.range.ascending = ascendingHorizontal( + node, + enabledNodes[index + 1], + true + ); + node.data.range.descending = descendingHorizontal( + node, + enabledNodes[index - 1] + ); + } else if (!aboveOrBelowPrevious && !aboveOrBelowAfter) { + node.data.range.ascending = ascendingHorizontal( + node, + enabledNodes[index + 1] + ); + node.data.range.descending = descendingHorizontal( + node, + enabledNodes[index - 1] + ); + } else if (aboveOrBelowPrevious && !nextNode) { + node.data.range.ascending = ascendingHorizontal(node); + } else if (aboveOrBelowPrevious && !aboveOrBelowAfter) { + node.data.range.ascending = ascendingVertical(node); + } else if (aboveOrBelowAfter && aboveOrBelowPrevious) { + node.data.range.ascending = ascendingVertical( + node, + enabledNodes[index + 1] + ); + } + + // if (index !== 0) { + // const aboveOrBelowPrevious = + // node.data.top > enabledNodes[index - 1].data.bottom || + // node.data.bottom < enabledNodes[index - 1].data.top; + + // const aboveOrBelowAfter = + + // console.log("above or below previous", aboveOrBelowPrevious); + + // } + + return; + if (index !== enabledNodes.length - 1) { - const vertical = - getLayoutDirection(node, enabledNodes[index + 1]) === "column"; - node.data.range.ascending = vertical - ? ascendingVertical(node, enabledNodes[index + 1]) - : ascendingHorizontal(node, enabledNodes[index + 1]); + console.log("getting here", node.el.id, enabledNodes[index + 1].el.id); + // const vertical = + // getLayoutDirection( + // node, + // enabledNodes[index - 1], + // enabledNodes[index + 1] + // ) === "column"; + // node.data.range.ascending = vertical + // ? ascendingVertical(node, enabledNodes[index - 1]) + // : ascendingHorizontal(node, enabledNodes[index + 1]); } else { - const vertical = - getLayoutDirection(node, enabledNodes[index - 1]) === "column"; - node.data.range.ascending = vertical - ? ascendingVertical(node) - : ascendingHorizontal(node); + // const vertical = + // getLayoutDirection( + // node, + // enabledNodes[index - 1], + // enabledNodes[index + 1] + // ) === "column"; + // node.data.range.ascending = vertical + // ? ascendingVertical(node) + // : ascendingHorizontal(node); } if (index === 0) { - const vertical = - getLayoutDirection(node, enabledNodes[index + 1]) === "column"; - - node.data.range.descending = vertical - ? descendingVertical(node) - : descendingHorizontal(node); + // const vertical = + // getLayoutDirection( + // node, + // enabledNodes[index - 1], + // enabledNodes[index + 1] + // ) === "column"; + // node.data.range.descending = vertical + // ? descendingVertical(node) + // : descendingHorizontal(node); } else { - const vertical = - getLayoutDirection(node, enabledNodes[index - 1]) === "column"; - - node.data.range.descending = vertical - ? descendingVertical(node, enabledNodes[index - 1]) - : descendingHorizontal(node, enabledNodes[index - 1]); + // const vertical = + // getLayoutDirection( + // node, + // enabledNodes[index - 1], + // enabledNodes[index + 1] + // ) === "column"; + // node.data.range.descending = vertical + // ? descendingVertical(node, enabledNodes[index - 1]) + // : descendingHorizontal(node, enabledNodes[index - 1]); } }); } @@ -274,8 +406,6 @@ export function handleDragoverNode(data: NodeDragEventData) { if (data.targetData.parent.el !== state.lastParent.el) return; - data.e.stopPropagation(); - data.e.preventDefault(); const { x, y } = eventCoordinates(data.e as DragEvent); @@ -374,10 +504,14 @@ function positionInsertionPoint( ascending: boolean, node: NodeRecord ) { + if (!state) return; + const div = document.getElementById("insertion-point"); if (!div) return; + if (node.el === state.draggedNodes[0].el) return; + if (position.vertical) { const topPosition = position.y[ascending ? 1 : 0] - div.getBoundingClientRect().height / 2; @@ -442,8 +576,6 @@ function handleEnd(data: NodeDragEventData | NodeTouchEventData) { let index = insertionState.draggedOverNodes[0].data.index; - console.log("original index", index); - if ( insertionState.targetIndex > state.draggedNodes[0].data.index && !insertionState.ascending @@ -456,8 +588,6 @@ function handleEnd(data: NodeDragEventData | NodeTouchEventData) { index++; } - console.log("new indedx", index); - newParentValues.splice(index, 0, ...draggedValues); setParentValues(data.targetData.parent.el, data.targetData.parent.data, [ @@ -480,5 +610,13 @@ function handleEnd(data: NodeDragEventData | NodeTouchEventData) { div.style.display = "none"; + const dragPlaceholderClass = + data.targetData.parent.data.config.dragPlaceholderClass; + + removeClass( + state.draggedNodes.map((node) => node.el), + dragPlaceholderClass + ); + originalHandleEnd(data); } diff --git a/tests/pages/insertion/horizontal.vue b/tests/pages/insertion/horizontal.vue index 13915b16..f634f6de 100644 --- a/tests/pages/insertion/horizontal.vue +++ b/tests/pages/insertion/horizontal.vue @@ -7,6 +7,7 @@ const [parent, values] = useDragAndDrop( { plugins: [insertion()], dropZoneClass: "hover", + draggingClass: "opacity", } ); diff --git a/tests/pages/insertion/mixed.vue b/tests/pages/insertion/mixed.vue index f4bea0fe..9c303129 100644 --- a/tests/pages/insertion/mixed.vue +++ b/tests/pages/insertion/mixed.vue @@ -3,10 +3,11 @@ import { useDragAndDrop } from "../../../src/vue/index"; import { insertion } from "../../../src/index"; const [parent, values] = useDragAndDrop( - ["Apple", "Banana", "Strawberry", "Pineapple", "Watermelon", "Grape"], + ["Banana", "Strawberry", "Pineapple", "Apple", "Grape"], { plugins: [insertion()], dropZoneClass: "hover", + dragPlaceholderClass: "placeholder", } ); @@ -25,7 +26,7 @@ const [parent, values] = useDragAndDrop( :key="value" :class="{ item: true, - large: value === 'Apple' || value === 'Grape', + large: value === 'Apple', }" >
    @@ -43,6 +44,9 @@ const [parent, values] = useDragAndDrop(