/** * Provides the ability to create a Drop Target. * @module dd * @submodule dd-drop */ /** * Provides the ability to create a Drop Target. * @class Drop * @extends Base * @constructor * @namespace DD */ var NODE = 'node', DDM = Y.DD.DDM, OFFSET_HEIGHT = 'offsetHeight', OFFSET_WIDTH = 'offsetWidth', /** * Fires when a drag element is over this target. * @event drop:over * @param {EventFacade} event An Event Facade object with the following specific property added: * <dl> * <dt>drop</dt><dd>The drop object at the time of the event.</dd> * <dt>drag</dt><dd>The drag object at the time of the event.</dd> * </dl> * @bubbles DDM * @type {CustomEvent} */ EV_DROP_OVER = 'drop:over', /** * Fires when a drag element enters this target. * @event drop:enter * @param {EventFacade} event An Event Facade object with the following specific property added: * <dl> * <dt>drop</dt><dd>The drop object at the time of the event.</dd> * <dt>drag</dt><dd>The drag object at the time of the event.</dd> * </dl> * @bubbles DDM * @type {CustomEvent} */ EV_DROP_ENTER = 'drop:enter', /** * Fires when a drag element exits this target. * @event drop:exit * @param {EventFacade} event An Event Facade object * @bubbles DDM * @type {CustomEvent} */ EV_DROP_EXIT = 'drop:exit', /** * Fires when a draggable node is dropped on this Drop Target. (Fired from dd-ddm-drop) * @event drop:hit * @param {EventFacade} event An Event Facade object with the following specific property added: * <dl> * <dt>drop</dt><dd>The best guess on what was dropped on.</dd> * <dt>drag</dt><dd>The drag object at the time of the event.</dd> * <dt>others</dt><dd>An array of all the other drop targets that was dropped on.</dd> * </dl> * @bubbles DDM * @type {CustomEvent} */ Drop = function() { this._lazyAddAttrs = false; Drop.superclass.constructor.apply(this, arguments); //DD init speed up. Y.on('domready', Y.bind(function() { Y.later(100, this, this._createShim); }, this)); DDM._regTarget(this); /* TODO if (Dom.getStyle(this.el, 'position') == 'fixed') { Event.on(window, 'scroll', function() { this.activateShim(); }, this, true); } */ }; Drop.NAME = 'drop'; Drop.ATTRS = { /** * Y.Node instance to use as the element to make a Drop Target * @attribute node * @type Node */ node: { setter: function(node) { var n = Y.one(node); if (!n) { Y.error('DD.Drop: Invalid Node Given: ' + node); } return n; } }, /** * Array of groups to add this drop into. * @attribute groups * @type Array */ groups: { value: ['default'], getter: function() { if (!this._groups) { this._groups = {}; return []; } return Y.Object.keys(this._groups); }, setter: function(g) { this._groups = Y.Array.hash(g); return g; } }, /** * CSS style padding to make the Drop Target bigger than the node. * @attribute padding * @type String */ padding: { value: '0', setter: function(p) { return DDM.cssSizestoObject(p); } }, /** * Set to lock this drop element. * @attribute lock * @type Boolean */ lock: { value: false, setter: function(lock) { if (lock) { this.get(NODE).addClass(DDM.CSS_PREFIX + '-drop-locked'); } else { this.get(NODE).removeClass(DDM.CSS_PREFIX + '-drop-locked'); } return lock; } }, /** * Controls the default bubble parent for this Drop instance. Default: Y.DD.DDM. Set to false to disable bubbling. * Use bubbleTargets in config. * @deprecated * @attribute bubbles * @type Object */ bubbles: { setter: function(t) { Y.log('bubbles is deprecated use bubbleTargets: HOST', 'warn', 'dd'); this.addTarget(t); return t; } }, /** * Use the Drop shim. Default: true * @deprecated * @attribute useShim * @type Boolean */ useShim: { value: true, setter: function(v) { Y.DD.DDM._noShim = !v; return v; } } }; Y.extend(Drop, Y.Base, { /** * The default bubbleTarget for this object. Default: Y.DD.DDM * @private * @property _bubbleTargets */ _bubbleTargets: Y.DD.DDM, /** * Add this Drop instance to a group, this should be used for on-the-fly group additions. * @method addToGroup * @param {String} g The group to add this Drop Instance to. * @chainable */ addToGroup: function(g) { this._groups[g] = true; return this; }, /** * Remove this Drop instance from a group, this should be used for on-the-fly group removals. * @method removeFromGroup * @param {String} g The group to remove this Drop Instance from. * @chainable */ removeFromGroup: function(g) { delete this._groups[g]; return this; }, /** * This method creates all the events for this Event Target and publishes them so we get Event Bubbling. * @private * @method _createEvents */ _createEvents: function() { var ev = [ EV_DROP_OVER, EV_DROP_ENTER, EV_DROP_EXIT, 'drop:hit' ]; Y.Array.each(ev, function(v) { this.publish(v, { type: v, emitFacade: true, preventable: false, bubbles: true, queuable: false, prefix: 'drop' }); }, this); }, /** * Flag for determining if the target is valid in this operation. * @private * @property _valid * @type Boolean */ _valid: null, /** * The groups this target belongs to. * @private * @property _groups * @type Array */ _groups: null, /** * Node reference to the targets shim * @property shim * @type {Object} */ shim: null, /** * A region object associated with this target, used for checking regions while dragging. * @property region * @type Object */ region: null, /** * This flag is tripped when a drag element is over this target. * @property overTarget * @type Boolean */ overTarget: null, /** * Check if this target is in one of the supplied groups. * @method inGroup * @param {Array} groups The groups to check against * @return Boolean */ inGroup: function(groups) { this._valid = false; var ret = false; Y.Array.each(groups, function(v) { if (this._groups[v]) { ret = true; this._valid = true; } }, this); return ret; }, /** * Private lifecycle method * @private * @method initializer */ initializer: function() { Y.later(100, this, this._createEvents); var node = this.get(NODE), id; if (!node.get('id')) { id = Y.stamp(node); node.set('id', id); } node.addClass(DDM.CSS_PREFIX + '-drop'); //Shouldn't have to do this.. this.set('groups', this.get('groups')); }, /** * Lifecycle destructor, unreg the drag from the DDM and remove listeners * @private * @method destructor */ destructor: function() { DDM._unregTarget(this); if (this.shim && (this.shim !== this.get(NODE))) { this.shim.detachAll(); this.shim.remove(); this.shim = null; } this.get(NODE).removeClass(DDM.CSS_PREFIX + '-drop'); this.detachAll(); }, /** * Removes classes from the target, resets some flags and sets the shims deactive position [-999, -999] * @private * @method _deactivateShim */ _deactivateShim: function() { if (!this.shim) { return false; } this.get(NODE).removeClass(DDM.CSS_PREFIX + '-drop-active-valid'); this.get(NODE).removeClass(DDM.CSS_PREFIX + '-drop-active-invalid'); this.get(NODE).removeClass(DDM.CSS_PREFIX + '-drop-over'); if (this.get('useShim')) { this.shim.setStyles({ top: '-999px', left: '-999px', zIndex: '1' }); } this.overTarget = false; }, /** * Activates the shim and adds some interaction CSS classes * @private * @method _activateShim */ _activateShim: function() { if (!DDM.activeDrag) { return false; //Nothing is dragging, no reason to activate. } if (this.get(NODE) === DDM.activeDrag.get(NODE)) { return false; } if (this.get('lock')) { return false; } var node = this.get(NODE); //TODO Visibility Check.. //if (this.inGroup(DDM.activeDrag.get('groups')) && this.get(NODE).isVisible()) { if (this.inGroup(DDM.activeDrag.get('groups'))) { node.removeClass(DDM.CSS_PREFIX + '-drop-active-invalid'); node.addClass(DDM.CSS_PREFIX + '-drop-active-valid'); DDM._addValid(this); this.overTarget = false; if (!this.get('useShim')) { this.shim = this.get(NODE); } this.sizeShim(); } else { DDM._removeValid(this); node.removeClass(DDM.CSS_PREFIX + '-drop-active-valid'); node.addClass(DDM.CSS_PREFIX + '-drop-active-invalid'); } }, /** * Positions and sizes the shim with the raw data from the node, * this can be used to programatically adjust the Targets shim for Animation.. * @method sizeShim */ sizeShim: function() { if (!DDM.activeDrag) { return false; //Nothing is dragging, no reason to activate. } if (this.get(NODE) === DDM.activeDrag.get(NODE)) { return false; } //if (this.get('lock') || !this.get('useShim')) { if (this.get('lock')) { return false; } if (!this.shim) { Y.later(100, this, this.sizeShim); return false; } var node = this.get(NODE), nh = node.get(OFFSET_HEIGHT), nw = node.get(OFFSET_WIDTH), xy = node.getXY(), p = this.get('padding'), dd, dH, dW; //Apply padding nw = nw + p.left + p.right; nh = nh + p.top + p.bottom; xy[0] = xy[0] - p.left; xy[1] = xy[1] - p.top; if (DDM.activeDrag.get('dragMode') === DDM.INTERSECT) { //Intersect Mode, make the shim bigger dd = DDM.activeDrag; dH = dd.get(NODE).get(OFFSET_HEIGHT); dW = dd.get(NODE).get(OFFSET_WIDTH); nh = (nh + dH); nw = (nw + dW); xy[0] = xy[0] - (dW - dd.deltaXY[0]); xy[1] = xy[1] - (dH - dd.deltaXY[1]); } if (this.get('useShim')) { //Set the style on the shim this.shim.setStyles({ height: nh + 'px', width: nw + 'px', top: xy[1] + 'px', left: xy[0] + 'px' }); } //Create the region to be used by intersect when a drag node is over us. this.region = { '0': xy[0], '1': xy[1], area: 0, top: xy[1], right: xy[0] + nw, bottom: xy[1] + nh, left: xy[0] }; }, /** * Creates the Target shim and adds it to the DDM's playground.. * @private * @method _createShim */ _createShim: function() { //No playground, defer if (!DDM._pg) { Y.later(10, this, this._createShim); return; } //Shim already here, cancel if (this.shim) { return; } var s = this.get('node'); if (this.get('useShim')) { s = Y.Node.create('<div id="' + this.get(NODE).get('id') + '_shim"></div>'); s.setStyles({ height: this.get(NODE).get(OFFSET_HEIGHT) + 'px', width: this.get(NODE).get(OFFSET_WIDTH) + 'px', backgroundColor: 'yellow', opacity: '.5', zIndex: '1', overflow: 'hidden', top: '-900px', left: '-900px', position: 'absolute' }); DDM._pg.appendChild(s); s.on('mouseover', Y.bind(this._handleOverEvent, this)); s.on('mouseout', Y.bind(this._handleOutEvent, this)); } this.shim = s; }, /** * This handles the over target call made from this object or from the DDM * @private * @method _handleOverTarget */ _handleTargetOver: function() { if (DDM.isOverTarget(this)) { this.get(NODE).addClass(DDM.CSS_PREFIX + '-drop-over'); DDM.activeDrop = this; DDM.otherDrops[this] = this; if (this.overTarget) { DDM.activeDrag.fire('drag:over', { drop: this, drag: DDM.activeDrag }); this.fire(EV_DROP_OVER, { drop: this, drag: DDM.activeDrag }); } else { //Prevent an enter before a start.. if (DDM.activeDrag.get('dragging')) { this.overTarget = true; this.fire(EV_DROP_ENTER, { drop: this, drag: DDM.activeDrag }); DDM.activeDrag.fire('drag:enter', { drop: this, drag: DDM.activeDrag }); DDM.activeDrag.get(NODE).addClass(DDM.CSS_PREFIX + '-drag-over'); //TODO - Is this needed?? //DDM._handleTargetOver(); } } } else { this._handleOut(); } }, /** * Handles the mouseover DOM event on the Target Shim * @private * @method _handleOverEvent */ _handleOverEvent: function() { this.shim.setStyle('zIndex', '999'); DDM._addActiveShim(this); }, /** * Handles the mouseout DOM event on the Target Shim * @private * @method _handleOutEvent */ _handleOutEvent: function() { this.shim.setStyle('zIndex', '1'); DDM._removeActiveShim(this); }, /** * Handles out of target calls/checks * @private * @method _handleOut */ _handleOut: function(force) { if (!DDM.isOverTarget(this) || force) { if (this.overTarget) { this.overTarget = false; if (!force) { DDM._removeActiveShim(this); } if (DDM.activeDrag) { this.get(NODE).removeClass(DDM.CSS_PREFIX + '-drop-over'); DDM.activeDrag.get(NODE).removeClass(DDM.CSS_PREFIX + '-drag-over'); this.fire(EV_DROP_EXIT, { drop: this, drag: DDM.activeDrag }); DDM.activeDrag.fire('drag:exit', { drop: this, drag: DDM.activeDrag }); delete DDM.otherDrops[this]; } } } } }); Y.DD.Drop = Drop;