Source: utils/draganddrop.mjs

/**
 * OpenSeadragon paperjs overlay plugin based on paper.js
 * @version 0.4.13
 * 
 * Includes additional open source libraries which are subject to copyright notices
 * as indicated accompanying those segments of code.
 * 
 * Original code:
 * Copyright (c) 2022-2024, Thomas Pearce
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 * 
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * 
 * * Neither the name of osd-paperjs-annotation nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 */

/**
 * @class
 * @description Set up an element to let children be dragged and dropped to rearrange their order.
 * Styling and certain functionality depend on css properties (currently found in annotationui.css)
 * targed with class names prefixed with draganddrop-
 * 
 */
export class DragAndDrop{
    /**
     * 
     * @param {Object} options 
     * @param {HTMLElement} options.parent the parent element
     * @param {String} options.selector the selector to use
     * @param {HTMLElement} options.dropTarget the element to drop onto
     * @param {function} [options.onDrop] a callback to call when drop occurs
        
     }}
     */
    constructor(options){
        if(!options.parent){
            console.error('element is required');
            return;
        }
        if(!options.selector){
            console.error('selector is required');
            return;
        }
        if(!options.dropTarget){
            console.error('droptarget is required');
            return;
        }

        this.element = options.parent;
        this.selector = options.selector;
        this.droptarget = options.dropTarget;
        this.onDrop = options.onDrop;

        this.placeholder = null;
        this.dragging = null;

        this.refresh();


        this.droptarget.addEventListener('dragover',ev => {
            ev.preventDefault();
            return false;
        });
        this.droptarget.addEventListener('dragleave',ev=>{
            ev.preventDefault();
            return false;
        });
        this.droptarget.addEventListener('dragend',ev=>{
            if(!this.dragging){
                return;
            }
            ev.preventDefault();
            this.cleanupDropTarget();
            return false;
        });
        this.element.addEventListener('dragstart',ev=>{
            if(ev.target.matches(this.selector)){
                ev.stopPropagation();
                this.dragging = ev.target;
                this.placeholder = ev.target.cloneNode(true);
                this.placeholder.classList.add('draganddrop-placeholder');
                // this.dragging.parentNode.insertBefore(this.placeholder, this.dragging);
                ev.target.classList.add('draganddrop-dragging');
                this.setupDropTarget();
            }
        });
        this.element.addEventListener('dragend',ev=>{
            if(ev.target.matches(this.selector)){
                ev.stopPropagation();
                ev.target.classList.remove('draganddrop-dragging', 'draganddrop-hide');

                if(this.placeholder){
                    this.placeholder.remove();
                }
                this.placeholder = null;
                this.dragging = null;

                this.cleanupDropTarget();
            }
            
        });
        this.element.addEventListener('dragleave',ev=>{
            if(ev.target.matches(this.selector)){
                ev.stopPropagation();
            }
            
        });
        this.element.addEventListener('drop',ev=>{
            if(this.dragging && this.placeholder.parentNode == this.droptarget){
                ev.stopPropagation();
                this.placeholder.replaceWith(this.dragging);
                this.dragging = null;
                this.placeholder = null;

                if(this.onDrop){
                    this.onDrop();
                }
            }
        });
        this.element.addEventListener('dragover',ev=>{
            if(!this.dragging){
                return;
            }
            ev.preventDefault();
            this.dragging.classList.add('draganddrop-hide');
            if(ev.target.matches(this.selector)){
                ev.stopPropagation();
                
                let top=ev.target.getBoundingClientRect().top;
                let bottom=ev.target.getBoundingClientRect().bottom;
                if(this.placeholder == ev.target){
                    // console.log('returning');
                    return;
                }
                if(ev.clientY<(top+bottom)/2) {
                    ev.target.parentNode.insertBefore(this.placeholder, ev.target);
                }
                else {
                    ev.target.parentNode.insertBefore(this.placeholder, ev.target.nextSibling);
                }
            } else if(ev.target == this.element){
                this.droptarget.appendChild(this.placeholder);
                ev.stopPropagation();
            }
            
        });
    }

    /**
     * Add draggable attribute to children that match the selector passed in at creation
     */
    refresh(){
        this.element.querySelectorAll(this.selector).forEach(element => {
            element.setAttribute('draggable', true);
        })
    }

    setupDropTarget(){
        this.droptarget.classList.add('draganddrop-drop-target');
    }

    cleanupDropTarget(){
        this.droptarget.classList.remove('draganddrop-drop-target');
    }
}