Source: sortable.js

sortable.js

const Events = require('eventemitter3')

const defaults = require('./defaults')
const utils = require('./utils')

class Sortable extends Events
{
    /**
     * Create sortable list
     * @param {HTMLElement} element
     * @param {object} [options]
     * @param {string} [options.name=sortable] dragging is allowed between Sortables with the same name
     * @param {string} [options.dragClass] if set then drag only items with this className under element; otherwise drag all children
     * @param {string} [options.orderClass] use this class to include elements in ordering but not dragging; otherwise all children elements are included in when sorting and ordering
     * @param {boolean} [options.deepSearch] if dragClass and deepSearch then search all descendents of element for dragClass
     * @param {boolean} [options.sort=true] allow sorting within list
     * @param {boolean} [options.drop=true] allow drop from related sortables (doesn't impact reordering this sortable's children until the children are moved to a differen sortable)
     * @param {boolean} [options.copy=false] create copy when dragging an item (this disables sort=true for this sortable)
     * @param {string} [options.orderId=data-order] for ordered lists, use this data id to figure out sort order
     * @param {boolean} [options.orderIdIsNumber=true] use parseInt on options.sortId to properly sort numbers
     * @param {string} [options.reverseOrder] reverse sort the orderId
     * @param {string} [options.offList=closest] how to handle when an element is dropped outside a sortable: closest=drop in closest sortable; cancel=return to starting sortable; delete=remove from all sortables
     * @param {number} [options.maximum] maximum number of elements allowed in a sortable list
     * @param {boolean} [options.maximumFIFO] direction of search to choose which item to remove when maximum is reached
     * @param {string} [options.cursorHover=grab -webkit-grab pointer] use this cursor list to set cursor when hovering over a sortable element
     * @param {string} [options.cursorDown=grabbing -webkit-grabbing pointer] use this cursor list to set cursor when mousedown/touchdown over a sortable element
     * @param {boolean} [options.useIcons=true] show icons when dragging
     * @param {object} [options.icons] default set of icons
     * @param {string} [options.icons.reorder]
     * @param {string} [options.icons.move]
     * @param {string} [options.icons.copy]
     * @param {string} [options.icons.delete]
     * @param {string} [options.customIcon] source of custom image when over this sortable
     * @fires pickup
     * @fires order
     * @fires add
     * @fires remove
     * @fires update
     * @fires delete
     * @fires copy
     * @fires maximum-remove
     * @fires order-pending
     * @fires add-pending
     * @fires remove-pending
     * @fires add-remove-pending
     * @fires update-pending
     * @fires delete-pending
     * @fires copy-pending
     * @fires maximum-remove-pending
     * @fires clicked
     */
    constructor(element, options)
    {
        super()
        this.options = utils.options(options, defaults)
        this.element = element
        this._addToGlobalTracker()
        const elements = this._getChildren()
        this.events = {
            dragStart: (e) => this._dragStart(e),
            dragEnd: (e) => this._dragEnd(e),
            dragOver: (e) => this._dragOver(e),
            drop: (e) => this._drop(e),
            dragLeave: (e) => this._dragLeave(e),
            mouseDown: (e) => this._mouseDown(e),
            mouseUp: (e) => this._mouseUp(e)
        }
        for (let child of elements)
        {
            if (!this.options.dragClass || utils.containsClassName(child, this.options.dragClass))
            {
                this.attachElement(child)
            }
        }
        element.addEventListener('dragover', this.events.dragOver)
        element.addEventListener('drop', this.events.drop)
        element.addEventListener('dragleave', this.events.dragLeave)
        if (this.options.cursorHover)
        {
            for (let child of this._getChildren())
            {
                utils.style(child, 'cursor', this.options.cursorHover)
                if (this.options.cursorDown)
                {
                    child.addEventListener('mousedown', this.events.mouseDown)
                }
                child.addEventListener('mouseup', this.events.mouseUp)
            }
        }
    }

    /**
     * removes all event handlers from this.element and children
     */
    destroy()
    {
        this.element.removeEventListener('dragover', this.events.dragOver)
        this.element.removeEventListener('drop', this.events.drop)
        const elements = this._getChildren()
        for (let child of elements)
        {
            this.removeElement(child)
        }
        // todo: remove Sortable.tracker and related event handlers if no more sortables
    }

    /**
     * the global defaults for new Sortable objects
     * @type {DefaultOptions}
     */
    static get defaults()
    {
        return defaults
    }

    /**
     * create multiple sortable elements
     * @param {HTMLElements[]} elements
     * @param {object} options - see constructor for options
     */
    static create(elements, options)
    {
        const results = []
        for (let element of elements)
        {
            results.push(new Sortable(element, options))
        }
        return results
    }

    /**
     * add an element as a child of the sortable element; can also be used to swap between sortables
     * NOTE: this may not work with deepSearch non-ordered elements; use attachElement instead
     * @param {HTMLElement} element
     * @param {number} index
     */
    add(element, index)
    {
        this.attachElement(element)
        if (this.options.sort)
        {
            if (typeof index === 'undefined' || index >= this.element.children.length)
            {
                this.element.appendChild(element)
            }
            else
            {
                this.element.insertBefore(element, this.element.children[index + 1])
            }
        }
        else
        {
            const id = this.options.orderId
            let dragOrder = element.getAttribute(id)
            dragOrder = this.options.orderIdIsNumber ? parseFloat(dragOrder) : dragOrder
            let found
            const children = this._getChildren(true)
            if (this.options.reverseOrder)
            {
                for (let i = children.length - 1; i >= 0; i--)
                {
                    const child = children[i]
                    let childDragOrder = child.getAttribute(id)
                    childDragOrder = this.options.orderIsNumber ? parseFloat(childDragOrder) : childDragOrder
                    if (dragOrder > childDragOrder)
                    {
                        child.parentNode.insertBefore(element, child)
                        found = true
                        break
                    }
                }
            }
            else
            {
                for (let child of children)
                {
                    let childDragOrder = child.getAttribute(id)
                    childDragOrder = this.options.orderIsNumber ? parseFloat(childDragOrder) : childDragOrder
                    if (dragOrder < childDragOrder)
                    {
                        child.parentNode.insertBefore(element, child)
                        found = true
                        break
                    }
                }
            }
            if (!found)
            {
                this.element.appendChild(element)
            }
        }
    }

    /**
     * attaches an HTML element to the sortable; can also be used to swap between sortables
     * NOTE: you need to manually insert the element into this.element (this is useful when you have a deep structure)
     * @param {HTMLElement} element
     */
    attachElement(element)
    {
        if (element.__sortable)
        {
            element.__sortable.original = this
        }
        else
        {
            element.__sortable = {
                sortable: this,
                original: this
            }

            // add a counter for maximum
            this._maximumCounter(element, this)

            // ensure every element has an id
            if (!element.id)
            {
                element.id = '__sortable-' + this.options.name + '-' + Sortable.tracker[this.options.name].counter
                Sortable.tracker[this.options.name].counter++
            }
            if (this.options.copy)
            {
                element.__sortable.copy = 0
            }
            element.addEventListener('dragstart', this.events.dragStart)
            element.addEventListener('dragend', this.events.dragEnd)
            element.setAttribute('draggable', true)
        }
    }

    /**
     * removes all events from an HTML element
     * NOTE: does not remove the element from its parent
     * @param {HTMLElement} element
     */
    removeElement(element)
    {
        element.removeEventListener('dragstart', this.events.dragStart)
        element.removeEventListener('dragend', this.events.dragEnd)
        element.setAttribute('draggable', false)
    }

    /**
     * add sortable to global list that tracks all sortables
     * @private
     */
    _addToGlobalTracker()
    {
        if (!Sortable.tracker || !document.getElementById('sortable-dragImage'))
        {
            Sortable.dragImage = document.createElement('div')
            Sortable.dragImage.style.background = 'transparent'
            Sortable.dragImage.style.position = 'fixed'
            Sortable.dragImage.style.left = -10
            Sortable.dragImage.style.top = -10
            Sortable.dragImage.style.width = Sortable.dragImage.style.height = '5px'
            Sortable.dragImage.style.zIndex = -1
            Sortable.dragImage.id = 'sortable-dragImage'
            document.body.appendChild(Sortable.dragImage)
            Sortable.tracker = {}
            document.body.addEventListener('dragover', (e) => this._bodyDragOver(e))
            document.body.addEventListener('drop', (e) => this._bodyDrop(e))
        }
        if (Sortable.tracker[this.options.name])
        {
            Sortable.tracker[this.options.name].list.push(this)
        }
        else
        {
            Sortable.tracker[this.options.name] = { list: [this], counter: 0 }
        }
    }

    /**
     * default drag over for the body
     * @param {DragEvent} e
     * @private
     */
    _bodyDragOver(e)
    {
        const name = e.dataTransfer.types[0]
        if (Sortable.tracker[name])
        {
            const id = e.dataTransfer.types[1]
            const element = document.getElementById(id)
            const sortable = this._findClosest(e, Sortable.tracker[name].list, element)
            if (element)
            {
                if (sortable)
                {
                    if (sortable.last && Math.abs(sortable.last.x - e.pageX) < sortable.options.threshold && Math.abs(sortable.last.y - e.pageY) < sortable.options.threshold)
                    {
                        sortable._updateDragging(e, element)
                        e.preventDefault()
                        e.stopPropagation()
                        return
                    }
                    sortable.last = { x: e.pageX, y: e.pageY }
                    sortable._placeInList(sortable, e.pageX, e.pageY, element)
                    e.dataTransfer.dropEffect = 'move'
                    sortable._updateDragging(e, element)
                }
                else
                {
                    this._noDrop(e)
                }
                e.preventDefault()
            }
        }
    }

    /**
     * handle no drop
     * @param {UIEvent} e
     * @param {boolean} [cancel] force cancel (for options.copy)
     * @private
     */
    _noDrop(e, cancel)
    {
        e.dataTransfer.dropEffect = 'move'
        const id = e.dataTransfer.types[1]
        const element = document.getElementById(id)
        if (element)
        {
            this._updateDragging(e, element)
            this._setIcon(element, null, cancel)
            if (!cancel)
            {
                if (element.__sortable.original.options.offList === 'delete')
                {
                    if (!element.__sortable.display)
                    {
                        element.__sortable.display = element.style.display || 'unset'
                        element.style.display = 'none'
                        element.__sortable.original.emit('delete-pending', element, element.__sortable.original)
                    }
                }
                else if (!element.__sortable.original.options.copy)
                {
                    this._replaceInList(element.__sortable.original, element)
                }
            }
            if (element.__sortable.current)
            {
                this._clearMaximumPending(element.__sortable.current)
                element.__sortable.current.emit('add-remove-pending', element, element.__sortable.current)
                element.__sortable.current.emit('update-pending', element, element.__sortable.current)
                element.__sortable.current = null
            }
        }
    }

    /**
     * default drop for the body
     * @param {DragEvent} e
     * @private
     */
    _bodyDrop(e)
    {
        const name = e.dataTransfer.types[0]
        if (Sortable.tracker[name])
        {
            const id = e.dataTransfer.types[1]
            const element = document.getElementById(id)
            const sortable = this._findClosest(e, Sortable.tracker[name].list, element)
            if (element)
            {
                if (sortable)
                {
                    e.preventDefault()
                }
                this._removeDragging(element)
                if (element.__sortable.display)
                {
                    element.remove()
                    element.style.display = element.__sortable.display
                    element.__sortable.display = null
                    element.__sortable.original.emit('delete', element, element.__sortable.original)
                    element.__sortable.original.emit('update', element, element.__sortable.original)
                    element.__sortable.original = null
                }
            }
        }
    }

    /**
     * end drag
     * @param {UIEvent} e
     * @private
     */
    _dragEnd(e)
    {
        const element = e.currentTarget
        const dragging = element.__sortable.dragging
        if (dragging)
        {
            dragging.remove()
            if (dragging.icon)
            {
                dragging.icon.remove()
            }
            element.__sortable.dragging = null
        }
        if (this.options.cursorHover)
        {
            utils.style(e.currentTarget, 'cursor', this.options.cursorHover)
        }
    }

    /**
     * start drag
     * @param {UIEvent} e
     * @private
     */
    _dragStart(e)
    {
        const sortable = e.currentTarget.__sortable.original
        const dragging = e.currentTarget.cloneNode(true)
        for (let style in sortable.options.dragStyle)
        {
            dragging.style[style] = sortable.options.dragStyle[style]
        }
        const pos = utils.toGlobal(e.currentTarget)
        dragging.style.left = pos.x + 'px'
        dragging.style.top = pos.y + 'px'
        const offset = { x: pos.x - e.pageX, y: pos.y - e.pageY }
        document.body.appendChild(dragging)
        if (sortable.options.useIcons)
        {
            const image = new Image()
            image.src = sortable.options.icons.reorder
            image.style.position = 'absolute'
            image.style.transform = 'translate(-50%, -50%)'
            image.style.left = dragging.offsetLeft + dragging.offsetWidth + 'px'
            image.style.top = dragging.offsetTop + dragging.offsetHeight + 'px'
            document.body.appendChild(image)
            dragging.icon = image
        }
        if (sortable.options.cursorHover)
        {
            utils.style(e.currentTarget, 'cursor', sortable.options.cursorHover)
        }
        let target = e.currentTarget
        if (sortable.options.copy)
        {
            target = e.currentTarget.cloneNode(true)
            target.id = e.currentTarget.id + '-copy-' + e.currentTarget.__sortable.copy
            e.currentTarget.__sortable.copy++
            sortable.attachElement(target)
            target.__sortable.isCopy = true
            target.__sortable.original = this
            target.__sortable.display = target.style.display || 'unset'
            target.style.display = 'none'
            document.body.appendChild(target)
        }
        e.dataTransfer.clearData()
        e.dataTransfer.setData(sortable.options.name, sortable.options.name)
        e.dataTransfer.setData(target.id, target.id)
        e.dataTransfer.setDragImage(document.getElementById('sortable-dragImage'), 0, 0)
        target.__sortable.current = this
        target.__sortable.index = sortable.options.copy ? -1 : sortable._getIndex(target)
        target.__sortable.dragging = dragging
        target.__sortable.offset = offset
    }

    /**
     * handle drag leave events for sortable element
     * @param {DragEvent} e
     * @private
     */
    _dragLeave(e)
    {
        // const id = e.dataTransfer.types[1]
        // if (id)
        // {
        //     const element = document.getElementById(id)
        //     if (element)
        //     {
        //         const sortable = element.__sortable.current
        //         sortable._maximumPending(element, sortable)
        //     }
        // }
    }

    /**
     * handle drag over events for sortable element
     * @param {DragEvent} e
     * @private
     */
    _dragOver(e)
    {
        const sortable = e.dataTransfer.types[0]
        if (sortable && sortable === this.options.name)
        {
            const id = e.dataTransfer.types[1]
            const element = document.getElementById(id)
            if (this.last && Math.abs(this.last.x - e.pageX) < this.options.threshold && Math.abs(this.last.y - e.pageY) < this.options.threshold)
            {
                this._updateDragging(e, element)
                e.preventDefault()
                e.stopPropagation()
                return
            }
            this.last = { x: e.pageX, y: e.pageY }
            if (element.__sortable.isCopy && element.__sortable.original === this)
            {
                this._noDrop(e, true)
            }
            else if (this.options.drop || element.__sortable.original === this)
            {
                this._placeInList(this, e.pageX, e.pageY, element)
                e.dataTransfer.dropEffect = element.__sortable.isCopy ? 'copy' : 'move'
                this._updateDragging(e, element)
            }
            else
            {
                this._noDrop(e)
            }
            e.preventDefault()
            e.stopPropagation()
        }
    }

    /**
     * update the dragging element
     * @param {UIEvent} e
     * @param {HTMLElement} element
     * @private
     */
    _updateDragging(e, element)
    {
        const dragging = element.__sortable.dragging
        const offset = element.__sortable.offset
        if (dragging)
        {
            dragging.style.left = e.pageX + offset.x + 'px'
            dragging.style.top = e.pageY + offset.y + 'px'
            if (dragging.icon)
            {
                dragging.icon.style.left = dragging.offsetLeft + dragging.offsetWidth + 'px'
                dragging.icon.style.top = dragging.offsetTop + dragging.offsetHeight + 'px'
            }
        }
    }

    /**
     * remove the dragging element
     * @param {HTMLElement} element
     * @private
     */
    _removeDragging(element)
    {
        const dragging = element.__sortable.dragging
        if (dragging)
        {
            dragging.remove()
            if (dragging.icon)
            {
                dragging.icon.remove()
            }
            element.__sortable.dragging = null
        }
        element.__sortable.isCopy = false
    }

    /**
     * drop the element into a sortable
     * @param {HTMLElement} e
     * @private
     */
    _drop(e)
    {
        const name = e.dataTransfer.types[0]
        if (Sortable.tracker[name] && name === this.options.name)
        {
            const id = e.dataTransfer.types[1]
            const element = document.getElementById(id)
            if (element.__sortable.current)
            {
                if (element.__sortable.original !== this)
                {
                    element.__sortable.original.emit('remove', element, element.__sortable.original)
                    this.emit('add', element, this)
                    element.__sortable.original = this
                    if (this.options.sort)
                    {
                        this.emit('order', element, this)
                    }
                    if (element.__sortable.isCopy)
                    {
                        this.emit('copy', element, this)
                    }
                    this._maximum(element, this)
                    this.emit('update', element, this)
                }
                else
                {
                    if (element.__sortable.index !== this._getIndex(e.currentTarget))
                    {
                        this.emit('order', element, this)
                        this.emit('update', element, this)
                    }
                }
            }
            this._removeDragging(element)
            e.preventDefault()
            e.stopPropagation()
        }
    }

    /**
     * find closest Sortable to screen location
     * @param {UIEvent} e
     * @param {Sortable[]} list of related Sortables
     * @param {HTMLElement} element
     * @private
     */
    _findClosest(e, list, element)
    {
        let min = Infinity, found
        for (let related of list)
        {
            if ((!related.options.drop && element.__sortable.original !== related) ||
                (element.__sortable.isCopy && element.__sortable.original === related))
            {
                continue
            }
            if (utils.inside(e.pageX, e.pageY, related.element))
            {
                return related
            }
            else if (related.options.offList === 'closest')
            {
                const calculate = utils.distanceToClosestCorner(e.pageX, e.pageY, related.element)
                if (calculate < min)
                {
                    min = calculate
                    found = related
                }
            }
        }
        return found
    }

    /**
     * place indicator in the sortable list according to options.sort
     * @param {number} x
     * @param {number} y
     * @param {Sortable} sortable
     * @param {HTMLElement} element
     * @private
     */
    _placeInList(sortable, x, y, element)
    {
        if (this.options.sort)
        {
            this._placeInSortableList(sortable, x, y, element)
        }
        else
        {
            this._placeInOrderedList(sortable, element)
        }
        this._setIcon(element, sortable)
        if (element.__sortable.display)
        {
            element.style.display = element.__sortable.display === 'unset' ? '' : element.__sortable.display
            element.__sortable.display = null
        }
    }

    /**
     * replace item in list at original index position
     * @private
     */
    _replaceInList(sortable, element)
    {
        const children = sortable._getChildren()
        if (children.length)
        {
            const index = element.__sortable.index
            if (index < children.length)
            {
                children[index].parentNode.insertBefore(element, children[index])
            }
            else
            {
                children[0].appendChild(element)
            }
        }
        else
        {
            sortable.element.appendChild(element)
        }
    }

    /**
     * count the index of the child in the list of children
     * @param {HTMLElement} child
     * @return {number}
     * @private
     */
    _getIndex(child)
    {
        const children = this._getChildren()
        for (let i = 0; i < children.length; i++)
        {
            if (children[i] === child)
            {
                return i
            }
        }
    }

    /**
     * traverse and search descendents in DOM
     * @param {HTMLElement} base
     * @param {string} search
     * @param {HTMLElement[]} results to return
     * @private
     */
    _traverseChildren(base, search, results)
    {
        for (let child of base.children)
        {
            if (search.length)
            {
                if (search.indexOf(child.className) !== -1)
                {
                    results.push(child)
                }
            }
            else
            {
                results.push(child)
            }
            this._traverseChildren(child, search, results)
        }
    }

    /**
     * find children in div
     * @param {Sortable} sortable
     * @param {boolean} [order] search for dragOrder as well
     * @private
     */
    _getChildren(order)
    {
        if (this.options.deepSearch)
        {
            let search = []
            if (order && this.options.orderClass)
            {
                if (this.options.dragClass)
                {
                    search.push(this.options.dragClass)
                }
                if (order && this.options.orderClass)
                {
                    search.push(this.options.orderClass)
                }
            }
            else if (!order && this.options.dragClass)
            {
                search.push(this.options.dragClass)
            }
            const results = []
            this._traverseChildren(this.element, search, results)
            return results
        }
        else
        {
            if (this.options.dragClass)
            {
                let list = []
                for (let child of this.element.children)
                {
                    if (utils.containsClassName(child, this.options.dragClass) || (order && !this.options.orderClass || (order && this.options.orderClass && utils.containsClassName(child, this.options.orderClass))))
                    {
                        list.push(child)
                    }
                }
                return list
            }
            else
            {
                const list = []
                for (let child of this.element.children)
                {
                    list.push(child)
                }
                return list
            }
        }
    }

    /**
     * place indicator in an ordered list
     * @param {Sortable} sortable
     * @param {HTMLElement} dragging
     * @private
     */
    _placeInOrderedList(sortable, dragging)
    {
        if (dragging.__sortable.current !== sortable)
        {
            const id = sortable.options.orderId
            let dragOrder = dragging.getAttribute(id)
            dragOrder = sortable.options.orderIdIsNumber ? parseFloat(dragOrder) : dragOrder
            let found
            const children = sortable._getChildren(true)
            if (sortable.options.reverseOrder)
            {
                for (let i = children.length - 1; i >= 0; i--)
                {
                    const child = children[i]
                    let childDragOrder = child.getAttribute(id)
                    childDragOrder = sortable.options.orderIsNumber ? parseFloat(childDragOrder) : childDragOrder
                    if (dragOrder > childDragOrder)
                    {
                        child.parentNode.insertBefore(dragging, child)
                        found = true
                        break
                    }
                }
            }
            else
            {
                for (let child of children)
                {
                    let childDragOrder = child.getAttribute(id)
                    childDragOrder = sortable.options.orderIsNumber ? parseFloat(childDragOrder) : childDragOrder
                    if (dragOrder < childDragOrder)
                    {
                        child.parentNode.insertBefore(dragging, child)
                        found = true
                        break
                    }
                }
            }
            if (!found)
            {
                sortable.element.appendChild(dragging)
            }
            if (dragging.__sortable.current)
            {
                if (dragging.__sortable.current !== dragging.__sortable.original)
                {
                    dragging.__sortable.current.emit('add-remove-pending', dragging, dragging.__sortable.current)
                }
                else
                {
                    dragging.__sortable.current.emit('remove-pending', dragging, dragging.__sortable.current)
                }
                this._clearMaximumPending(dragging.__sortable.current)
                this._maximum(null, dragging.__sortable.current)
            }
            sortable.emit('add-pending', dragging, sortable)
            if (dragging.__sortable.isCopy)
            {
                sortable.emit('copy-pending', dragging, sortable)
            }
            dragging.__sortable.current = sortable
            this._maximumPending(dragging, sortable)
            sortable.emit('update-pending', dragging, sortable)
        }
    }

    /**
     * search for where to place using percentage
     * @param {Sortable} sortable
     * @param {HTMLElement} dragging
     * @returns {number} 0 = not found; 1 = nothing to do; 2 = moved
     * @private
     */
    _placeByPercentage(sortable, dragging)
    {
        const cursor = dragging.__sortable.dragging
        const xa1 = cursor.offsetLeft
        const ya1 = cursor.offsetTop
        const xa2 = cursor.offsetLeft + cursor.offsetWidth
        const ya2 = cursor.offsetTop + cursor.offsetHeight
        let largest = 0, closest, isBefore, indicator
        const element = sortable.element
        const elements = sortable._getChildren(true)
        for (let child of elements)
        {
            if (child === dragging)
            {
                indicator = true
            }
            const pos = utils.toGlobal(child)
            const xb1 = pos.x
            const yb1 = pos.y
            const xb2 = pos.x + child.offsetWidth
            const yb2 = pos.y + child.offsetHeight
            const percentage = utils.percentage(xa1, ya1, xa2, ya2, xb1, yb1, xb2, yb2)
            if (percentage > largest)
            {
                largest = percentage
                closest = child
                isBefore = indicator
            }
        }
        if (closest)
        {
            if (closest === dragging)
            {
                return 1
            }
            if (isBefore && closest.nextSibling)
            {
                element.insertBefore(dragging, closest.nextSibling)
                sortable.emit('order-pending', sortable)
            }
            else
            {
                element.insertBefore(dragging, closest)
                sortable.emit('order-pending', sortable)
            }
            return 2
        }
        else
        {
            return 0
        }
    }

    /**
     * search for where to place using distance
     * @param {Sortable} sortable
     * @param {HTMLElement} dragging
     * @param {number} x
     * @param {number} y
     * @return {boolean} false=nothing to do
     * @private
     */
    _placeByDistance(sortable, dragging, x, y)
    {
        if (utils.inside(x, y, dragging))
        {
            return true
        }
        let index = -1
        if (dragging.__sortable.current === sortable)
        {
            index = sortable._getIndex(dragging)
            sortable.element.appendChild(dragging)
        }
        let distance = Infinity, closest
        const element = sortable.element
        const elements = sortable._getChildren(true)
        for (let child of elements)
        {
            if (utils.inside(x, y, child))
            {
                closest = child
                break
            }
            else
            {
                const measure = utils.distanceToClosestCorner(x, y, child)
                if (measure < distance)
                {
                    closest = child
                    distance = measure
                }
            }
        }
        element.insertBefore(dragging, closest)
        if (index === sortable._getIndex(dragging))
        {
            return true
        }
        this._maximumPending(dragging, sortable)
        sortable.emit('order-pending', dragging, sortable)
    }

    /**
     * place indicator in an sortable list
     * @param {number} x
     * @param {number} y
     * @param {HTMLElement} dragging
     * @private
     */
    _placeInSortableList(sortable, x, y, dragging)
    {
        const element = sortable.element
        const children = sortable._getChildren()
        if (!children.length)
        {
            element.appendChild(dragging)
        }
        else
        {
            // const percentage = this._placeByPercentage(sortable, dragging)
            if (this._placeByDistance(sortable, dragging, x, y))
            {
                return
            }
        }
        if (dragging.__sortable.current !== sortable)
        {
            sortable.emit('add-pending', dragging, sortable)
            if (dragging.__sortable.isCopy)
            {
                sortable.emit('copy-pending', dragging, sortable)
            }
            if (dragging.__sortable.current)
            {
                if (dragging.__sortable.current !== dragging.__sortable.original)
                {
                    dragging.__sortable.current.emit('add-remove-pending', dragging, dragging.__sortable.current)
                }
                else
                {
                    dragging.__sortable.current.emit('remove-pending', dragging, dragging.__sortable.current)
                }
                this._clearMaximumPending(dragging.__sortable.current)
                this._maximum(null, dragging.__sortable.current)
            }
            dragging.__sortable.current = sortable
        }
        this._maximumPending(dragging, sortable)
        sortable.emit('update-pending', dragging, sortable)
    }

    /**
     * set icon if available
     * @param {HTMLElement} dragging
     * @param {Sortable} sortable
     * @param {boolean} [cancel] force cancel (for options.copy)
     * @private
     */
    _setIcon(element, sortable, cancel)
    {
        const dragging = element.__sortable.dragging
        if (dragging && dragging.icon)
        {
            if (!sortable)
            {
                sortable = element.__sortable.original
                if (cancel)
                {
                    dragging.icon.src = sortable.options.icons.cancel
                }
                else
                {
                    dragging.icon.src = sortable.options.offList === 'delete' ? sortable.options.icons.delete : sortable.options.icons.cancel
                }
            }
            else
            {
                if (element.__sortable.isCopy)
                {
                    dragging.icon.src = sortable.options.icons.copy
                }
                else
                {
                    dragging.icon.src = element.__sortable.original === sortable ? sortable.options.icons.reorder : sortable.options.icons.move
                }
            }
        }
    }

    /**
     * add a maximum counter to the element
     * @param {HTMLElement} element
     * @param {Sortable} sortable
     * @private
     */
    _maximumCounter(element, sortable)
    {
        let count = -1
        if (sortable.options.maximum)
        {
            const children = sortable._getChildren()
            for (let child of children)
            {
                if (child !== element && child.__sortable)
                {
                    count = child.__sortable.maximum > count ? child.__sortable.maximum : count
                }
            }
        }
        element.__sortable.maximum = count + 1
    }

    /**
     * handle maximum
     * @private
     */
    _maximum(element, sortable)
    {
        if (sortable.options.maximum)
        {
            const children = sortable._getChildren()
            if (children.length > sortable.options.maximum)
            {
                if (sortable.removePending)
                {
                    while (sortable.removePending.length)
                    {
                        const child = sortable.removePending.pop()
                        child.style.display = child.__sortable.display === 'unset' ? '' : child.__sortable.display
                        child.__sortable.display = null
                        child.remove()
                        sortable.emit('maximum-remove', child, sortable)
                    }
                    sortable.removePending = null
                }
            }
            if (element)
            {
                this._maximumCounter(element, sortable)
            }
        }
    }

    /**
     * clear pending list
     * @param {Sortable} sortable
     * @private
     */
    _clearMaximumPending(sortable)
    {
        if (sortable.removePending)
        {
            while (sortable.removePending.length)
            {
                const child = sortable.removePending.pop()
                child.style.display = child.__sortable.display === 'unset' ? '' : child.__sortable.display
                child.__sortable.display = null
            }
            sortable.removePending = null
        }
    }

    /**
     * handle pending maximum
     * @param {HTMLElement} element
     * @param {Sortable} sortable
     * @private
     */
    _maximumPending(element, sortable)
    {
        if (sortable.options.maximum)
        {
            const children = sortable._getChildren()
            if (children.length > sortable.options.maximum)
            {
                const savePending = sortable.removePending ? sortable.removePending.slice(0) : []
                this._clearMaximumPending(sortable)
                sortable.removePending = []
                let sort
                if (sortable.options.maximumFIFO)
                {
                    sort = children.sort((a, b) => { return a === element ? 1 : a.__sortable.maximum - b.__sortable.maximum })
                }
                else
                {
                    sort = children.sort((a, b) => { return a === element ? 1 : b.__sortable.maximum - a.__sortable.maximum })
                }
                for (let i = 0; i < children.length - sortable.options.maximum; i++)
                {
                    const hide = sort[i]
                    hide.__sortable.display = hide.style.display || 'unset'
                    hide.style.display = 'none'
                    sortable.removePending.push(hide)
                    if (savePending.indexOf(hide) === -1)
                    {
                        sortable.emit('maximum-remove-pending', hide, sortable)
                    }
                }
            }
        }
    }

    /**
     * change cursor during mousedown
     * @param {MouseEvent} e
     * @private
     */
    _mouseDown(e)
    {
        if (this.options.cursorHover)
        {
            utils.style(e.currentTarget, 'cursor', this.options.cursorDown)
        }
    }

    /**
     * change cursor during mouseup
     * @param {MouseEvent} e
     * @private
     */
    _mouseUp(e)
    {
        this.emit('clicked', e.currentTarget, this)
        if (this.options.cursorHover)
        {
            utils.style(e.currentTarget, 'cursor', this.options.cursorHover)
        }
    }
}

/**
 * fires when an element is picked up because it was moved beyond the options.threshold
 * @event Sortable#pickup
 * @property {HTMLElement} element being dragged
 * @property {Sortable} current sortable with element placeholder
 */

/**
 * fires when a sortable is reordered
 * @event Sortable#order
 * @property {HTMLElement} element that was reordered
 * @property {Sortable} sortable where element was placed
 */

/**
 * fires when an element is added to this sortable
 * @event Sortable#add
 * @property {HTMLElement} element added
 * @property {Sortable} sortable where element was added
 */

/**
 * fires when an element is removed from this sortable
 * @event Sortable#remove
 * @property {HTMLElement} element removed
 * @property {Sortable} sortable where element was removed
 */

/**
 * fires when an element is removed from all sortables
 * @event Sortable#delete
 * @property {HTMLElement} element removed
 * @property {Sortable} sortable where element was dragged from
 */

/**
 * fires when a copy of an element is dropped
 * @event Sortable#copy
 * @property {HTMLElement} element removed
 * @property {Sortable} sortable where element was dragged from
 */

/**
 * fires when the sortable is updated with an add, remove, or order change
 * @event Sortable#update
 * @property {HTMLElement} element changed
 * @property {Sortable} sortable with element
 */

/**
 * fires when an element is removed because maximum was reached for the sortable
 * @event Sortable#maximum-remove
 * @property {HTMLElement} element removed
 * @property {Sortable} sortable where element was dragged from
 */

/**
 * fires when order was changed but element was not dropped yet
 * @event Sortable#order-pending
 * @property {HTMLElement} element being dragged
 * @property {Sortable} current sortable with element placeholder
 */

/**
 * fires when element is added but not dropped yet
 * @event Sortable#add-pending
 * @property {HTMLElement} element being dragged
 * @property {Sortable} current sortable with element placeholder
 */

/**
 * fires when element is removed but not dropped yet
 * @event Sortable#remove-pending
 * @property {HTMLElement} element being dragged
 * @property {Sortable} current sortable with element placeholder
 */

/**
 * fires when element is removed after being temporarily added
 * @event Sortable#add-remove-pending
 * @property {HTMLElement} element being dragged
 * @property {Sortable} current sortable with element placeholder
 */

/**
 * fires when an element is about to be removed from all sortables
 * @event Sortable#delete-pending
 * @property {HTMLElement} element removed
 * @property {Sortable} sortable where element was dragged from
 */

/**
 * fires when an element is added, removed, or reorder but element has not dropped yet
 * @event Sortable#update-pending
 * @property {HTMLElement} element being dragged
 * @property {Sortable} current sortable with element placeholder
 */

/**
 * fires when a copy of an element is about to drop
 * @event Sortable#copy-pending
 * @property {HTMLElement} element removed
 * @property {Sortable} sortable where element was dragged from
 */

/**
 * fires when an element is about to be removed because maximum was reached for the sortable
 * @event Sortable#maximum-remove-pending
 * @property {HTMLElement} element removed
 * @property {Sortable} sortable where element was dragged from
 */

/**
 * fires when an element is clicked without dragging
 * @event Sortable#clicked
 * @property {HTMLElement} element clicked
 * @property {Sortable} sortable where element is a child
 */

module.exports = Sortable