wikimedia/mediawiki-core

View on GitHub
resources/lib/jquery.ui/jquery.ui.droppable.js

Summary

Maintainability
F
3 days
Test Coverage
/*!
 * jQuery UI Droppable 1.9.2
 * http://jqueryui.com
 *
 * Copyright 2012 jQuery Foundation and other contributors
 * Released under the MIT license.
 * http://jquery.org/license
 *
 * http://api.jqueryui.com/droppable/
 *
 * Depends:
 *    jquery.ui.core.js
 *    jquery.ui.widget.js
 *    jquery.ui.mouse.js
 *    jquery.ui.draggable.js
 */
(function( $, undefined ) {

$.widget("ui.droppable", {
    version: "1.9.2",
    widgetEventPrefix: "drop",
    options: {
        accept: '*',
        activeClass: false,
        addClasses: true,
        greedy: false,
        hoverClass: false,
        scope: 'default',
        tolerance: 'intersect'
    },
    _create: function() {

        var o = this.options, accept = o.accept;
        this.isover = 0; this.isout = 1;

        this.accept = typeof accept === 'function' ? accept : function(d) {
            return d.is(accept);
        };

        //Store the droppable's proportions
        this.proportions = { width: this.element[0].offsetWidth, height: this.element[0].offsetHeight };

        // Add the reference and positions to the manager
        $.ui.ddmanager.droppables[o.scope] = $.ui.ddmanager.droppables[o.scope] || [];
        $.ui.ddmanager.droppables[o.scope].push(this);

        (o.addClasses && this.element.addClass("ui-droppable"));

    },

    _destroy: function() {
        var drop = $.ui.ddmanager.droppables[this.options.scope];
        for ( var i = 0; i < drop.length; i++ )
            if ( drop[i] == this )
                drop.splice(i, 1);

        this.element.removeClass("ui-droppable ui-droppable-disabled");
    },

    _setOption: function(key, value) {

        if(key == 'accept') {
            this.accept = typeof value === 'function' ? value : function(d) {
                return d.is(value);
            };
        }
        $.Widget.prototype._setOption.apply(this, arguments);
    },

    _activate: function(event) {
        var draggable = $.ui.ddmanager.current;
        if(this.options.activeClass) this.element.addClass(this.options.activeClass);
        (draggable && this._trigger('activate', event, this.ui(draggable)));
    },

    _deactivate: function(event) {
        var draggable = $.ui.ddmanager.current;
        if(this.options.activeClass) this.element.removeClass(this.options.activeClass);
        (draggable && this._trigger('deactivate', event, this.ui(draggable)));
    },

    _over: function(event) {

        var draggable = $.ui.ddmanager.current;
        if (!draggable || (draggable.currentItem || draggable.element)[0] == this.element[0]) return; // Bail if draggable and droppable are same element

        if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
            if(this.options.hoverClass) this.element.addClass(this.options.hoverClass);
            this._trigger('over', event, this.ui(draggable));
        }

    },

    _out: function(event) {

        var draggable = $.ui.ddmanager.current;
        if (!draggable || (draggable.currentItem || draggable.element)[0] == this.element[0]) return; // Bail if draggable and droppable are same element

        if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
            if(this.options.hoverClass) this.element.removeClass(this.options.hoverClass);
            this._trigger('out', event, this.ui(draggable));
        }

    },

    _drop: function(event,custom) {

        var draggable = custom || $.ui.ddmanager.current;
        if (!draggable || (draggable.currentItem || draggable.element)[0] == this.element[0]) return false; // Bail if draggable and droppable are same element

        var childrenIntersection = false;
        this.element.find(":data(droppable)").not(".ui-draggable-dragging").each(function() {
            var inst = $.data(this, 'droppable');
            if(
                inst.options.greedy
                && !inst.options.disabled
                && inst.options.scope == draggable.options.scope
                && inst.accept.call(inst.element[0], (draggable.currentItem || draggable.element))
                && $.ui.intersect(draggable, $.extend(inst, { offset: inst.element.offset() }), inst.options.tolerance)
            ) { childrenIntersection = true; return false; }
        });
        if(childrenIntersection) return false;

        if(this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
            if(this.options.activeClass) this.element.removeClass(this.options.activeClass);
            if(this.options.hoverClass) this.element.removeClass(this.options.hoverClass);
            this._trigger('drop', event, this.ui(draggable));
            return this.element;
        }

        return false;

    },

    ui: function(c) {
        return {
            draggable: (c.currentItem || c.element),
            helper: c.helper,
            position: c.position,
            offset: c.positionAbs
        };
    }

});

$.ui.intersect = function(draggable, droppable, toleranceMode) {

    if (!droppable.offset) return false;

    var x1 = (draggable.positionAbs || draggable.position.absolute).left, x2 = x1 + draggable.helperProportions.width,
        y1 = (draggable.positionAbs || draggable.position.absolute).top, y2 = y1 + draggable.helperProportions.height;
    var l = droppable.offset.left, r = l + droppable.proportions.width,
        t = droppable.offset.top, b = t + droppable.proportions.height;

    switch (toleranceMode) {
        case 'fit':
            return (l <= x1 && x2 <= r
                && t <= y1 && y2 <= b);
            break;
        case 'intersect':
            return (l < x1 + (draggable.helperProportions.width / 2) // Right Half
                && x2 - (draggable.helperProportions.width / 2) < r // Left Half
                && t < y1 + (draggable.helperProportions.height / 2) // Bottom Half
                && y2 - (draggable.helperProportions.height / 2) < b ); // Top Half
            break;
        case 'pointer':
            var draggableLeft = ((draggable.positionAbs || draggable.position.absolute).left + (draggable.clickOffset || draggable.offset.click).left),
                draggableTop = ((draggable.positionAbs || draggable.position.absolute).top + (draggable.clickOffset || draggable.offset.click).top),
                isOver = $.ui.isOver(draggableTop, draggableLeft, t, l, droppable.proportions.height, droppable.proportions.width);
            return isOver;
            break;
        case 'touch':
            return (
                    (y1 >= t && y1 <= b) ||    // Top edge touching
                    (y2 >= t && y2 <= b) ||    // Bottom edge touching
                    (y1 < t && y2 > b)        // Surrounded vertically
                ) && (
                    (x1 >= l && x1 <= r) ||    // Left edge touching
                    (x2 >= l && x2 <= r) ||    // Right edge touching
                    (x1 < l && x2 > r)        // Surrounded horizontally
                );
            break;
        default:
            return false;
            break;
        }

};

/*
    This manager tracks offsets of draggables and droppables
*/
$.ui.ddmanager = {
    current: null,
    droppables: { 'default': [] },
    prepareOffsets: function(t, event) {

        var m = $.ui.ddmanager.droppables[t.options.scope] || [];
        var type = event ? event.type : null; // workaround for #2317
        var list = (t.currentItem || t.element).find(":data(droppable)").addBack();

        droppablesLoop: for (var i = 0; i < m.length; i++) {

            if(m[i].options.disabled || (t && !m[i].accept.call(m[i].element[0],(t.currentItem || t.element)))) continue;    //No disabled and non-accepted
            for (var j=0; j < list.length; j++) { if(list[j] == m[i].element[0]) { m[i].proportions.height = 0; continue droppablesLoop; } }; //Filter out elements in the current dragged item
            m[i].visible = m[i].element.css("display") != "none"; if(!m[i].visible) continue;                                     //If the element is not visible, continue

            if(type == "mousedown") m[i]._activate.call(m[i], event); //Activate the droppable if used directly from draggables

            m[i].offset = m[i].element.offset();
            m[i].proportions = { width: m[i].element[0].offsetWidth, height: m[i].element[0].offsetHeight };

        }

    },
    drop: function(draggable, event) {

        var dropped = false;
        $.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() {

            if(!this.options) return;
            if (!this.options.disabled && this.visible && $.ui.intersect(draggable, this, this.options.tolerance))
                dropped = this._drop.call(this, event) || dropped;

            if (!this.options.disabled && this.visible && this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
                this.isout = 1; this.isover = 0;
                this._deactivate.call(this, event);
            }

        });
        return dropped;

    },
    dragStart: function( draggable, event ) {
        //Listen for scrolling so that if the dragging causes scrolling the position of the droppables can be recalculated (see #5003)
        draggable.element.parentsUntil( "body" ).bind( "scroll.droppable", function() {
            if( !draggable.options.refreshPositions ) $.ui.ddmanager.prepareOffsets( draggable, event );
        });
    },
    drag: function(draggable, event) {

        //If you have a highly dynamic page, you might try this option. It renders positions every time you move the mouse.
        if(draggable.options.refreshPositions) $.ui.ddmanager.prepareOffsets(draggable, event);

        //Run through all droppables and check their positions based on specific tolerance options
        $.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() {

            if(this.options.disabled || this.greedyChild || !this.visible) return;
            var intersects = $.ui.intersect(draggable, this, this.options.tolerance);

            var c = !intersects && this.isover == 1 ? 'isout' : (intersects && this.isover == 0 ? 'isover' : null);
            if(!c) return;

            var parentInstance;
            if (this.options.greedy) {
                // find droppable parents with same scope
                var scope = this.options.scope;
                var parent = this.element.parents(':data(droppable)').filter(function () {
                    return $.data(this, 'droppable').options.scope === scope;
                });

                if (parent.length) {
                    parentInstance = $.data(parent[0], 'droppable');
                    parentInstance.greedyChild = (c == 'isover' ? 1 : 0);
                }
            }

            // we just moved into a greedy child
            if (parentInstance && c == 'isover') {
                parentInstance['isover'] = 0;
                parentInstance['isout'] = 1;
                parentInstance._out.call(parentInstance, event);
            }

            this[c] = 1; this[c == 'isout' ? 'isover' : 'isout'] = 0;
            this[c == "isover" ? "_over" : "_out"].call(this, event);

            // we just moved out of a greedy child
            if (parentInstance && c == 'isout') {
                parentInstance['isout'] = 0;
                parentInstance['isover'] = 1;
                parentInstance._over.call(parentInstance, event);
            }
        });

    },
    dragStop: function( draggable, event ) {
        draggable.element.parentsUntil( "body" ).unbind( "scroll.droppable" );
        //Call prepareOffsets one final time since IE does not fire return scroll events when overflow was caused by drag (see #5003)
        if( !draggable.options.refreshPositions ) $.ui.ddmanager.prepareOffsets( draggable, event );
    }
};

})(jQuery);