droppable.js (12817B)
1 /*! 2 * jQuery UI Droppable 1.12.1 3 * http://jqueryui.com 4 * 5 * Copyright jQuery Foundation and other contributors 6 * Released under the MIT license. 7 * http://jquery.org/license 8 */ 9 10 //>>label: Droppable 11 //>>group: Interactions 12 //>>description: Enables drop targets for draggable elements. 13 //>>docs: http://api.jqueryui.com/droppable/ 14 //>>demos: http://jqueryui.com/droppable/ 15 16 ( function( factory ) { 17 if ( typeof define === "function" && define.amd ) { 18 19 // AMD. Register as an anonymous module. 20 define( [ 21 "jquery", 22 "./draggable", 23 "./mouse", 24 "./core" 25 ], factory ); 26 } else { 27 28 // Browser globals 29 factory( jQuery ); 30 } 31 }( function( $ ) { 32 33 $.widget( "ui.droppable", { 34 version: "1.12.1", 35 widgetEventPrefix: "drop", 36 options: { 37 accept: "*", 38 addClasses: true, 39 greedy: false, 40 scope: "default", 41 tolerance: "intersect", 42 43 // Callbacks 44 activate: null, 45 deactivate: null, 46 drop: null, 47 out: null, 48 over: null 49 }, 50 _create: function() { 51 52 var proportions, 53 o = this.options, 54 accept = o.accept; 55 56 this.isover = false; 57 this.isout = true; 58 59 this.accept = $.isFunction( accept ) ? accept : function( d ) { 60 return d.is( accept ); 61 }; 62 63 this.proportions = function( /* valueToWrite */ ) { 64 if ( arguments.length ) { 65 66 // Store the droppable's proportions 67 proportions = arguments[ 0 ]; 68 } else { 69 70 // Retrieve or derive the droppable's proportions 71 return proportions ? 72 proportions : 73 proportions = { 74 width: this.element[ 0 ].offsetWidth, 75 height: this.element[ 0 ].offsetHeight 76 }; 77 } 78 }; 79 80 this._addToManager( o.scope ); 81 82 o.addClasses && this._addClass( "ui-droppable" ); 83 84 }, 85 86 _addToManager: function( scope ) { 87 88 // Add the reference and positions to the manager 89 $.ui.ddmanager.droppables[ scope ] = $.ui.ddmanager.droppables[ scope ] || []; 90 $.ui.ddmanager.droppables[ scope ].push( this ); 91 }, 92 93 _splice: function( drop ) { 94 var i = 0; 95 for ( ; i < drop.length; i++ ) { 96 if ( drop[ i ] === this ) { 97 drop.splice( i, 1 ); 98 } 99 } 100 }, 101 102 _destroy: function() { 103 var drop = $.ui.ddmanager.droppables[ this.options.scope ]; 104 105 this._splice( drop ); 106 }, 107 108 _setOption: function( key, value ) { 109 110 if ( key === "accept" ) { 111 this.accept = $.isFunction( value ) ? value : function( d ) { 112 return d.is( value ); 113 }; 114 } else if ( key === "scope" ) { 115 var drop = $.ui.ddmanager.droppables[ this.options.scope ]; 116 117 this._splice( drop ); 118 this._addToManager( value ); 119 } 120 121 this._super( key, value ); 122 }, 123 124 _activate: function( event ) { 125 var draggable = $.ui.ddmanager.current; 126 127 this._addActiveClass(); 128 if ( draggable ) { 129 this._trigger( "activate", event, this.ui( draggable ) ); 130 } 131 }, 132 133 _deactivate: function( event ) { 134 var draggable = $.ui.ddmanager.current; 135 136 this._removeActiveClass(); 137 if ( draggable ) { 138 this._trigger( "deactivate", event, this.ui( draggable ) ); 139 } 140 }, 141 142 _over: function( event ) { 143 144 var draggable = $.ui.ddmanager.current; 145 146 // Bail if draggable and droppable are same element 147 if ( !draggable || ( draggable.currentItem || 148 draggable.element )[ 0 ] === this.element[ 0 ] ) { 149 return; 150 } 151 152 if ( this.accept.call( this.element[ 0 ], ( draggable.currentItem || 153 draggable.element ) ) ) { 154 this._addHoverClass(); 155 this._trigger( "over", event, this.ui( draggable ) ); 156 } 157 158 }, 159 160 _out: function( event ) { 161 162 var draggable = $.ui.ddmanager.current; 163 164 // Bail if draggable and droppable are same element 165 if ( !draggable || ( draggable.currentItem || 166 draggable.element )[ 0 ] === this.element[ 0 ] ) { 167 return; 168 } 169 170 if ( this.accept.call( this.element[ 0 ], ( draggable.currentItem || 171 draggable.element ) ) ) { 172 this._removeHoverClass(); 173 this._trigger( "out", event, this.ui( draggable ) ); 174 } 175 176 }, 177 178 _drop: function( event, custom ) { 179 180 var draggable = custom || $.ui.ddmanager.current, 181 childrenIntersection = false; 182 183 // Bail if draggable and droppable are same element 184 if ( !draggable || ( draggable.currentItem || 185 draggable.element )[ 0 ] === this.element[ 0 ] ) { 186 return false; 187 } 188 189 this.element 190 .find( ":data(ui-droppable)" ) 191 .not( ".ui-draggable-dragging" ) 192 .each( function() { 193 var inst = $( this ).droppable( "instance" ); 194 if ( 195 inst.options.greedy && 196 !inst.options.disabled && 197 inst.options.scope === draggable.options.scope && 198 inst.accept.call( 199 inst.element[ 0 ], ( draggable.currentItem || draggable.element ) 200 ) && 201 intersect( 202 draggable, 203 $.extend( inst, { offset: inst.element.offset() } ), 204 inst.options.tolerance, event 205 ) 206 ) { 207 childrenIntersection = true; 208 return false; } 209 } ); 210 if ( childrenIntersection ) { 211 return false; 212 } 213 214 if ( this.accept.call( this.element[ 0 ], 215 ( draggable.currentItem || draggable.element ) ) ) { 216 this._removeActiveClass(); 217 this._removeHoverClass(); 218 219 this._trigger( "drop", event, this.ui( draggable ) ); 220 return this.element; 221 } 222 223 return false; 224 225 }, 226 227 ui: function( c ) { 228 return { 229 draggable: ( c.currentItem || c.element ), 230 helper: c.helper, 231 position: c.position, 232 offset: c.positionAbs 233 }; 234 }, 235 236 // Extension points just to make backcompat sane and avoid duplicating logic 237 // TODO: Remove in 1.13 along with call to it below 238 _addHoverClass: function() { 239 this._addClass( "ui-droppable-hover" ); 240 }, 241 242 _removeHoverClass: function() { 243 this._removeClass( "ui-droppable-hover" ); 244 }, 245 246 _addActiveClass: function() { 247 this._addClass( "ui-droppable-active" ); 248 }, 249 250 _removeActiveClass: function() { 251 this._removeClass( "ui-droppable-active" ); 252 } 253 } ); 254 255 var intersect = $.ui.intersect = ( function() { 256 function isOverAxis( x, reference, size ) { 257 return ( x >= reference ) && ( x < ( reference + size ) ); 258 } 259 260 return function( draggable, droppable, toleranceMode, event ) { 261 262 if ( !droppable.offset ) { 263 return false; 264 } 265 266 var x1 = ( draggable.positionAbs || 267 draggable.position.absolute ).left + draggable.margins.left, 268 y1 = ( draggable.positionAbs || 269 draggable.position.absolute ).top + draggable.margins.top, 270 x2 = x1 + draggable.helperProportions.width, 271 y2 = y1 + draggable.helperProportions.height, 272 l = droppable.offset.left, 273 t = droppable.offset.top, 274 r = l + droppable.proportions().width, 275 b = t + droppable.proportions().height; 276 277 switch ( toleranceMode ) { 278 case "fit": 279 return ( l <= x1 && x2 <= r && t <= y1 && y2 <= b ); 280 case "intersect": 281 return ( l < x1 + ( draggable.helperProportions.width / 2 ) && // Right Half 282 x2 - ( draggable.helperProportions.width / 2 ) < r && // Left Half 283 t < y1 + ( draggable.helperProportions.height / 2 ) && // Bottom Half 284 y2 - ( draggable.helperProportions.height / 2 ) < b ); // Top Half 285 case "pointer": 286 return isOverAxis( event.pageY, t, droppable.proportions().height ) && 287 isOverAxis( event.pageX, l, droppable.proportions().width ); 288 case "touch": 289 return ( 290 ( y1 >= t && y1 <= b ) || // Top edge touching 291 ( y2 >= t && y2 <= b ) || // Bottom edge touching 292 ( y1 < t && y2 > b ) // Surrounded vertically 293 ) && ( 294 ( x1 >= l && x1 <= r ) || // Left edge touching 295 ( x2 >= l && x2 <= r ) || // Right edge touching 296 ( x1 < l && x2 > r ) // Surrounded horizontally 297 ); 298 default: 299 return false; 300 } 301 }; 302 } )(); 303 304 /* 305 This manager tracks offsets of draggables and droppables 306 */ 307 $.ui.ddmanager = { 308 current: null, 309 droppables: { "default": [] }, 310 prepareOffsets: function( t, event ) { 311 312 var i, j, 313 m = $.ui.ddmanager.droppables[ t.options.scope ] || [], 314 type = event ? event.type : null, // workaround for #2317 315 list = ( t.currentItem || t.element ).find( ":data(ui-droppable)" ).addBack(); 316 317 droppablesLoop: for ( i = 0; i < m.length; i++ ) { 318 319 // No disabled and non-accepted 320 if ( m[ i ].options.disabled || ( t && !m[ i ].accept.call( m[ i ].element[ 0 ], 321 ( t.currentItem || t.element ) ) ) ) { 322 continue; 323 } 324 325 // Filter out elements in the current dragged item 326 for ( j = 0; j < list.length; j++ ) { 327 if ( list[ j ] === m[ i ].element[ 0 ] ) { 328 m[ i ].proportions().height = 0; 329 continue droppablesLoop; 330 } 331 } 332 333 m[ i ].visible = m[ i ].element.css( "display" ) !== "none"; 334 if ( !m[ i ].visible ) { 335 continue; 336 } 337 338 // Activate the droppable if used directly from draggables 339 if ( type === "mousedown" ) { 340 m[ i ]._activate.call( m[ i ], event ); 341 } 342 343 m[ i ].offset = m[ i ].element.offset(); 344 m[ i ].proportions( { 345 width: m[ i ].element[ 0 ].offsetWidth, 346 height: m[ i ].element[ 0 ].offsetHeight 347 } ); 348 349 } 350 351 }, 352 drop: function( draggable, event ) { 353 354 var dropped = false; 355 356 // Create a copy of the droppables in case the list changes during the drop (#9116) 357 $.each( ( $.ui.ddmanager.droppables[ draggable.options.scope ] || [] ).slice(), function() { 358 359 if ( !this.options ) { 360 return; 361 } 362 if ( !this.options.disabled && this.visible && 363 intersect( draggable, this, this.options.tolerance, event ) ) { 364 dropped = this._drop.call( this, event ) || dropped; 365 } 366 367 if ( !this.options.disabled && this.visible && this.accept.call( this.element[ 0 ], 368 ( draggable.currentItem || draggable.element ) ) ) { 369 this.isout = true; 370 this.isover = false; 371 this._deactivate.call( this, event ); 372 } 373 374 } ); 375 return dropped; 376 377 }, 378 dragStart: function( draggable, event ) { 379 380 // Listen for scrolling so that if the dragging causes scrolling the position of the 381 // droppables can be recalculated (see #5003) 382 draggable.element.parentsUntil( "body" ).on( "scroll.droppable", function() { 383 if ( !draggable.options.refreshPositions ) { 384 $.ui.ddmanager.prepareOffsets( draggable, event ); 385 } 386 } ); 387 }, 388 drag: function( draggable, event ) { 389 390 // If you have a highly dynamic page, you might try this option. It renders positions 391 // every time you move the mouse. 392 if ( draggable.options.refreshPositions ) { 393 $.ui.ddmanager.prepareOffsets( draggable, event ); 394 } 395 396 // Run through all droppables and check their positions based on specific tolerance options 397 $.each( $.ui.ddmanager.droppables[ draggable.options.scope ] || [], function() { 398 399 if ( this.options.disabled || this.greedyChild || !this.visible ) { 400 return; 401 } 402 403 var parentInstance, scope, parent, 404 intersects = intersect( draggable, this, this.options.tolerance, event ), 405 c = !intersects && this.isover ? 406 "isout" : 407 ( intersects && !this.isover ? "isover" : null ); 408 if ( !c ) { 409 return; 410 } 411 412 if ( this.options.greedy ) { 413 414 // find droppable parents with same scope 415 scope = this.options.scope; 416 parent = this.element.parents( ":data(ui-droppable)" ).filter( function() { 417 return $( this ).droppable( "instance" ).options.scope === scope; 418 } ); 419 420 if ( parent.length ) { 421 parentInstance = $( parent[ 0 ] ).droppable( "instance" ); 422 parentInstance.greedyChild = ( c === "isover" ); 423 } 424 } 425 426 // We just moved into a greedy child 427 if ( parentInstance && c === "isover" ) { 428 parentInstance.isover = false; 429 parentInstance.isout = true; 430 parentInstance._out.call( parentInstance, event ); 431 } 432 433 this[ c ] = true; 434 this[ c === "isout" ? "isover" : "isout" ] = false; 435 this[ c === "isover" ? "_over" : "_out" ].call( this, event ); 436 437 // We just moved out of a greedy child 438 if ( parentInstance && c === "isout" ) { 439 parentInstance.isout = false; 440 parentInstance.isover = true; 441 parentInstance._over.call( parentInstance, event ); 442 } 443 } ); 444 445 }, 446 dragStop: function( draggable, event ) { 447 draggable.element.parentsUntil( "body" ).off( "scroll.droppable" ); 448 449 // Call prepareOffsets one final time since IE does not fire return scroll events when 450 // overflow was caused by drag (see #5003) 451 if ( !draggable.options.refreshPositions ) { 452 $.ui.ddmanager.prepareOffsets( draggable, event ); 453 } 454 } 455 }; 456 457 // DEPRECATED 458 // TODO: switch return back to widget declaration at top of file when this is removed 459 if ( $.uiBackCompat !== false ) { 460 461 // Backcompat for activeClass and hoverClass options 462 $.widget( "ui.droppable", $.ui.droppable, { 463 options: { 464 hoverClass: false, 465 activeClass: false 466 }, 467 _addActiveClass: function() { 468 this._super(); 469 if ( this.options.activeClass ) { 470 this.element.addClass( this.options.activeClass ); 471 } 472 }, 473 _removeActiveClass: function() { 474 this._super(); 475 if ( this.options.activeClass ) { 476 this.element.removeClass( this.options.activeClass ); 477 } 478 }, 479 _addHoverClass: function() { 480 this._super(); 481 if ( this.options.hoverClass ) { 482 this.element.addClass( this.options.hoverClass ); 483 } 484 }, 485 _removeHoverClass: function() { 486 this._super(); 487 if ( this.options.hoverClass ) { 488 this.element.removeClass( this.options.hoverClass ); 489 } 490 } 491 } ); 492 } 493 494 return $.ui.droppable; 495 496 } ) );