app/assets/builds/katalyst/content.js in katalyst-content-2.1.2 vs app/assets/builds/katalyst/content.js in katalyst-content-2.1.3

- old
+ new

@@ -735,106 +735,164 @@ this.dispatch("reindex", { bubbles: true, prefix: "content" }); } } class ListController extends Controller { + connect() { + this.enterCount = 0; + } + + /** + * When the user starts a drag within the list, set the item's dataTransfer + * properties to indicate that it's being dragged and update its style. + * + * We delay setting the dataset property until the next animation frame + * so that the style updates can be computed before the drag begins. + * + * @param event {DragEvent} + */ dragstart(event) { if (this.element !== event.target.parentElement) return; const target = event.target; event.dataTransfer.effectAllowed = "move"; // update element style after drag has begun - setTimeout(() => (target.dataset.dragging = "")); + requestAnimationFrame(() => (target.dataset.dragging = "")); } + /** + * When the user drags an item over another item in the last, swap the + * dragging item with the item under the cursor. + * + * As a special case, if the item is dragged over placeholder space at the end + * of the list, move the item to the bottom of the list instead. This allows + * users to hit the list element more easily when adding new items to an empty + * list. + * + * @param event {DragEvent} + */ dragover(event) { - const item = this.dragItem(); + const item = this.dragItem; if (!item) return; - swap(this.dropTarget(event.target), item); + swap(dropTarget(event.target), item); event.preventDefault(); return true; } + /** + * When the user drags an item into the list, create a placeholder item to + * represent the new item. Note that we can't access the drag data + * until drop, so we assume that this is our template item for now. + * + * Users can cancel the drag by dragging the item out of the list or by + * pressing escape. Both are handled by `cancelDrag`. + * + * @param event {DragEvent} + */ dragenter(event) { event.preventDefault(); - if (event.dataTransfer.effectAllowed === "copy" && !this.dragItem()) { + // Safari doesn't support relatedTarget, so we count enter/leave pairs + this.enterCount++; + + if (copyAllowed(event) && !this.dragItem) { const item = document.createElement("li"); item.dataset.dragging = ""; item.dataset.newItem = ""; - this.element.prepend(item); + this.element.appendChild(item); } } + /** + * When the user drags the item out of the list, remove the placeholder. + * This allows users to cancel the drag by dragging the item out of the list. + * + * @param event {DragEvent} + */ dragleave(event) { - const item = this.dragItem(); - const related = this.dropTarget(event.relatedTarget); + // Safari doesn't support relatedTarget, so we count enter/leave pairs + // https://bugs.webkit.org/show_bug.cgi?id=66547 + this.enterCount--; - // ignore if item is not set or we're moving into a valid drop target - if (!item || related) return; - - // remove item if it's a new item - if (item.dataset.hasOwnProperty("newItem")) { - item.remove(); + if (this.enterCount <= 0 && this.dragItem.dataset.hasOwnProperty("newItem")) { + this.cancelDrag(event); } } + /** + * When the user drops an item into the list, end the drag and reindex the list. + * + * If the item is a new item, we replace the placeholder with the template + * item data from the dataTransfer API. + * + * @param event {DragEvent} + */ drop(event) { - let item = this.dragItem(); + let item = this.dragItem; if (!item) return; event.preventDefault(); delete item.dataset.dragging; - swap(this.dropTarget(event.target), item); + swap(dropTarget(event.target), item); if (item.dataset.hasOwnProperty("newItem")) { const placeholder = item; const template = document.createElement("template"); template.innerHTML = event.dataTransfer.getData("text/html"); item = template.content.querySelector("li"); this.element.replaceChild(item, placeholder); - setTimeout(() => + requestAnimationFrame(() => item.querySelector("[role='button'][value='edit']").click() ); } - this.dispatch("drop", { target: item, bubbles: true, prefix: "content" }); + this.dispatch("drop", {target: item, bubbles: true, prefix: "content"}); } + /** + * End an in-progress drag. If the item is a new item, remove it, otherwise + * reset the item's style and restore its original position in the list. + */ dragend() { - const item = this.dragItem(); - if (!item) return; + const item = this.dragItem; - delete item.dataset.dragging; - this.reset(); + if (!item) ; else if (item.dataset.hasOwnProperty("newItem")) { + item.remove(); + } else { + delete item.dataset.dragging; + this.reset(); + } } - dragItem() { - return this.element.querySelector("[data-dragging]"); + get isDragging() { + return !!this.dragItem; } - dropTarget(e) { - return ( - e.closest("[data-controller='content--editor--list'] > *") || - e.closest("[data-controller='content--editor--list']") - ); + get dragItem() { + return this.element.querySelector("[data-dragging]"); } reindex() { - this.dispatch("reindex", { bubbles: true, prefix: "content" }); + this.dispatch("reindex", {bubbles: true, prefix: "content"}); } reset() { - this.dispatch("reset", { bubbles: true, prefix: "content" }); + this.dispatch("reset", {bubbles: true, prefix: "content"}); } } +/** + * Swaps two list items. If target is a list, the item is appended. + * + * @param target the target element to swap with + * @param item the item the user is dragging + */ function swap(target, item) { if (!target) return; if (target === item) return; if (target.nodeName === "LI") { @@ -847,9 +905,31 @@ } if (target.nodeName === "OL") { target.appendChild(item); } +} + +/** + * Returns true if the event supports copy or copy move. + * + * Chrome and Firefox use copy, but Safari only supports copyMove. + */ +function copyAllowed(event) { + return ( + event.dataTransfer.effectAllowed === "copy" || + event.dataTransfer.effectAllowed === "copyMove" + ); +} + +/** + * Given an event target, return the closest drop target, if any. + */ +function dropTarget(e) { + return e && ( + e.closest("[data-controller='content--editor--list'] > *") || + e.closest("[data-controller='content--editor--list']") + ); } class NewItemController extends Controller { static targets = ["template"];