Sfoglia il codice sorgente

refactor: temporarily use DraggableList locally with some WIP changes

Kristian Vos 1 mese fa
parent
commit
d0c9bb409d

+ 7 - 0
frontend/package-lock.json

@@ -17,6 +17,7 @@
         "dompurify": "^3.2.3",
         "eslint-config-airbnb-base": "^15.0.0",
         "marked": "^15.0.6",
+        "mobile-drag-drop": "^3.0.0-rc.0",
         "normalize.css": "^8.0.1",
         "pinia": "^2.3.0",
         "toasters": "^2.3.1",
@@ -5020,6 +5021,12 @@
       "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==",
       "license": "MIT"
     },
+    "node_modules/mobile-drag-drop": {
+      "version": "3.0.0-rc.0",
+      "resolved": "https://registry.npmjs.org/mobile-drag-drop/-/mobile-drag-drop-3.0.0-rc.0.tgz",
+      "integrity": "sha512-f8wIDTbBYLBW/+5sei1cqUE+StyDpf/LP+FRZELlVX6tmOOmELk84r3wh1z3woxCB9G5octhF06K5COvFjGgqg==",
+      "license": "MIT"
+    },
     "node_modules/ms": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

+ 1 - 0
frontend/package.json

@@ -48,6 +48,7 @@
     "dompurify": "^3.2.3",
     "eslint-config-airbnb-base": "^15.0.0",
     "marked": "^15.0.6",
+    "mobile-drag-drop": "^3.0.0-rc.0",
     "normalize.css": "^8.0.1",
     "pinia": "^2.3.0",
     "toasters": "^2.3.1",

+ 441 - 0
frontend/src/components/TempDraggableList.vue

@@ -0,0 +1,441 @@
+<script setup lang="ts">
+/* eslint-disable prettier/prettier */
+import { PropType, Slot as SlotType, watch, onMounted, ref } from "vue";
+</script>
+
+<script lang="ts">
+import { polyfill as mobileDragDropPolyfill } from "mobile-drag-drop";
+import { scrollBehaviourDragImageTranslateOverride as mobileDragDropScrollBehaviourDragImageTranslateOverride } from "mobile-drag-drop/scroll-behaviour";
+
+const props = defineProps({
+    itemKey: { type: String, default: "" },
+    list: { type: Array as PropType<any[]>, default: () => [] },
+    attributes: { type: Object, default: () => ({}) },
+    tag: { type: String, default: "div" },
+    group: { type: String, default: "" },
+    disabled: { type: [Boolean, Function], default: false },
+    touchTimeout: { type: Number, default: 250 },
+    handleClass: { type: String, required: false, default: null },
+    // New
+    readOnly: { type: Boolean, default: false },
+    unique: { type: Boolean, default: false },
+    debugName: { type: String, default: "" }
+});
+
+const listUuid = ref(
+    "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, symbol => {
+        let array;
+
+        if (symbol === "y") {
+            array = ["8", "9", "a", "b"];
+            return array[Math.floor(Math.random() * array.length)];
+        }
+
+        array = new Uint8Array(1);
+        window.crypto.getRandomValues(array);
+        return (array[0] % 16).toString(16);
+    })
+);
+const mounted = ref(false);
+const data = ref([] as any[]);
+const isDraggable = ref(!props.handleClass);
+
+const touching = ref(false);
+const touchDragging = ref(false);
+const touchingTimeout = ref<null | number>(null);
+
+watch(
+    () => props.list,
+    list => {
+        data.value = list;
+    }
+);
+
+onMounted(() => {
+    data.value = props.list;
+    mounted.value = true;
+});
+
+const emit = defineEmits(["update:list", "start", "end", "update"]);
+
+const hasHandleAtPosition = (event: MouseEvent | DragEvent) => {
+    const { clientX: x, clientY: y } = event;
+    const elementsAtPosition = document.elementsFromPoint(x, y);
+    return elementsAtPosition.reduce<boolean | null>(
+        (clickedHandle, elementAtPosition) => {
+            // If we already have a boolean result, return that
+            if (typeof clickedHandle === "boolean") return clickedHandle;
+            // If the clicked element (or one of its parents) has the handle class, we clicked the handle
+            if (elementAtPosition.classList.contains(props.handleClass!))
+                return true;
+            // If we've reached the draggable element itself, we found no handle, so return false to avoid
+            // accidentally using handles with the same class outside the draggable element
+            if (elementAtPosition.classList.contains("draggable-item"))
+                return false;
+            return null;
+        },
+        null
+    );
+};
+
+const onMouseDown = (itemIndex: number, event: MouseEvent) => {
+    if (!props.handleClass) return;
+
+    isDraggable.value = !!hasHandleAtPosition(event);
+};
+
+const onMouseUp = (itemIndex: number, event: MouseEvent) => {
+    if (props.handleClass) isDraggable.value = false;
+};
+
+const itemOnMove = (index: number, remove: boolean = false) => {
+    console.log("TDL itemOnMove", index, remove, props.debugName);
+    // debugger;
+    // Deletes the remove function for the dragging element
+    if (window.draggingItem && window.draggingItem.itemOnMove)
+        delete window.draggingItem.itemOnMove;
+
+    if (remove) {
+        // Remove the item from the current list and return it
+        const listItem = data.value.splice(index, 1)[0];
+        emit("update:list", data.value);
+        return listItem;
+    }
+
+    const listItem = data.value[index];
+    return listItem;
+};
+
+// When an element starts being dragged
+const onDragStart = (itemIndex: number, event: DragEvent) => {
+    const { draggable } = event.target as HTMLElement;
+
+    if (
+        props.disabled === true ||
+        (typeof props.disabled === "function" &&
+            props.disabled(data.value[itemIndex])) ||
+        !draggable ||
+        !event.dataTransfer ||
+        (touching.value && !touchDragging.value)
+    ) {
+        event.preventDefault();
+        return;
+    }
+
+    // If we only want to start dragging if the user clicked on a handle element
+    if (props.handleClass) {
+        // If no handle was clicked, we don't want to start dragging the element
+        if (!hasHandleAtPosition(event)) return;
+    }
+
+    // Set the effect of moving an element, which by default is clone. Not being used right now
+    event.dataTransfer.dropEffect = "move";
+
+    console.log("Set dragging item", props.debugName, props.readOnly);
+    // Sets the dragging element index, list uuid and adds a remove function for when this item is moved to a different list
+    window.draggingItem = {
+        itemIndex,
+        itemListUuid: listUuid.value,
+        itemGroup: props.group,
+        itemOnMove,
+        initialItemIndex: itemIndex,
+        initialItemListUuid: listUuid.value,
+        itemReadOnly: props.readOnly
+    };
+
+    // Emits the start event to the parent component, indicating that dragging has started
+    emit("start");
+};
+
+// When a dragging element hovers over another draggable element, this gets triggered, usually many times in a second
+// Push is for if the current list is empty
+const onDragOver = (itemIndex: number, event: DragEvent, push = false) => {
+    const getDraggableElement = (
+        element: HTMLElement
+    ): HTMLElement | undefined => {
+        if (
+            element.classList.contains("draggable-item") ||
+            element.classList.contains("empty-list-placeholder")
+        )
+            return element;
+        if (element.parentElement)
+            return getDraggableElement(element.parentElement);
+        return undefined;
+    };
+    const draggableElement = getDraggableElement(event.target as HTMLElement);
+
+    console.log("TDL onDragOver start ------------------------------------- ", props.debugName);
+
+    // console.log("TDL onDragOver 111", itemIndex, push, props, JSON.stringify(draggableElement.dataset), props.debugName);
+
+    // console.log("TDL onDragOver 222", props.disabled, props.readOnly, !draggableElement, (!draggableElement.draggable && !push), !window.draggingItem, window.draggingItem);
+
+    if (
+        props.disabled === true ||
+        !draggableElement ||
+        (!draggableElement.draggable && !push) ||
+        !window.draggingItem
+    )
+        return;
+
+    // console.log("TDL Update dragging item", props.debugName, props.readOnly);
+    // The index and list uuid of the item that is being dragged, stored in window since it can come from another list as well
+    const fromIndex = window.draggingItem.itemIndex;
+    const fromList = window.draggingItem.itemListUuid;
+    // The new index and list uuid of the item that is being dragged
+    const toIndex = itemIndex;
+    const toList = listUuid.value;
+
+    // console.log("TDL onDragOver 333");
+
+    // If the item hasn't changed position in the same list, don't continue
+    if (fromIndex === toIndex && fromList === toList) return;
+
+    // If the dragging item isn't from the same group, don't continue
+    if (
+        fromList !== toList &&
+        (props.group === "" || window.draggingItem.itemGroup !== props.group)
+    )
+        return;
+
+    // If dragging within the same list, but the list is readonly, don't continue
+    if (toList === fromList && props.readOnly)
+        return;
+
+    const oldListReadOnly = window.draggingItem.itemReadOnly; // TODO set readOnly
+    const newListReadOnly = props.readOnly;
+
+    if (!newListReadOnly) {
+        // Update the index and list uuid of the dragged item
+        window.draggingItem.itemIndex = toIndex;
+        window.draggingItem.itemListUuid = listUuid.value;
+        window.draggingItem.itemReadOnly = newListReadOnly;
+    }
+
+    console.log(`TDL onDragOver fromIndex ${fromIndex}, fromList ${fromList}, toIndex ${toIndex}, toList ${toList}, draggingItem.itemOnMove ${window.draggingItem.itemOnMove}`);
+
+    // If the item comes from another list
+    if (toList !== fromList && window.draggingItem.itemOnMove) {
+        // Call the remove function from the dragging element, which removes the item from the previous list and returns it
+        console.log("TDL onDragOver in difference. Remove from previous list:", !oldListReadOnly, "Add to new list:", !newListReadOnly);
+        const item = window.draggingItem.itemOnMove(fromIndex, !oldListReadOnly);
+        console.log(`TDL onDragOver in difference. Item: ${item}`);
+
+        if (newListReadOnly) {
+            // console.log("444");
+            console.log("TDL onDragOver new list read only return 111");
+            window.draggingItem.itemOnMove = () => {
+                console.log("TDL onDragOver fake itemOnMove 111");
+                return item;
+            }
+            return;
+        }
+
+        if (props.unique && data.value.includes(item)) {
+            console.log("TDL onDragOver unique return 222");
+            window.draggingItem.itemOnMove = () => {
+                console.log("TDL onDragOver fake itemOnMove 222");
+                return item;
+            }
+            return;
+        }
+
+        // Define a new remove function for the dragging element
+        window.draggingItem.itemOnMove = itemOnMove;
+        window.draggingItem.itemGroup = props.group;
+        // Add the item to the list at the new index
+        if (push) data.value.push(item);
+        else data.value.splice(toIndex, 0, item);
+        emit("update:list", data.value);
+    }
+    // If the item is being reordered in the same list
+    else {
+        console.log("TDL onDragOver in same list to a different position", itemIndex);
+        // if (props.readOnly) {
+        //     console.log("555");
+        //     return;
+        // }
+        // Remove the item from the old position, and add the item to the new position
+
+        data.value.splice(toIndex, 0, data.value.splice(fromIndex, 1)[0]);
+        emit("update:list", data.value);
+    }
+};
+// Gets called when the element that is being dragged is released
+const onDragEnd = () => {
+    // Emits the end event to parent component, indicating that dragging has ended
+    emit("end");
+
+    if (props.handleClass) isDraggable.value = false;
+
+    // Emits the update event to parent component, indicating that the order is now done and ordering/moving is done
+    if (!window.draggingItem) return;
+    const { itemIndex, itemListUuid, initialItemIndex, initialItemListUuid } =
+        window.draggingItem;
+    if (itemListUuid === initialItemListUuid) {
+        if (initialItemIndex === itemIndex) return;
+
+        emit("update", {
+            moved: {
+                oldIndex: initialItemIndex,
+                newIndex: itemIndex,
+                updatedList: data.value
+            }
+        });
+    } else emit("update", {});
+    delete window.draggingItem;
+};
+
+// Gets called when an element is dropped on another element, currently not used
+const onDrop = () => {};
+
+// Function that gets called for each item and returns attributes
+const convertAttributes = (item: any) =>
+    Object.fromEntries(
+        Object.entries(props.attributes).map(([key, value]) => [
+            key,
+            typeof value === "function" ? value(item) : value
+        ])
+    );
+
+const hasSlotContent = (slot: SlotType | undefined, slotProps = {}) => {
+    if (!slot) return false;
+
+    return slot(slotProps).some(vnode => {
+        if (
+            vnode.type === Comment ||
+            vnode.type.toString() === "Symbol(Comment)"
+        )
+            return false;
+
+        if (Array.isArray(vnode.children) && !vnode.children.length)
+            return false;
+
+        if (vnode.children === "") return false;
+
+        return (
+            vnode.type !== Text ||
+            vnode.type.toString() !== "Symbol(Text)" ||
+            (typeof vnode.children === "string" && vnode.children.trim() !== "")
+        );
+    });
+};
+
+const onTouchStart = (event: TouchEvent) => {
+    touching.value = true;
+    touchDragging.value = false;
+
+    // When we use handles, we need to find the element that is the handle, up until the draggable-item element itself
+    if (props.handleClass) {
+        let handleElement = event.target as HTMLElement | null;
+        while (
+            handleElement &&
+            !handleElement.classList.contains(props.handleClass)
+        ) {
+            if (handleElement.classList.contains("draggable-item")) {
+                handleElement = null;
+                break;
+            }
+            handleElement = handleElement.parentElement;
+        }
+        // If the user is touching the handle, set isDraggable to true so dragging is allowed to start in onDragStart
+        if (handleElement) isDraggable.value = true;
+    }
+
+    if (touchingTimeout.value) clearTimeout(touchingTimeout.value);
+
+    touchingTimeout.value = setTimeout(() => {
+        touchDragging.value = true;
+    }, props.touchTimeout);
+};
+
+const onTouchEnd = () => {
+    touching.value = false;
+    touchDragging.value = false;
+    // When we use handles, isDragging should default to false, so the user has to start dragging the handle for isDragging to be changed to true
+    if (props.handleClass) isDraggable.value = false;
+
+    if (touchingTimeout.value) clearTimeout(touchingTimeout.value);
+    touchingTimeout.value = null;
+};
+
+mobileDragDropPolyfill({
+    dragImageTranslateOverride:
+        mobileDragDropScrollBehaviourDragImageTranslateOverride,
+    tryFindDraggableTarget: event => {
+        const getDraggableElement = (
+            element: HTMLElement
+        ): HTMLElement | undefined => {
+            if (element.classList.contains("draggable-item")) return element;
+            if (element.parentElement)
+                return getDraggableElement(element.parentElement);
+            return undefined;
+        };
+
+        return getDraggableElement(event.target as HTMLElement);
+    }
+});
+
+window.addEventListener("touchmove", () => {});
+</script>
+
+<template>
+    <template v-for="(item, itemIndex) in data" :key="itemKey ? item[itemKey] : item">
+        <component
+            v-if="hasSlotContent($slots.item, { element: item })"
+            :is="tag"
+            :draggable="
+                (typeof disabled === 'function'
+                    ? !disabled(item)
+                    : !disabled) && isDraggable
+            "
+            @mousedown="onMouseDown(itemIndex, $event)"
+            @mouseup="onMouseUp(itemIndex, $event)"
+            @touchstart.passive="onTouchStart($event)"
+            @touchend="onTouchEnd()"
+            @dragstart="onDragStart(itemIndex, $event)"
+            @dragenter.prevent
+            @dragover.prevent="onDragOver(itemIndex, $event)"
+            @dragend="onDragEnd()"
+            @drop="onDrop()"
+            :data-index="itemIndex"
+            :data-list="listUuid"
+            class="draggable-item"
+            v-bind="convertAttributes(item)"
+        >
+            <slot name="item" :element="item" :index="itemIndex"></slot>
+        </component>
+    </template>
+    <div
+        v-if="data.length === 0"
+        class="empty-list-placeholder"
+        @dragover.prevent="onDragOver(0, $event, true)"
+        @drop.prevent
+    >
+        <slot name="empty-list-placeholder"></slot>
+    </div>
+    <!-- <div
+        v-else-if="data.length === 0"
+        class="empty-list-placeholder"
+        @dragover.prevent="onDragOver(0, $event, true)"
+        @drop.prevent
+    ></div> -->
+</template>
+
+<style lang="css">
+@import "mobile-drag-drop/default.css";
+
+.draggable-item[draggable="true"] {
+    cursor: move;
+}
+.draggable-item:not(:last-of-type) {
+    margin-bottom: 10px;
+}
+.draggable-item .draggable-handle {
+    cursor: move;
+    user-select: none;
+}
+.empty-list-placeholder {
+    flex: 1;
+}
+</style>

+ 2 - 1
frontend/src/types/global.d.ts

@@ -15,9 +15,10 @@ declare global {
 				itemIndex: number;
 				itemListUuid: string;
 				itemGroup: string;
-				itemOnMove?: (index: number) => any;
+				itemOnMove?: (index: number, remove: boolean) => any;
 				initialItemIndex: number;
 				initialItemListUuid: string;
+				itemReadOnly: boolean;
 		  };
 	var soundcloudIframeLockUuid: string;
 	var soundcloudIframeLockUuids: Set<string>;