diff --git a/scripts/draggable.js b/scripts/draggable.js index 05d9395..7b8507a 100644 --- a/scripts/draggable.js +++ b/scripts/draggable.js @@ -1,3 +1,26 @@ +// Map valid UUIDs used by draggable-item elements in order to better validate data transfers to ignore random data transfers. +const draggableItemUUIDs = {}; + +/** + * Validate a data transfer object through its data types. Valid draggable-item events have one type of the format `application/json/${uuid}`. + * The function takes the entire type list from event.dataTransfer.types and returns true if the dataTransfer object is likely to be valid. + * @param {*} formatList from event.dataTransfer.types + * @returns {Boolean} true if dataTransfer is valid (from a draggable-item source on this page), false otherwise + */ +function getDragSource (typesList) { + if (typesList.length !== 1) { + return null; + } + const typeString = typesList[0]; + const type = typeString.split("/"); + if (type.length === 3 && type[0] === "application" && type[1] === "json" && draggableItemUUIDs[type[2]]) { + return { type: typeString, uuid: type[2], element: draggableItemUUIDs[type[2]] }; + } + else { + return null; + } +} + class DraggableContainer extends HTMLElement { constructor () { super(); @@ -22,10 +45,14 @@ class DraggableContainer extends HTMLElement { } append (newNode) { + newNode.uuid = window.crypto.randomUUID(); + draggableItemUUIDs[newNode.uuid] = newNode; this.content.insertBefore(newNode, this.bottom); } insertBefore (newNode, referenceNode) { + newNode.uuid = window.crypto.randomUUID(); + draggableItemUUIDs[newNode.uuid] = newNode; this.content.insertBefore(newNode, referenceNode); } @@ -35,6 +62,7 @@ class DraggableContainer extends HTMLElement { removeChild (node) { if (node && this.content.contains(node)) { + draggableItemUUIDs[node.uuid] = null; this.content.removeChild(node); return true; } @@ -57,20 +85,25 @@ class DraggableContainer extends HTMLElement { } class DraggableItem extends HTMLElement { + uuid = null; constructor () { super(); this.attachShadow({ mode: "open" }); // for whatever reason, only grid layout seems to respect the parent's content bounds this.shadowRoot.innerHTML = ` +
@@ -80,8 +113,14 @@ class DraggableItem extends HTMLElement { this.addEventListener("dragstart", (event) => { this.content.style.opacity = "0.5"; const data = { id: this.id, data: this.data, content: this.content.innerHTML, value: this.value }; - event.dataTransfer.setData("application/json", JSON.stringify(data)); + event.dataTransfer.setData(`application/json/${this.uuid}`, JSON.stringify(data)); event.dataTransfer.effectAllowed = "move"; + const blank = document.createElement("img"); + event.dataTransfer.setDragImage(blank, 0, 0); + setTimeout(() => { + this.content.style.visibility = "hidden"; + this.content.style.height = "0"; + }, 0); }); this.addEventListener("dragend", (event) => { if (event.dataTransfer.dropEffect === "move") { @@ -92,13 +131,14 @@ class DraggableItem extends HTMLElement { } }); this.addEventListener("dragenter", (event) => { - if (event.target.dropTarget) { - event.target.dragOver = true; + const sourceElement = getDragSource(event.dataTransfer.types); + if (event.target.dropTarget && sourceElement) { + event.target.dragOver = sourceElement.element.innerHTML; } event.preventDefault(); }); this.addEventListener("dragleave", (event) => { - if (event.target.dragOver) { + if (event.target.dragOver && getDragSource(event.dataTransfer.types)) { event.target.dragOver = false; } event.preventDefault(); @@ -110,8 +150,9 @@ class DraggableItem extends HTMLElement { if (event.target.dragOver) { event.target.dragOver = false; } - if (event.target.dropTarget) { - const transfer = JSON.parse(event.dataTransfer.getData("application/json")); + const sourceElement = getDragSource(event.dataTransfer.types); + if (event.target.dropTarget && sourceElement) { + const transfer = JSON.parse(event.dataTransfer.getData(sourceElement.type)); const item = document.createElement("draggable-item"); item.data = transfer.data; item.innerHTML = transfer.content; @@ -119,6 +160,7 @@ class DraggableItem extends HTMLElement { item.dropTarget = true; item.id = transfer.id; item.value = transfer.data.value; + item.uuid = sourceElement.uuid; event.target.parentElement.insertBefore(item, event.target); } this.content.attributeStyleMap.clear(); @@ -154,13 +196,13 @@ class DraggableItem extends HTMLElement { set dragOver (dragOver) { if (dragOver) { this.classList.add("drag-over"); - // temp hover over effect - this.content.style.borderTop = "1px dotted"; + this.shadowRoot.querySelector("#drag-over").style.display = "block"; + this.shadowRoot.querySelector("#drag-over").innerHTML = dragOver; } else { this.classList.remove("drag-over"); - // temp hover over effect - this.content.style.borderTop = ""; + this.shadowRoot.querySelector("#drag-over").style.display = "none"; + this.shadowRoot.querySelector("#drag-over").innerHTML = ""; } } }