MobileFirstLLC/shortcuts-for-chrome

View on GitHub
src/menu/dragging.js

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * @class Dragging
 * @classdesc Makes child-nodes of some DOM Element draggable, using
 * native HTML drag and drop. This class has no dependencies and can be
 * applied to any collection of UI elements to make them draggable.
 *
 * @description Initializing a new `Draggable` specifies the parent of
 * the draggable content: its immediate children become draggable;
 * the id-attribute that uniquely identifies the children; and handlers
 * that are called each time after the draggable content is rendered,
 * and when element order changes due to dragging.
 *
 * !!! example "Create a new draggable"
 *     ```{ .js }
 *     new Draggable(
 *         // id attribute of child elements
 *         "data-drag-id",
 *         // parent container
 *         document.getElementById("drag-container"),
 *         // function to call after rendering elements
 *         onDragRenderCallback,
 *         // callback to handle drag action
 *         onOrderChangeCallback)
 *     ```
 *
 * @param {string} idAttribute - For each draggable element, this
 * attribute will provide its id, for example `id` when element is
 * expected to have such attribute e.g.
 * `<span id='label-1'>example</span>`.
 *
 * @param {Element} container - The first parent of all draggable
 * elements -> provide a DOM element reference. Typically, a `<div>` or
 * similar block-level element.
 *
 * @param {function} onElementRender - After drag events have been
 * attached, other remaining action handlers still need to be attached.
 * This callback function will allow initiator to bind additional events
 * to draggable elements.
 *
 * @param {function} onDragEndCallback - After drag is done, this
 * callback function notifies initiator that the item order within the
 * draggable area has changed.
 *
 */
export default class Dragging {

    constructor(idAttribute, container, onElementRender,
    onDragEndCallback) {
        this.idAttribute = idAttribute;
        this.container = container;
        this.onElementRender = onElementRender;
        this.onDragEndCallback = onDragEndCallback;
        this.notifyParent = this.notifyParent.bind(this);
        this.addDragHandlers = this.addDragHandlers.bind(this);
        [].forEach.call(this.container.childNodes, this.addDragHandlers);
    }

    /**
     * @private
     * @static
     * @memberOf Dragging
     * @description Get or set the element that is being actively
     * dragged.
     * @returns {Dragging|null}
     */
    static get dragSourceElement() {
        return this._activedragSrcEl;
    }

    static set dragSourceElement(value) {
        this._activedragSrcEl = value;
    }

    /**
     * @private
     * @static
     * @memberOf Dragging
     * @description Tell the initiating module that element order has
     * changed as a result of drag/drop. This method has some added
     * latency to make sure the DOM nodes have updated before this event
     * fires.
     */
    notifyParent() {
        let newOrder = [];

        setTimeout(() => {
            [].forEach.call(this.container.childNodes, (elem) => {
                newOrder.push(elem.getAttribute(this.idAttribute));
            });
            this.onDragEndCallback(newOrder);
        }, 1);
    }

    /**
     * @private
     * @static
     * @memberOf Dragging
     * @description Attach drag and drop events to some DOM element.
     * @param {Element} element - DOM element.
     */
    addDragHandlers(element) {
        element.setAttribute('draggable', 'true');
        element.addEventListener('dragstart', this.handleDragStart, false);
        element.addEventListener('dragover', this.handleDragOver, false);
        element.addEventListener('dragleave', this.handleDragLeave, false);
        element.addEventListener('dragend', this.handleDragEnd, false);
        element.addEventListener('drop', this.handleDrop, false);
        element.addEventListener('drop', this.notifyParent, false);
        this.onElementRender(element);
    }

    /**
     * @private
     * @static
     * @memberOf Dragging
     * @description The drop event is fired when an element or text
     * selection is dropped on a valid drop target. When this event
     * occurs, move the dragged element to new DOM location.
     * @param {Event} event - `drop` event.
     */
    handleDrop(event) {
        event.stopPropagation();
        Dragging.removeClasses(this);
        const dragSrc = Dragging.dragSourceElement;

        if (dragSrc !== this) {
            if (Dragging.isBefore(dragSrc, this)) {
                this.parentNode.insertBefore(dragSrc, this.nextSibling);
            } else {
                this.parentNode.insertBefore(dragSrc, this);
            }
        }
        return false;
    }

    /**
     * @private
     * @static
     * @memberOf Dragging
     * @description The dragstart event is fired when the user starts
     * dragging an element or text selection.
     * @param {Object} event - `dragstart` event.
     */
    handleDragStart(event) {
        Dragging.dragSourceElement = this;
        event.dataTransfer.effectAllowed = 'move';
        event.dataTransfer.setData('text/html', this.outerHTML);
    }

    /**
     * @private
     * @static
     * @memberOf Dragging
     * @description The dragover event is fired when an element or text
     * selection is being dragged over a valid drop target (every few
     * hundred milliseconds). The event is fired on the drop target(s).
     * @param {Event} event - `dragover` event.
     */
    handleDragOver(event) {
        event.preventDefault();
        event.dataTransfer.dropEffect = 'move';
        if (Dragging.dragSourceElement !== this) {
            let isBefore = Dragging.isBefore(Dragging.dragSourceElement, this);

            this.classList.add(isBefore ? 'after' : 'before');
        }
        return false;
    }

    /**
     * @private
     * @static
     * @memberOf Dragging
     * @description The `dragleave` event is fired when a dragged
     * element or text selection leaves a valid drop target.
     */
    handleDragLeave() {
        Dragging.removeClasses(this);
    }

    /**
     * @private
     * @static
     * @memberOf Dragging
     * @description The `dragend` event is fired when a drag operation
     * is being ended (by releasing a mouse button or hitting the escape
     * key).
     */
    handleDragEnd() {
        Dragging.removeClasses(this);
    }

    /**
     * @private
     * @static
     * @memberOf Dragging
     * @description Test if some node `a` exists before another `b` in
     * the DOM tree.
     * @param {Dragging} a - DOM Element.
     * @param {Dragging} b - DOM Element.
     * @returns {boolean} True if `a` exists before `b`.
     */
    static isBefore(a, b) {
        for (let cur = a; cur; cur = cur.previousSibling) {
            if (cur === b) return false;
        }
        return true;
    }

    /**
     * @private
     * @static
     * @memberOf Dragging
     * @description Remove classes that indicate active dragging.
     * @param {Dragging} element - DOM node for which classes will be
     * removed.
     */
    static removeClasses(element) {
        element.classList.remove('before');
        element.classList.remove('after');
    }
}