[ Index ] |
WordPress Cross Reference |
[Summary view] [Print] [Text view]
1 /* global _wpMediaModelsL10n:false */ 2 window.wp = window.wp || {}; 3 4 (function($){ 5 var Attachment, Attachments, Query, compare, l10n, media; 6 7 /** 8 * wp.media( attributes ) 9 * 10 * Handles the default media experience. Automatically creates 11 * and opens a media frame, and returns the result. 12 * Does nothing if the controllers do not exist. 13 * 14 * @param {object} attributes The properties passed to the main media controller. 15 * @return {object} A media workflow. 16 */ 17 media = wp.media = function( attributes ) { 18 var MediaFrame = media.view.MediaFrame, 19 frame; 20 21 if ( ! MediaFrame ) 22 return; 23 24 attributes = _.defaults( attributes || {}, { 25 frame: 'select' 26 }); 27 28 if ( 'select' === attributes.frame && MediaFrame.Select ) 29 frame = new MediaFrame.Select( attributes ); 30 else if ( 'post' === attributes.frame && MediaFrame.Post ) 31 frame = new MediaFrame.Post( attributes ); 32 33 delete attributes.frame; 34 35 return frame; 36 }; 37 38 _.extend( media, { model: {}, view: {}, controller: {}, frames: {} }); 39 40 // Link any localized strings. 41 l10n = media.model.l10n = typeof _wpMediaModelsL10n === 'undefined' ? {} : _wpMediaModelsL10n; 42 43 // Link any settings. 44 media.model.settings = l10n.settings || {}; 45 delete l10n.settings; 46 47 /** 48 * ======================================================================== 49 * UTILITIES 50 * ======================================================================== 51 */ 52 53 /** 54 * A basic comparator. 55 * 56 * @param {mixed} a The primary parameter to compare. 57 * @param {mixed} b The primary parameter to compare. 58 * @param {string} ac The fallback parameter to compare, a's cid. 59 * @param {string} bc The fallback parameter to compare, b's cid. 60 * @return {number} -1: a should come before b. 61 * 0: a and b are of the same rank. 62 * 1: b should come before a. 63 */ 64 compare = function( a, b, ac, bc ) { 65 if ( _.isEqual( a, b ) ) 66 return ac === bc ? 0 : (ac > bc ? -1 : 1); 67 else 68 return a > b ? -1 : 1; 69 }; 70 71 _.extend( media, { 72 /** 73 * media.template( id ) 74 * 75 * Fetches a template by id. 76 * See wp.template() in `wp-includes/js/wp-util.js`. 77 */ 78 template: wp.template, 79 80 /** 81 * media.post( [action], [data] ) 82 * 83 * Sends a POST request to WordPress. 84 * See wp.ajax.post() in `wp-includes/js/wp-util.js`. 85 */ 86 post: wp.ajax.post, 87 88 /** 89 * media.ajax( [action], [options] ) 90 * 91 * Sends an XHR request to WordPress. 92 * See wp.ajax.send() in `wp-includes/js/wp-util.js`. 93 */ 94 ajax: wp.ajax.send, 95 96 // Scales a set of dimensions to fit within bounding dimensions. 97 fit: function( dimensions ) { 98 var width = dimensions.width, 99 height = dimensions.height, 100 maxWidth = dimensions.maxWidth, 101 maxHeight = dimensions.maxHeight, 102 constraint; 103 104 // Compare ratios between the two values to determine which 105 // max to constrain by. If a max value doesn't exist, then the 106 // opposite side is the constraint. 107 if ( ! _.isUndefined( maxWidth ) && ! _.isUndefined( maxHeight ) ) { 108 constraint = ( width / height > maxWidth / maxHeight ) ? 'width' : 'height'; 109 } else if ( _.isUndefined( maxHeight ) ) { 110 constraint = 'width'; 111 } else if ( _.isUndefined( maxWidth ) && height > maxHeight ) { 112 constraint = 'height'; 113 } 114 115 // If the value of the constrained side is larger than the max, 116 // then scale the values. Otherwise return the originals; they fit. 117 if ( 'width' === constraint && width > maxWidth ) { 118 return { 119 width : maxWidth, 120 height: Math.round( maxWidth * height / width ) 121 }; 122 } else if ( 'height' === constraint && height > maxHeight ) { 123 return { 124 width : Math.round( maxHeight * width / height ), 125 height: maxHeight 126 }; 127 } else { 128 return { 129 width : width, 130 height: height 131 }; 132 } 133 }, 134 135 // Truncates a string by injecting an ellipsis into the middle. 136 // Useful for filenames. 137 truncate: function( string, length, replacement ) { 138 length = length || 30; 139 replacement = replacement || '…'; 140 141 if ( string.length <= length ) 142 return string; 143 144 return string.substr( 0, length / 2 ) + replacement + string.substr( -1 * length / 2 ); 145 } 146 }); 147 148 149 /** 150 * ======================================================================== 151 * MODELS 152 * ======================================================================== 153 */ 154 155 /** 156 * wp.media.attachment 157 */ 158 media.attachment = function( id ) { 159 return Attachment.get( id ); 160 }; 161 162 /** 163 * wp.media.model.Attachment 164 */ 165 Attachment = media.model.Attachment = Backbone.Model.extend({ 166 sync: function( method, model, options ) { 167 // If the attachment does not yet have an `id`, return an instantly 168 // rejected promise. Otherwise, all of our requests will fail. 169 if ( _.isUndefined( this.id ) ) 170 return $.Deferred().rejectWith( this ).promise(); 171 172 // Overload the `read` request so Attachment.fetch() functions correctly. 173 if ( 'read' === method ) { 174 options = options || {}; 175 options.context = this; 176 options.data = _.extend( options.data || {}, { 177 action: 'get-attachment', 178 id: this.id 179 }); 180 return media.ajax( options ); 181 182 // Overload the `update` request so properties can be saved. 183 } else if ( 'update' === method ) { 184 // If we do not have the necessary nonce, fail immeditately. 185 if ( ! this.get('nonces') || ! this.get('nonces').update ) 186 return $.Deferred().rejectWith( this ).promise(); 187 188 options = options || {}; 189 options.context = this; 190 191 // Set the action and ID. 192 options.data = _.extend( options.data || {}, { 193 action: 'save-attachment', 194 id: this.id, 195 nonce: this.get('nonces').update, 196 post_id: media.model.settings.post.id 197 }); 198 199 // Record the values of the changed attributes. 200 if ( model.hasChanged() ) { 201 options.data.changes = {}; 202 203 _.each( model.changed, function( value, key ) { 204 options.data.changes[ key ] = this.get( key ); 205 }, this ); 206 } 207 208 return media.ajax( options ); 209 210 // Overload the `delete` request so attachments can be removed. 211 // This will permanently delete an attachment. 212 } else if ( 'delete' === method ) { 213 options = options || {}; 214 215 if ( ! options.wait ) 216 this.destroyed = true; 217 218 options.context = this; 219 options.data = _.extend( options.data || {}, { 220 action: 'delete-post', 221 id: this.id, 222 _wpnonce: this.get('nonces')['delete'] 223 }); 224 225 return media.ajax( options ).done( function() { 226 this.destroyed = true; 227 }).fail( function() { 228 this.destroyed = false; 229 }); 230 231 // Otherwise, fall back to `Backbone.sync()`. 232 } else { 233 return Backbone.Model.prototype.sync.apply( this, arguments ); 234 } 235 }, 236 237 parse: function( resp ) { 238 if ( ! resp ) 239 return resp; 240 241 // Convert date strings into Date objects. 242 resp.date = new Date( resp.date ); 243 resp.modified = new Date( resp.modified ); 244 return resp; 245 }, 246 247 saveCompat: function( data, options ) { 248 var model = this; 249 250 // If we do not have the necessary nonce, fail immeditately. 251 if ( ! this.get('nonces') || ! this.get('nonces').update ) 252 return $.Deferred().rejectWith( this ).promise(); 253 254 return media.post( 'save-attachment-compat', _.defaults({ 255 id: this.id, 256 nonce: this.get('nonces').update, 257 post_id: media.model.settings.post.id 258 }, data ) ).done( function( resp, status, xhr ) { 259 model.set( model.parse( resp, xhr ), options ); 260 }); 261 } 262 }, { 263 create: function( attrs ) { 264 return Attachments.all.push( attrs ); 265 }, 266 267 get: _.memoize( function( id, attachment ) { 268 return Attachments.all.push( attachment || { id: id } ); 269 }) 270 }); 271 272 /** 273 * wp.media.model.Attachments 274 */ 275 Attachments = media.model.Attachments = Backbone.Collection.extend({ 276 model: Attachment, 277 278 initialize: function( models, options ) { 279 options = options || {}; 280 281 this.props = new Backbone.Model(); 282 this.filters = options.filters || {}; 283 284 // Bind default `change` events to the `props` model. 285 this.props.on( 'change', this._changeFilteredProps, this ); 286 287 this.props.on( 'change:order', this._changeOrder, this ); 288 this.props.on( 'change:orderby', this._changeOrderby, this ); 289 this.props.on( 'change:query', this._changeQuery, this ); 290 291 // Set the `props` model and fill the default property values. 292 this.props.set( _.defaults( options.props || {} ) ); 293 294 // Observe another `Attachments` collection if one is provided. 295 if ( options.observe ) 296 this.observe( options.observe ); 297 }, 298 299 // Automatically sort the collection when the order changes. 300 _changeOrder: function() { 301 if ( this.comparator ) 302 this.sort(); 303 }, 304 305 // Set the default comparator only when the `orderby` property is set. 306 _changeOrderby: function( model, orderby ) { 307 // If a different comparator is defined, bail. 308 if ( this.comparator && this.comparator !== Attachments.comparator ) 309 return; 310 311 if ( orderby && 'post__in' !== orderby ) 312 this.comparator = Attachments.comparator; 313 else 314 delete this.comparator; 315 }, 316 317 // If the `query` property is set to true, query the server using 318 // the `props` values, and sync the results to this collection. 319 _changeQuery: function( model, query ) { 320 if ( query ) { 321 this.props.on( 'change', this._requery, this ); 322 this._requery(); 323 } else { 324 this.props.off( 'change', this._requery, this ); 325 } 326 }, 327 328 _changeFilteredProps: function( model ) { 329 // If this is a query, updating the collection will be handled by 330 // `this._requery()`. 331 if ( this.props.get('query') ) 332 return; 333 334 var changed = _.chain( model.changed ).map( function( t, prop ) { 335 var filter = Attachments.filters[ prop ], 336 term = model.get( prop ); 337 338 if ( ! filter ) 339 return; 340 341 if ( term && ! this.filters[ prop ] ) 342 this.filters[ prop ] = filter; 343 else if ( ! term && this.filters[ prop ] === filter ) 344 delete this.filters[ prop ]; 345 else 346 return; 347 348 // Record the change. 349 return true; 350 }, this ).any().value(); 351 352 if ( ! changed ) 353 return; 354 355 // If no `Attachments` model is provided to source the searches 356 // from, then automatically generate a source from the existing 357 // models. 358 if ( ! this._source ) 359 this._source = new Attachments( this.models ); 360 361 this.reset( this._source.filter( this.validator, this ) ); 362 }, 363 364 validateDestroyed: false, 365 366 validator: function( attachment ) { 367 if ( ! this.validateDestroyed && attachment.destroyed ) 368 return false; 369 return _.all( this.filters, function( filter ) { 370 return !! filter.call( this, attachment ); 371 }, this ); 372 }, 373 374 validate: function( attachment, options ) { 375 var valid = this.validator( attachment ), 376 hasAttachment = !! this.get( attachment.cid ); 377 378 if ( ! valid && hasAttachment ) 379 this.remove( attachment, options ); 380 else if ( valid && ! hasAttachment ) 381 this.add( attachment, options ); 382 383 return this; 384 }, 385 386 validateAll: function( attachments, options ) { 387 options = options || {}; 388 389 _.each( attachments.models, function( attachment ) { 390 this.validate( attachment, { silent: true }); 391 }, this ); 392 393 if ( ! options.silent ) 394 this.trigger( 'reset', this, options ); 395 396 return this; 397 }, 398 399 observe: function( attachments ) { 400 this.observers = this.observers || []; 401 this.observers.push( attachments ); 402 403 attachments.on( 'add change remove', this._validateHandler, this ); 404 attachments.on( 'reset', this._validateAllHandler, this ); 405 this.validateAll( attachments ); 406 return this; 407 }, 408 409 unobserve: function( attachments ) { 410 if ( attachments ) { 411 attachments.off( null, null, this ); 412 this.observers = _.without( this.observers, attachments ); 413 414 } else { 415 _.each( this.observers, function( attachments ) { 416 attachments.off( null, null, this ); 417 }, this ); 418 delete this.observers; 419 } 420 421 return this; 422 }, 423 424 _validateHandler: function( attachment, attachments, options ) { 425 // If we're not mirroring this `attachments` collection, 426 // only retain the `silent` option. 427 options = attachments === this.mirroring ? options : { 428 silent: options && options.silent 429 }; 430 431 return this.validate( attachment, options ); 432 }, 433 434 _validateAllHandler: function( attachments, options ) { 435 return this.validateAll( attachments, options ); 436 }, 437 438 mirror: function( attachments ) { 439 if ( this.mirroring && this.mirroring === attachments ) 440 return this; 441 442 this.unmirror(); 443 this.mirroring = attachments; 444 445 // Clear the collection silently. A `reset` event will be fired 446 // when `observe()` calls `validateAll()`. 447 this.reset( [], { silent: true } ); 448 this.observe( attachments ); 449 450 return this; 451 }, 452 453 unmirror: function() { 454 if ( ! this.mirroring ) 455 return; 456 457 this.unobserve( this.mirroring ); 458 delete this.mirroring; 459 }, 460 461 more: function( options ) { 462 var deferred = $.Deferred(), 463 mirroring = this.mirroring, 464 attachments = this; 465 466 if ( ! mirroring || ! mirroring.more ) 467 return deferred.resolveWith( this ).promise(); 468 469 // If we're mirroring another collection, forward `more` to 470 // the mirrored collection. Account for a race condition by 471 // checking if we're still mirroring that collection when 472 // the request resolves. 473 mirroring.more( options ).done( function() { 474 if ( this === attachments.mirroring ) 475 deferred.resolveWith( this ); 476 }); 477 478 return deferred.promise(); 479 }, 480 481 hasMore: function() { 482 return this.mirroring ? this.mirroring.hasMore() : false; 483 }, 484 485 parse: function( resp, xhr ) { 486 if ( ! _.isArray( resp ) ) 487 resp = [resp]; 488 489 return _.map( resp, function( attrs ) { 490 var id, attachment, newAttributes; 491 492 if ( attrs instanceof Backbone.Model ) { 493 id = attrs.get( 'id' ); 494 attrs = attrs.attributes; 495 } else { 496 id = attrs.id; 497 } 498 499 attachment = Attachment.get( id ); 500 newAttributes = attachment.parse( attrs, xhr ); 501 502 if ( ! _.isEqual( attachment.attributes, newAttributes ) ) 503 attachment.set( newAttributes ); 504 505 return attachment; 506 }); 507 }, 508 509 _requery: function() { 510 if ( this.props.get('query') ) 511 this.mirror( Query.get( this.props.toJSON() ) ); 512 }, 513 514 // If this collection is sorted by `menuOrder`, recalculates and saves 515 // the menu order to the database. 516 saveMenuOrder: function() { 517 if ( 'menuOrder' !== this.props.get('orderby') ) 518 return; 519 520 // Removes any uploading attachments, updates each attachment's 521 // menu order, and returns an object with an { id: menuOrder } 522 // mapping to pass to the request. 523 var attachments = this.chain().filter( function( attachment ) { 524 return ! _.isUndefined( attachment.id ); 525 }).map( function( attachment, index ) { 526 // Indices start at 1. 527 index = index + 1; 528 attachment.set( 'menuOrder', index ); 529 return [ attachment.id, index ]; 530 }).object().value(); 531 532 if ( _.isEmpty( attachments ) ) 533 return; 534 535 return media.post( 'save-attachment-order', { 536 nonce: media.model.settings.post.nonce, 537 post_id: media.model.settings.post.id, 538 attachments: attachments 539 }); 540 } 541 }, { 542 comparator: function( a, b, options ) { 543 var key = this.props.get('orderby'), 544 order = this.props.get('order') || 'DESC', 545 ac = a.cid, 546 bc = b.cid; 547 548 a = a.get( key ); 549 b = b.get( key ); 550 551 if ( 'date' === key || 'modified' === key ) { 552 a = a || new Date(); 553 b = b || new Date(); 554 } 555 556 // If `options.ties` is set, don't enforce the `cid` tiebreaker. 557 if ( options && options.ties ) 558 ac = bc = null; 559 560 return ( 'DESC' === order ) ? compare( a, b, ac, bc ) : compare( b, a, bc, ac ); 561 }, 562 563 filters: { 564 // Note that this client-side searching is *not* equivalent 565 // to our server-side searching. 566 search: function( attachment ) { 567 if ( ! this.props.get('search') ) 568 return true; 569 570 return _.any(['title','filename','description','caption','name'], function( key ) { 571 var value = attachment.get( key ); 572 return value && -1 !== value.search( this.props.get('search') ); 573 }, this ); 574 }, 575 576 type: function( attachment ) { 577 var type = this.props.get('type'); 578 return ! type || -1 !== type.indexOf( attachment.get('type') ); 579 }, 580 581 uploadedTo: function( attachment ) { 582 var uploadedTo = this.props.get('uploadedTo'); 583 if ( _.isUndefined( uploadedTo ) ) 584 return true; 585 586 return uploadedTo === attachment.get('uploadedTo'); 587 } 588 } 589 }); 590 591 Attachments.all = new Attachments(); 592 593 /** 594 * wp.media.query 595 */ 596 media.query = function( props ) { 597 return new Attachments( null, { 598 props: _.extend( _.defaults( props || {}, { orderby: 'date' } ), { query: true } ) 599 }); 600 }; 601 602 /** 603 * wp.media.model.Query 604 * 605 * A set of attachments that corresponds to a set of consecutively paged 606 * queries on the server. 607 * 608 * Note: Do NOT change this.args after the query has been initialized. 609 * Things will break. 610 */ 611 Query = media.model.Query = Attachments.extend({ 612 initialize: function( models, options ) { 613 var allowed; 614 615 options = options || {}; 616 Attachments.prototype.initialize.apply( this, arguments ); 617 618 this.args = options.args; 619 this._hasMore = true; 620 this.created = new Date(); 621 622 this.filters.order = function( attachment ) { 623 var orderby = this.props.get('orderby'), 624 order = this.props.get('order'); 625 626 if ( ! this.comparator ) 627 return true; 628 629 // We want any items that can be placed before the last 630 // item in the set. If we add any items after the last 631 // item, then we can't guarantee the set is complete. 632 if ( this.length ) { 633 return 1 !== this.comparator( attachment, this.last(), { ties: true }); 634 635 // Handle the case where there are no items yet and 636 // we're sorting for recent items. In that case, we want 637 // changes that occurred after we created the query. 638 } else if ( 'DESC' === order && ( 'date' === orderby || 'modified' === orderby ) ) { 639 return attachment.get( orderby ) >= this.created; 640 641 // If we're sorting by menu order and we have no items, 642 // accept any items that have the default menu order (0). 643 } else if ( 'ASC' === order && 'menuOrder' === orderby ) { 644 return attachment.get( orderby ) === 0; 645 } 646 647 // Otherwise, we don't want any items yet. 648 return false; 649 }; 650 651 // Observe the central `wp.Uploader.queue` collection to watch for 652 // new matches for the query. 653 // 654 // Only observe when a limited number of query args are set. There 655 // are no filters for other properties, so observing will result in 656 // false positives in those queries. 657 allowed = [ 's', 'order', 'orderby', 'posts_per_page', 'post_mime_type', 'post_parent' ]; 658 if ( wp.Uploader && _( this.args ).chain().keys().difference( allowed ).isEmpty().value() ) 659 this.observe( wp.Uploader.queue ); 660 }, 661 662 hasMore: function() { 663 return this._hasMore; 664 }, 665 666 more: function( options ) { 667 var query = this; 668 669 if ( this._more && 'pending' === this._more.state() ) 670 return this._more; 671 672 if ( ! this.hasMore() ) 673 return $.Deferred().resolveWith( this ).promise(); 674 675 options = options || {}; 676 options.remove = false; 677 678 return this._more = this.fetch( options ).done( function( resp ) { 679 if ( _.isEmpty( resp ) || -1 === this.args.posts_per_page || resp.length < this.args.posts_per_page ) 680 query._hasMore = false; 681 }); 682 }, 683 684 sync: function( method, model, options ) { 685 var args, fallback; 686 687 // Overload the read method so Attachment.fetch() functions correctly. 688 if ( 'read' === method ) { 689 options = options || {}; 690 options.context = this; 691 options.data = _.extend( options.data || {}, { 692 action: 'query-attachments', 693 post_id: media.model.settings.post.id 694 }); 695 696 // Clone the args so manipulation is non-destructive. 697 args = _.clone( this.args ); 698 699 // Determine which page to query. 700 if ( -1 !== args.posts_per_page ) 701 args.paged = Math.floor( this.length / args.posts_per_page ) + 1; 702 703 options.data.query = args; 704 return media.ajax( options ); 705 706 // Otherwise, fall back to Backbone.sync() 707 } else { 708 fallback = Attachments.prototype.sync ? Attachments.prototype : Backbone; 709 return fallback.sync.apply( this, arguments ); 710 } 711 } 712 }, { 713 defaultProps: { 714 orderby: 'date', 715 order: 'DESC' 716 }, 717 718 defaultArgs: { 719 posts_per_page: 40 720 }, 721 722 orderby: { 723 allowed: [ 'name', 'author', 'date', 'title', 'modified', 'uploadedTo', 'id', 'post__in', 'menuOrder' ], 724 valuemap: { 725 'id': 'ID', 726 'uploadedTo': 'parent', 727 'menuOrder': 'menu_order ID' 728 } 729 }, 730 731 propmap: { 732 'search': 's', 733 'type': 'post_mime_type', 734 'perPage': 'posts_per_page', 735 'menuOrder': 'menu_order', 736 'uploadedTo': 'post_parent' 737 }, 738 739 // Caches query objects so queries can be easily reused. 740 get: (function(){ 741 var queries = []; 742 743 return function( props, options ) { 744 var args = {}, 745 orderby = Query.orderby, 746 defaults = Query.defaultProps, 747 query; 748 749 // Remove the `query` property. This isn't linked to a query, 750 // this *is* the query. 751 delete props.query; 752 753 // Fill default args. 754 _.defaults( props, defaults ); 755 756 // Normalize the order. 757 props.order = props.order.toUpperCase(); 758 if ( 'DESC' !== props.order && 'ASC' !== props.order ) 759 props.order = defaults.order.toUpperCase(); 760 761 // Ensure we have a valid orderby value. 762 if ( ! _.contains( orderby.allowed, props.orderby ) ) 763 props.orderby = defaults.orderby; 764 765 // Generate the query `args` object. 766 // Correct any differing property names. 767 _.each( props, function( value, prop ) { 768 if ( _.isNull( value ) ) 769 return; 770 771 args[ Query.propmap[ prop ] || prop ] = value; 772 }); 773 774 // Fill any other default query args. 775 _.defaults( args, Query.defaultArgs ); 776 777 // `props.orderby` does not always map directly to `args.orderby`. 778 // Substitute exceptions specified in orderby.keymap. 779 args.orderby = orderby.valuemap[ props.orderby ] || props.orderby; 780 781 // Search the query cache for matches. 782 query = _.find( queries, function( query ) { 783 return _.isEqual( query.args, args ); 784 }); 785 786 // Otherwise, create a new query and add it to the cache. 787 if ( ! query ) { 788 query = new Query( [], _.extend( options || {}, { 789 props: props, 790 args: args 791 } ) ); 792 queries.push( query ); 793 } 794 795 return query; 796 }; 797 }()) 798 }); 799 800 /** 801 * wp.media.model.Selection 802 * 803 * Used to manage a selection of attachments in the views. 804 */ 805 media.model.Selection = Attachments.extend({ 806 initialize: function( models, options ) { 807 Attachments.prototype.initialize.apply( this, arguments ); 808 this.multiple = options && options.multiple; 809 810 // Refresh the `single` model whenever the selection changes. 811 // Binds `single` instead of using the context argument to ensure 812 // it receives no parameters. 813 this.on( 'add remove reset', _.bind( this.single, this, false ) ); 814 }, 815 816 // Override the selection's add method. 817 // If the workflow does not support multiple 818 // selected attachments, reset the selection. 819 add: function( models, options ) { 820 if ( ! this.multiple ) 821 this.remove( this.models ); 822 823 return Attachments.prototype.add.call( this, models, options ); 824 }, 825 826 single: function( model ) { 827 var previous = this._single; 828 829 // If a `model` is provided, use it as the single model. 830 if ( model ) 831 this._single = model; 832 833 // If the single model isn't in the selection, remove it. 834 if ( this._single && ! this.get( this._single.cid ) ) 835 delete this._single; 836 837 this._single = this._single || this.last(); 838 839 // If single has changed, fire an event. 840 if ( this._single !== previous ) { 841 if ( previous ) { 842 previous.trigger( 'selection:unsingle', previous, this ); 843 844 // If the model was already removed, trigger the collection 845 // event manually. 846 if ( ! this.get( previous.cid ) ) 847 this.trigger( 'selection:unsingle', previous, this ); 848 } 849 if ( this._single ) 850 this._single.trigger( 'selection:single', this._single, this ); 851 } 852 853 // Return the single model, or the last model as a fallback. 854 return this._single; 855 } 856 }); 857 858 // Clean up. Prevents mobile browsers caching 859 $(window).on('unload', function(){ 860 window.wp = null; 861 }); 862 863 }(jQuery));
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Tue Mar 25 01:41:18 2014 | WordPress honlapkészítés: online1.hu |