[ Index ] |
WordPress Cross Reference |
[Summary view] [Print] [Text view]
1 /* global _wpMediaViewsL10n, confirm, getUserSetting, setUserSetting */ 2 (function($){ 3 var media = wp.media, 4 Attachment = media.model.Attachment, 5 Attachments = media.model.Attachments, 6 l10n; 7 8 // Link any localized strings. 9 l10n = media.view.l10n = typeof _wpMediaViewsL10n === 'undefined' ? {} : _wpMediaViewsL10n; 10 11 // Link any settings. 12 media.view.settings = l10n.settings || {}; 13 delete l10n.settings; 14 15 // Copy the `post` setting over to the model settings. 16 media.model.settings.post = media.view.settings.post; 17 18 // Check if the browser supports CSS 3.0 transitions 19 $.support.transition = (function(){ 20 var style = document.documentElement.style, 21 transitions = { 22 WebkitTransition: 'webkitTransitionEnd', 23 MozTransition: 'transitionend', 24 OTransition: 'oTransitionEnd otransitionend', 25 transition: 'transitionend' 26 }, transition; 27 28 transition = _.find( _.keys( transitions ), function( transition ) { 29 return ! _.isUndefined( style[ transition ] ); 30 }); 31 32 return transition && { 33 end: transitions[ transition ] 34 }; 35 }()); 36 37 // Makes it easier to bind events using transitions. 38 media.transition = function( selector, sensitivity ) { 39 var deferred = $.Deferred(); 40 41 sensitivity = sensitivity || 2000; 42 43 if ( $.support.transition ) { 44 if ( ! (selector instanceof $) ) 45 selector = $( selector ); 46 47 // Resolve the deferred when the first element finishes animating. 48 selector.first().one( $.support.transition.end, deferred.resolve ); 49 50 // Just in case the event doesn't trigger, fire a callback. 51 _.delay( deferred.resolve, sensitivity ); 52 53 // Otherwise, execute on the spot. 54 } else { 55 deferred.resolve(); 56 } 57 58 return deferred.promise(); 59 }; 60 61 /** 62 * ======================================================================== 63 * CONTROLLERS 64 * ======================================================================== 65 */ 66 67 /** 68 * wp.media.controller.Region 69 */ 70 media.controller.Region = function( options ) { 71 _.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) ); 72 }; 73 74 // Use Backbone's self-propagating `extend` inheritance method. 75 media.controller.Region.extend = Backbone.Model.extend; 76 77 _.extend( media.controller.Region.prototype, { 78 mode: function( mode ) { 79 if ( ! mode ) 80 return this._mode; 81 82 // Bail if we're trying to change to the current mode. 83 if ( mode === this._mode ) 84 return this; 85 86 this.trigger('deactivate'); 87 this._mode = mode; 88 this.render( mode ); 89 this.trigger('activate'); 90 return this; 91 }, 92 93 render: function( mode ) { 94 // If no mode is provided, just re-render the current mode. 95 // If the provided mode isn't active, perform a full switch. 96 if ( mode && mode !== this._mode ) 97 return this.mode( mode ); 98 99 var set = { view: null }, 100 view; 101 102 this.trigger( 'create', set ); 103 view = set.view; 104 this.trigger( 'render', view ); 105 if ( view ) 106 this.set( view ); 107 return this; 108 }, 109 110 get: function() { 111 return this.view.views.first( this.selector ); 112 }, 113 114 set: function( views, options ) { 115 if ( options ) 116 options.add = false; 117 return this.view.views.set( this.selector, views, options ); 118 }, 119 120 trigger: function( event ) { 121 var base, args; 122 123 if ( ! this._mode ) 124 return; 125 126 args = _.toArray( arguments ); 127 base = this.id + ':' + event; 128 129 // Trigger `region:action:mode` event. 130 args[0] = base + ':' + this._mode; 131 this.view.trigger.apply( this.view, args ); 132 133 // Trigger `region:action` event. 134 args[0] = base; 135 this.view.trigger.apply( this.view, args ); 136 return this; 137 } 138 }); 139 140 /** 141 * wp.media.controller.StateMachine 142 */ 143 media.controller.StateMachine = function( states ) { 144 this.states = new Backbone.Collection( states ); 145 }; 146 147 // Use Backbone's self-propagating `extend` inheritance method. 148 media.controller.StateMachine.extend = Backbone.Model.extend; 149 150 // Add events to the `StateMachine`. 151 _.extend( media.controller.StateMachine.prototype, Backbone.Events, { 152 153 // Fetch a state. 154 // 155 // If no `id` is provided, returns the active state. 156 // 157 // Implicitly creates states. 158 state: function( id ) { 159 // Ensure that the `states` collection exists so the `StateMachine` 160 // can be used as a mixin. 161 this.states = this.states || new Backbone.Collection(); 162 163 // Default to the active state. 164 id = id || this._state; 165 166 if ( id && ! this.states.get( id ) ) 167 this.states.add({ id: id }); 168 return this.states.get( id ); 169 }, 170 171 // Sets the active state. 172 setState: function( id ) { 173 var previous = this.state(); 174 175 // Bail if we're trying to select the current state, if we haven't 176 // created the `states` collection, or are trying to select a state 177 // that does not exist. 178 if ( ( previous && id === previous.id ) || ! this.states || ! this.states.get( id ) ) 179 return this; 180 181 if ( previous ) { 182 previous.trigger('deactivate'); 183 this._lastState = previous.id; 184 } 185 186 this._state = id; 187 this.state().trigger('activate'); 188 189 return this; 190 }, 191 192 // Returns the previous active state. 193 // 194 // Call the `state()` method with no parameters to retrieve the current 195 // active state. 196 lastState: function() { 197 if ( this._lastState ) 198 return this.state( this._lastState ); 199 } 200 }); 201 202 // Map methods from the `states` collection to the `StateMachine` itself. 203 _.each([ 'on', 'off', 'trigger' ], function( method ) { 204 media.controller.StateMachine.prototype[ method ] = function() { 205 // Ensure that the `states` collection exists so the `StateMachine` 206 // can be used as a mixin. 207 this.states = this.states || new Backbone.Collection(); 208 // Forward the method to the `states` collection. 209 this.states[ method ].apply( this.states, arguments ); 210 return this; 211 }; 212 }); 213 214 215 // wp.media.controller.State 216 // --------------------------- 217 media.controller.State = Backbone.Model.extend({ 218 constructor: function() { 219 this.on( 'activate', this._preActivate, this ); 220 this.on( 'activate', this.activate, this ); 221 this.on( 'activate', this._postActivate, this ); 222 this.on( 'deactivate', this._deactivate, this ); 223 this.on( 'deactivate', this.deactivate, this ); 224 this.on( 'reset', this.reset, this ); 225 this.on( 'ready', this._ready, this ); 226 this.on( 'ready', this.ready, this ); 227 Backbone.Model.apply( this, arguments ); 228 this.on( 'change:menu', this._updateMenu, this ); 229 }, 230 231 ready: function() {}, 232 activate: function() {}, 233 deactivate: function() {}, 234 reset: function() {}, 235 236 _ready: function() { 237 this._updateMenu(); 238 }, 239 240 _preActivate: function() { 241 this.active = true; 242 }, 243 244 _postActivate: function() { 245 this.on( 'change:menu', this._menu, this ); 246 this.on( 'change:titleMode', this._title, this ); 247 this.on( 'change:content', this._content, this ); 248 this.on( 'change:toolbar', this._toolbar, this ); 249 250 this.frame.on( 'title:render:default', this._renderTitle, this ); 251 252 this._title(); 253 this._menu(); 254 this._toolbar(); 255 this._content(); 256 this._router(); 257 }, 258 259 260 _deactivate: function() { 261 this.active = false; 262 263 this.frame.off( 'title:render:default', this._renderTitle, this ); 264 265 this.off( 'change:menu', this._menu, this ); 266 this.off( 'change:titleMode', this._title, this ); 267 this.off( 'change:content', this._content, this ); 268 this.off( 'change:toolbar', this._toolbar, this ); 269 }, 270 271 _title: function() { 272 this.frame.title.render( this.get('titleMode') || 'default' ); 273 }, 274 275 _renderTitle: function( view ) { 276 view.$el.text( this.get('title') || '' ); 277 }, 278 279 _router: function() { 280 var router = this.frame.router, 281 mode = this.get('router'), 282 view; 283 284 this.frame.$el.toggleClass( 'hide-router', ! mode ); 285 if ( ! mode ) 286 return; 287 288 this.frame.router.render( mode ); 289 290 view = router.get(); 291 if ( view && view.select ) 292 view.select( this.frame.content.mode() ); 293 }, 294 295 _menu: function() { 296 var menu = this.frame.menu, 297 mode = this.get('menu'), 298 view; 299 300 if ( ! mode ) 301 return; 302 303 menu.mode( mode ); 304 305 view = menu.get(); 306 if ( view && view.select ) 307 view.select( this.id ); 308 }, 309 310 _updateMenu: function() { 311 var previous = this.previous('menu'), 312 menu = this.get('menu'); 313 314 if ( previous ) 315 this.frame.off( 'menu:render:' + previous, this._renderMenu, this ); 316 317 if ( menu ) 318 this.frame.on( 'menu:render:' + menu, this._renderMenu, this ); 319 }, 320 321 _renderMenu: function( view ) { 322 var menuItem = this.get('menuItem'), 323 title = this.get('title'), 324 priority = this.get('priority'); 325 326 if ( ! menuItem && title ) { 327 menuItem = { text: title }; 328 329 if ( priority ) 330 menuItem.priority = priority; 331 } 332 333 if ( ! menuItem ) 334 return; 335 336 view.set( this.id, menuItem ); 337 } 338 }); 339 340 _.each(['toolbar','content'], function( region ) { 341 media.controller.State.prototype[ '_' + region ] = function() { 342 var mode = this.get( region ); 343 if ( mode ) 344 this.frame[ region ].render( mode ); 345 }; 346 }); 347 348 // wp.media.controller.Library 349 // --------------------------- 350 media.controller.Library = media.controller.State.extend({ 351 defaults: { 352 id: 'library', 353 multiple: false, // false, 'add', 'reset' 354 describe: false, 355 toolbar: 'select', 356 sidebar: 'settings', 357 content: 'upload', 358 router: 'browse', 359 menu: 'default', 360 searchable: true, 361 filterable: false, 362 sortable: true, 363 title: l10n.mediaLibraryTitle, 364 365 // Uses a user setting to override the content mode. 366 contentUserSetting: true, 367 368 // Sync the selection from the last state when 'multiple' matches. 369 syncSelection: true 370 }, 371 372 initialize: function() { 373 var selection = this.get('selection'), 374 props; 375 376 // If a library isn't provided, query all media items. 377 if ( ! this.get('library') ) 378 this.set( 'library', media.query() ); 379 380 // If a selection instance isn't provided, create one. 381 if ( ! (selection instanceof media.model.Selection) ) { 382 props = selection; 383 384 if ( ! props ) { 385 props = this.get('library').props.toJSON(); 386 props = _.omit( props, 'orderby', 'query' ); 387 } 388 389 // If the `selection` attribute is set to an object, 390 // it will use those values as the selection instance's 391 // `props` model. Otherwise, it will copy the library's 392 // `props` model. 393 this.set( 'selection', new media.model.Selection( null, { 394 multiple: this.get('multiple'), 395 props: props 396 }) ); 397 } 398 399 if ( ! this.get('edge') ) 400 this.set( 'edge', 120 ); 401 402 if ( ! this.get('gutter') ) 403 this.set( 'gutter', 8 ); 404 405 this.resetDisplays(); 406 }, 407 408 activate: function() { 409 this.syncSelection(); 410 411 wp.Uploader.queue.on( 'add', this.uploading, this ); 412 413 this.get('selection').on( 'add remove reset', this.refreshContent, this ); 414 415 if ( this.get('contentUserSetting') ) { 416 this.frame.on( 'content:activate', this.saveContentMode, this ); 417 this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) ); 418 } 419 }, 420 421 deactivate: function() { 422 this.recordSelection(); 423 424 this.frame.off( 'content:activate', this.saveContentMode, this ); 425 426 // Unbind all event handlers that use this state as the context 427 // from the selection. 428 this.get('selection').off( null, null, this ); 429 430 wp.Uploader.queue.off( null, null, this ); 431 }, 432 433 reset: function() { 434 this.get('selection').reset(); 435 this.resetDisplays(); 436 this.refreshContent(); 437 }, 438 439 resetDisplays: function() { 440 var defaultProps = media.view.settings.defaultProps; 441 this._displays = []; 442 this._defaultDisplaySettings = { 443 align: defaultProps.align || getUserSetting( 'align', 'none' ), 444 size: defaultProps.size || getUserSetting( 'imgsize', 'medium' ), 445 link: defaultProps.link || getUserSetting( 'urlbutton', 'file' ) 446 }; 447 }, 448 449 display: function( attachment ) { 450 var displays = this._displays; 451 452 if ( ! displays[ attachment.cid ] ) 453 displays[ attachment.cid ] = new Backbone.Model( this.defaultDisplaySettings( attachment ) ); 454 455 return displays[ attachment.cid ]; 456 }, 457 458 defaultDisplaySettings: function( attachment ) { 459 var settings = this._defaultDisplaySettings; 460 if ( settings.canEmbed = this.canEmbed( attachment ) ) 461 settings.link = 'embed'; 462 return settings; 463 }, 464 465 canEmbed: function( attachment ) { 466 // If uploading, we know the filename but not the mime type. 467 if ( ! attachment.get('uploading') ) { 468 var type = attachment.get('type'); 469 if ( type !== 'audio' && type !== 'video' ) 470 return false; 471 } 472 473 return _.contains( media.view.settings.embedExts, attachment.get('filename').split('.').pop() ); 474 }, 475 476 syncSelection: function() { 477 var selection = this.get('selection'), 478 manager = this.frame._selection; 479 480 if ( ! this.get('syncSelection') || ! manager || ! selection ) 481 return; 482 483 // If the selection supports multiple items, validate the stored 484 // attachments based on the new selection's conditions. Record 485 // the attachments that are not included; we'll maintain a 486 // reference to those. Other attachments are considered in flux. 487 if ( selection.multiple ) { 488 selection.reset( [], { silent: true }); 489 selection.validateAll( manager.attachments ); 490 manager.difference = _.difference( manager.attachments.models, selection.models ); 491 } 492 493 // Sync the selection's single item with the master. 494 selection.single( manager.single ); 495 }, 496 497 recordSelection: function() { 498 var selection = this.get('selection'), 499 manager = this.frame._selection; 500 501 if ( ! this.get('syncSelection') || ! manager || ! selection ) 502 return; 503 504 // Record the currently active attachments, which is a combination 505 // of the selection's attachments and the set of selected 506 // attachments that this specific selection considered invalid. 507 // Reset the difference and record the single attachment. 508 if ( selection.multiple ) { 509 manager.attachments.reset( selection.toArray().concat( manager.difference ) ); 510 manager.difference = []; 511 } else { 512 manager.attachments.add( selection.toArray() ); 513 } 514 515 manager.single = selection._single; 516 }, 517 518 refreshContent: function() { 519 var selection = this.get('selection'), 520 frame = this.frame, 521 router = frame.router.get(), 522 mode = frame.content.mode(); 523 524 // If the state is active, no items are selected, and the current 525 // content mode is not an option in the state's router (provided 526 // the state has a router), reset the content mode to the default. 527 if ( this.active && ! selection.length && router && ! router.get( mode ) ) 528 this.frame.content.render( this.get('content') ); 529 }, 530 531 uploading: function( attachment ) { 532 var content = this.frame.content; 533 534 // If the uploader was selected, navigate to the browser. 535 if ( 'upload' === content.mode() ) 536 this.frame.content.mode('browse'); 537 538 // Automatically select any uploading attachments. 539 // 540 // Selections that don't support multiple attachments automatically 541 // limit themselves to one attachment (in this case, the last 542 // attachment in the upload queue). 543 this.get('selection').add( attachment ); 544 }, 545 546 saveContentMode: function() { 547 // Only track the browse router on library states. 548 if ( 'browse' !== this.get('router') ) 549 return; 550 551 var mode = this.frame.content.mode(), 552 view = this.frame.router.get(); 553 554 if ( view && view.get( mode ) ) 555 setUserSetting( 'libraryContent', mode ); 556 } 557 }); 558 559 // wp.media.controller.GalleryEdit 560 // ------------------------------- 561 media.controller.GalleryEdit = media.controller.Library.extend({ 562 defaults: { 563 id: 'gallery-edit', 564 multiple: false, 565 describe: true, 566 edge: 199, 567 editing: false, 568 sortable: true, 569 searchable: false, 570 toolbar: 'gallery-edit', 571 content: 'browse', 572 title: l10n.editGalleryTitle, 573 priority: 60, 574 dragInfo: true, 575 576 // Don't sync the selection, as the Edit Gallery library 577 // *is* the selection. 578 syncSelection: false 579 }, 580 581 initialize: function() { 582 // If we haven't been provided a `library`, create a `Selection`. 583 if ( ! this.get('library') ) 584 this.set( 'library', new media.model.Selection() ); 585 586 // The single `Attachment` view to be used in the `Attachments` view. 587 if ( ! this.get('AttachmentView') ) 588 this.set( 'AttachmentView', media.view.Attachment.EditLibrary ); 589 media.controller.Library.prototype.initialize.apply( this, arguments ); 590 }, 591 592 activate: function() { 593 var library = this.get('library'); 594 595 // Limit the library to images only. 596 library.props.set( 'type', 'image' ); 597 598 // Watch for uploaded attachments. 599 this.get('library').observe( wp.Uploader.queue ); 600 601 this.frame.on( 'content:render:browse', this.gallerySettings, this ); 602 603 media.controller.Library.prototype.activate.apply( this, arguments ); 604 }, 605 606 deactivate: function() { 607 // Stop watching for uploaded attachments. 608 this.get('library').unobserve( wp.Uploader.queue ); 609 610 this.frame.off( 'content:render:browse', this.gallerySettings, this ); 611 612 media.controller.Library.prototype.deactivate.apply( this, arguments ); 613 }, 614 615 gallerySettings: function( browser ) { 616 var library = this.get('library'); 617 618 if ( ! library || ! browser ) 619 return; 620 621 library.gallery = library.gallery || new Backbone.Model(); 622 623 browser.sidebar.set({ 624 gallery: new media.view.Settings.Gallery({ 625 controller: this, 626 model: library.gallery, 627 priority: 40 628 }) 629 }); 630 631 browser.toolbar.set( 'reverse', { 632 text: l10n.reverseOrder, 633 priority: 80, 634 635 click: function() { 636 library.reset( library.toArray().reverse() ); 637 } 638 }); 639 } 640 }); 641 642 // wp.media.controller.GalleryAdd 643 // --------------------------------- 644 media.controller.GalleryAdd = media.controller.Library.extend({ 645 defaults: _.defaults({ 646 id: 'gallery-library', 647 filterable: 'uploaded', 648 multiple: 'add', 649 menu: 'gallery', 650 toolbar: 'gallery-add', 651 title: l10n.addToGalleryTitle, 652 priority: 100, 653 654 // Don't sync the selection, as the Edit Gallery library 655 // *is* the selection. 656 syncSelection: false 657 }, media.controller.Library.prototype.defaults ), 658 659 initialize: function() { 660 // If we haven't been provided a `library`, create a `Selection`. 661 if ( ! this.get('library') ) 662 this.set( 'library', media.query({ type: 'image' }) ); 663 664 media.controller.Library.prototype.initialize.apply( this, arguments ); 665 }, 666 667 activate: function() { 668 var library = this.get('library'), 669 edit = this.frame.state('gallery-edit').get('library'); 670 671 if ( this.editLibrary && this.editLibrary !== edit ) 672 library.unobserve( this.editLibrary ); 673 674 // Accepts attachments that exist in the original library and 675 // that do not exist in gallery's library. 676 library.validator = function( attachment ) { 677 return !! this.mirroring.get( attachment.cid ) && ! edit.get( attachment.cid ) && media.model.Selection.prototype.validator.apply( this, arguments ); 678 }; 679 680 // Reset the library to ensure that all attachments are re-added 681 // to the collection. Do so silently, as calling `observe` will 682 // trigger the `reset` event. 683 library.reset( library.mirroring.models, { silent: true }); 684 library.observe( edit ); 685 this.editLibrary = edit; 686 687 media.controller.Library.prototype.activate.apply( this, arguments ); 688 } 689 }); 690 691 // wp.media.controller.FeaturedImage 692 // --------------------------------- 693 media.controller.FeaturedImage = media.controller.Library.extend({ 694 defaults: _.defaults({ 695 id: 'featured-image', 696 filterable: 'uploaded', 697 multiple: false, 698 toolbar: 'featured-image', 699 title: l10n.setFeaturedImageTitle, 700 priority: 60, 701 702 syncSelection: false 703 }, media.controller.Library.prototype.defaults ), 704 705 initialize: function() { 706 var library, comparator; 707 708 // If we haven't been provided a `library`, create a `Selection`. 709 if ( ! this.get('library') ) 710 this.set( 'library', media.query({ type: 'image' }) ); 711 712 media.controller.Library.prototype.initialize.apply( this, arguments ); 713 714 library = this.get('library'); 715 comparator = library.comparator; 716 717 // Overload the library's comparator to push items that are not in 718 // the mirrored query to the front of the aggregate collection. 719 library.comparator = function( a, b ) { 720 var aInQuery = !! this.mirroring.get( a.cid ), 721 bInQuery = !! this.mirroring.get( b.cid ); 722 723 if ( ! aInQuery && bInQuery ) 724 return -1; 725 else if ( aInQuery && ! bInQuery ) 726 return 1; 727 else 728 return comparator.apply( this, arguments ); 729 }; 730 731 // Add all items in the selection to the library, so any featured 732 // images that are not initially loaded still appear. 733 library.observe( this.get('selection') ); 734 }, 735 736 activate: function() { 737 this.updateSelection(); 738 this.frame.on( 'open', this.updateSelection, this ); 739 media.controller.Library.prototype.activate.apply( this, arguments ); 740 }, 741 742 deactivate: function() { 743 this.frame.off( 'open', this.updateSelection, this ); 744 media.controller.Library.prototype.deactivate.apply( this, arguments ); 745 }, 746 747 updateSelection: function() { 748 var selection = this.get('selection'), 749 id = media.view.settings.post.featuredImageId, 750 attachment; 751 752 if ( '' !== id && -1 !== id ) { 753 attachment = Attachment.get( id ); 754 attachment.fetch(); 755 } 756 757 selection.reset( attachment ? [ attachment ] : [] ); 758 } 759 }); 760 761 762 // wp.media.controller.Embed 763 // ------------------------- 764 media.controller.Embed = media.controller.State.extend({ 765 defaults: { 766 id: 'embed', 767 url: '', 768 menu: 'default', 769 content: 'embed', 770 toolbar: 'main-embed', 771 type: 'link', 772 773 title: l10n.insertFromUrlTitle, 774 priority: 120 775 }, 776 777 // The amount of time used when debouncing the scan. 778 sensitivity: 200, 779 780 initialize: function() { 781 this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity ); 782 this.props = new Backbone.Model({ url: '' }); 783 this.props.on( 'change:url', this.debouncedScan, this ); 784 this.props.on( 'change:url', this.refresh, this ); 785 this.on( 'scan', this.scanImage, this ); 786 }, 787 788 scan: function() { 789 var scanners, 790 embed = this, 791 attributes = { 792 type: 'link', 793 scanners: [] 794 }; 795 796 // Scan is triggered with the list of `attributes` to set on the 797 // state, useful for the 'type' attribute and 'scanners' attribute, 798 // an array of promise objects for asynchronous scan operations. 799 if ( this.props.get('url') ) 800 this.trigger( 'scan', attributes ); 801 802 if ( attributes.scanners.length ) { 803 scanners = attributes.scanners = $.when.apply( $, attributes.scanners ); 804 scanners.always( function() { 805 if ( embed.get('scanners') === scanners ) 806 embed.set( 'loading', false ); 807 }); 808 } else { 809 attributes.scanners = null; 810 } 811 812 attributes.loading = !! attributes.scanners; 813 this.set( attributes ); 814 }, 815 816 scanImage: function( attributes ) { 817 var frame = this.frame, 818 state = this, 819 url = this.props.get('url'), 820 image = new Image(), 821 deferred = $.Deferred(); 822 823 attributes.scanners.push( deferred.promise() ); 824 825 // Try to load the image and find its width/height. 826 image.onload = function() { 827 deferred.resolve(); 828 829 if ( state !== frame.state() || url !== state.props.get('url') ) 830 return; 831 832 state.set({ 833 type: 'image' 834 }); 835 836 state.props.set({ 837 width: image.width, 838 height: image.height 839 }); 840 }; 841 842 image.onerror = deferred.reject; 843 image.src = url; 844 }, 845 846 refresh: function() { 847 this.frame.toolbar.get().refresh(); 848 }, 849 850 reset: function() { 851 this.props.clear().set({ url: '' }); 852 853 if ( this.active ) 854 this.refresh(); 855 } 856 }); 857 858 /** 859 * ======================================================================== 860 * VIEWS 861 * ======================================================================== 862 */ 863 864 // wp.media.View 865 // ------------- 866 // 867 // The base view class. 868 // 869 // Undelegating events, removing events from the model, and 870 // removing events from the controller mirror the code for 871 // `Backbone.View.dispose` in Backbone 0.9.8 development. 872 // 873 // This behavior has since been removed, and should not be used 874 // outside of the media manager. 875 media.View = wp.Backbone.View.extend({ 876 constructor: function( options ) { 877 if ( options && options.controller ) 878 this.controller = options.controller; 879 880 wp.Backbone.View.apply( this, arguments ); 881 }, 882 883 dispose: function() { 884 // Undelegating events, removing events from the model, and 885 // removing events from the controller mirror the code for 886 // `Backbone.View.dispose` in Backbone 0.9.8 development. 887 this.undelegateEvents(); 888 889 if ( this.model && this.model.off ) 890 this.model.off( null, null, this ); 891 892 if ( this.collection && this.collection.off ) 893 this.collection.off( null, null, this ); 894 895 // Unbind controller events. 896 if ( this.controller && this.controller.off ) 897 this.controller.off( null, null, this ); 898 899 return this; 900 }, 901 902 remove: function() { 903 this.dispose(); 904 return wp.Backbone.View.prototype.remove.apply( this, arguments ); 905 } 906 }); 907 908 /** 909 * wp.media.view.Frame 910 */ 911 media.view.Frame = media.View.extend({ 912 initialize: function() { 913 this._createRegions(); 914 this._createStates(); 915 }, 916 917 _createRegions: function() { 918 // Clone the regions array. 919 this.regions = this.regions ? this.regions.slice() : []; 920 921 // Initialize regions. 922 _.each( this.regions, function( region ) { 923 this[ region ] = new media.controller.Region({ 924 view: this, 925 id: region, 926 selector: '.media-frame-' + region 927 }); 928 }, this ); 929 }, 930 931 _createStates: function() { 932 // Create the default `states` collection. 933 this.states = new Backbone.Collection( null, { 934 model: media.controller.State 935 }); 936 937 // Ensure states have a reference to the frame. 938 this.states.on( 'add', function( model ) { 939 model.frame = this; 940 model.trigger('ready'); 941 }, this ); 942 943 if ( this.options.states ) 944 this.states.add( this.options.states ); 945 }, 946 947 reset: function() { 948 this.states.invoke( 'trigger', 'reset' ); 949 return this; 950 } 951 }); 952 953 // Make the `Frame` a `StateMachine`. 954 _.extend( media.view.Frame.prototype, media.controller.StateMachine.prototype ); 955 956 /** 957 * wp.media.view.MediaFrame 958 */ 959 media.view.MediaFrame = media.view.Frame.extend({ 960 className: 'media-frame', 961 template: media.template('media-frame'), 962 regions: ['menu','title','content','toolbar','router'], 963 964 initialize: function() { 965 media.view.Frame.prototype.initialize.apply( this, arguments ); 966 967 _.defaults( this.options, { 968 title: '', 969 modal: true, 970 uploader: true 971 }); 972 973 // Ensure core UI is enabled. 974 this.$el.addClass('wp-core-ui'); 975 976 // Initialize modal container view. 977 if ( this.options.modal ) { 978 this.modal = new media.view.Modal({ 979 controller: this, 980 title: this.options.title 981 }); 982 983 this.modal.content( this ); 984 } 985 986 // Force the uploader off if the upload limit has been exceeded or 987 // if the browser isn't supported. 988 if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported ) 989 this.options.uploader = false; 990 991 // Initialize window-wide uploader. 992 if ( this.options.uploader ) { 993 this.uploader = new media.view.UploaderWindow({ 994 controller: this, 995 uploader: { 996 dropzone: this.modal ? this.modal.$el : this.$el, 997 container: this.$el 998 } 999 }); 1000 this.views.set( '.media-frame-uploader', this.uploader ); 1001 } 1002 1003 this.on( 'attach', _.bind( this.views.ready, this.views ), this ); 1004 1005 // Bind default title creation. 1006 this.on( 'title:create:default', this.createTitle, this ); 1007 this.title.mode('default'); 1008 1009 // Bind default menu. 1010 this.on( 'menu:create:default', this.createMenu, this ); 1011 }, 1012 1013 render: function() { 1014 // Activate the default state if no active state exists. 1015 if ( ! this.state() && this.options.state ) 1016 this.setState( this.options.state ); 1017 1018 return media.view.Frame.prototype.render.apply( this, arguments ); 1019 }, 1020 1021 createTitle: function( title ) { 1022 title.view = new media.View({ 1023 controller: this, 1024 tagName: 'h1' 1025 }); 1026 }, 1027 1028 createMenu: function( menu ) { 1029 menu.view = new media.view.Menu({ 1030 controller: this 1031 }); 1032 }, 1033 1034 createToolbar: function( toolbar ) { 1035 toolbar.view = new media.view.Toolbar({ 1036 controller: this 1037 }); 1038 }, 1039 1040 createRouter: function( router ) { 1041 router.view = new media.view.Router({ 1042 controller: this 1043 }); 1044 }, 1045 1046 createIframeStates: function( options ) { 1047 var settings = media.view.settings, 1048 tabs = settings.tabs, 1049 tabUrl = settings.tabUrl, 1050 $postId; 1051 1052 if ( ! tabs || ! tabUrl ) 1053 return; 1054 1055 // Add the post ID to the tab URL if it exists. 1056 $postId = $('#post_ID'); 1057 if ( $postId.length ) 1058 tabUrl += '&post_id=' + $postId.val(); 1059 1060 // Generate the tab states. 1061 _.each( tabs, function( title, id ) { 1062 this.state( 'iframe:' + id ).set( _.defaults({ 1063 tab: id, 1064 src: tabUrl + '&tab=' + id, 1065 title: title, 1066 content: 'iframe', 1067 menu: 'default' 1068 }, options ) ); 1069 }, this ); 1070 1071 this.on( 'content:create:iframe', this.iframeContent, this ); 1072 this.on( 'menu:render:default', this.iframeMenu, this ); 1073 this.on( 'open', this.hijackThickbox, this ); 1074 this.on( 'close', this.restoreThickbox, this ); 1075 }, 1076 1077 iframeContent: function( content ) { 1078 this.$el.addClass('hide-toolbar'); 1079 content.view = new media.view.Iframe({ 1080 controller: this 1081 }); 1082 }, 1083 1084 iframeMenu: function( view ) { 1085 var views = {}; 1086 1087 if ( ! view ) 1088 return; 1089 1090 _.each( media.view.settings.tabs, function( title, id ) { 1091 views[ 'iframe:' + id ] = { 1092 text: this.state( 'iframe:' + id ).get('title'), 1093 priority: 200 1094 }; 1095 }, this ); 1096 1097 view.set( views ); 1098 }, 1099 1100 hijackThickbox: function() { 1101 var frame = this; 1102 1103 if ( ! window.tb_remove || this._tb_remove ) 1104 return; 1105 1106 this._tb_remove = window.tb_remove; 1107 window.tb_remove = function() { 1108 frame.close(); 1109 frame.reset(); 1110 frame.setState( frame.options.state ); 1111 frame._tb_remove.call( window ); 1112 }; 1113 }, 1114 1115 restoreThickbox: function() { 1116 if ( ! this._tb_remove ) 1117 return; 1118 1119 window.tb_remove = this._tb_remove; 1120 delete this._tb_remove; 1121 } 1122 }); 1123 1124 // Map some of the modal's methods to the frame. 1125 _.each(['open','close','attach','detach','escape'], function( method ) { 1126 media.view.MediaFrame.prototype[ method ] = function() { 1127 if ( this.modal ) 1128 this.modal[ method ].apply( this.modal, arguments ); 1129 return this; 1130 }; 1131 }); 1132 1133 /** 1134 * wp.media.view.MediaFrame.Select 1135 */ 1136 media.view.MediaFrame.Select = media.view.MediaFrame.extend({ 1137 initialize: function() { 1138 media.view.MediaFrame.prototype.initialize.apply( this, arguments ); 1139 1140 _.defaults( this.options, { 1141 selection: [], 1142 library: {}, 1143 multiple: false, 1144 state: 'library' 1145 }); 1146 1147 this.createSelection(); 1148 this.createStates(); 1149 this.bindHandlers(); 1150 }, 1151 1152 createSelection: function() { 1153 var selection = this.options.selection; 1154 1155 if ( ! (selection instanceof media.model.Selection) ) { 1156 this.options.selection = new media.model.Selection( selection, { 1157 multiple: this.options.multiple 1158 }); 1159 } 1160 1161 this._selection = { 1162 attachments: new Attachments(), 1163 difference: [] 1164 }; 1165 }, 1166 1167 createStates: function() { 1168 var options = this.options; 1169 1170 if ( this.options.states ) 1171 return; 1172 1173 // Add the default states. 1174 this.states.add([ 1175 // Main states. 1176 new media.controller.Library({ 1177 library: media.query( options.library ), 1178 multiple: options.multiple, 1179 title: options.title, 1180 priority: 20 1181 }) 1182 ]); 1183 }, 1184 1185 bindHandlers: function() { 1186 this.on( 'router:create:browse', this.createRouter, this ); 1187 this.on( 'router:render:browse', this.browseRouter, this ); 1188 this.on( 'content:create:browse', this.browseContent, this ); 1189 this.on( 'content:render:upload', this.uploadContent, this ); 1190 this.on( 'toolbar:create:select', this.createSelectToolbar, this ); 1191 }, 1192 1193 // Routers 1194 browseRouter: function( view ) { 1195 view.set({ 1196 upload: { 1197 text: l10n.uploadFilesTitle, 1198 priority: 20 1199 }, 1200 browse: { 1201 text: l10n.mediaLibraryTitle, 1202 priority: 40 1203 } 1204 }); 1205 }, 1206 1207 // Content 1208 browseContent: function( content ) { 1209 var state = this.state(); 1210 1211 this.$el.removeClass('hide-toolbar'); 1212 1213 // Browse our library of attachments. 1214 content.view = new media.view.AttachmentsBrowser({ 1215 controller: this, 1216 collection: state.get('library'), 1217 selection: state.get('selection'), 1218 model: state, 1219 sortable: state.get('sortable'), 1220 search: state.get('searchable'), 1221 filters: state.get('filterable'), 1222 display: state.get('displaySettings'), 1223 dragInfo: state.get('dragInfo'), 1224 1225 AttachmentView: state.get('AttachmentView') 1226 }); 1227 }, 1228 1229 uploadContent: function() { 1230 this.$el.removeClass('hide-toolbar'); 1231 this.content.set( new media.view.UploaderInline({ 1232 controller: this 1233 }) ); 1234 }, 1235 1236 // Toolbars 1237 createSelectToolbar: function( toolbar, options ) { 1238 options = options || this.options.button || {}; 1239 options.controller = this; 1240 1241 toolbar.view = new media.view.Toolbar.Select( options ); 1242 } 1243 }); 1244 1245 /** 1246 * wp.media.view.MediaFrame.Post 1247 */ 1248 media.view.MediaFrame.Post = media.view.MediaFrame.Select.extend({ 1249 initialize: function() { 1250 _.defaults( this.options, { 1251 multiple: true, 1252 editing: false, 1253 state: 'insert' 1254 }); 1255 1256 media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments ); 1257 this.createIframeStates(); 1258 }, 1259 1260 createStates: function() { 1261 var options = this.options; 1262 1263 // Add the default states. 1264 this.states.add([ 1265 // Main states. 1266 new media.controller.Library({ 1267 id: 'insert', 1268 title: l10n.insertMediaTitle, 1269 priority: 20, 1270 toolbar: 'main-insert', 1271 filterable: 'all', 1272 library: media.query( options.library ), 1273 multiple: options.multiple ? 'reset' : false, 1274 editable: true, 1275 1276 // If the user isn't allowed to edit fields, 1277 // can they still edit it locally? 1278 allowLocalEdits: true, 1279 1280 // Show the attachment display settings. 1281 displaySettings: true, 1282 // Update user settings when users adjust the 1283 // attachment display settings. 1284 displayUserSettings: true 1285 }), 1286 1287 new media.controller.Library({ 1288 id: 'gallery', 1289 title: l10n.createGalleryTitle, 1290 priority: 40, 1291 toolbar: 'main-gallery', 1292 filterable: 'uploaded', 1293 multiple: 'add', 1294 editable: false, 1295 1296 library: media.query( _.defaults({ 1297 type: 'image' 1298 }, options.library ) ) 1299 }), 1300 1301 // Embed states. 1302 new media.controller.Embed(), 1303 1304 // Gallery states. 1305 new media.controller.GalleryEdit({ 1306 library: options.selection, 1307 editing: options.editing, 1308 menu: 'gallery' 1309 }), 1310 1311 new media.controller.GalleryAdd() 1312 ]); 1313 1314 1315 if ( media.view.settings.post.featuredImageId ) { 1316 this.states.add( new media.controller.FeaturedImage() ); 1317 } 1318 }, 1319 1320 bindHandlers: function() { 1321 media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments ); 1322 this.on( 'menu:create:gallery', this.createMenu, this ); 1323 this.on( 'toolbar:create:main-insert', this.createToolbar, this ); 1324 this.on( 'toolbar:create:main-gallery', this.createToolbar, this ); 1325 this.on( 'toolbar:create:featured-image', this.featuredImageToolbar, this ); 1326 this.on( 'toolbar:create:main-embed', this.mainEmbedToolbar, this ); 1327 1328 var handlers = { 1329 menu: { 1330 'default': 'mainMenu', 1331 'gallery': 'galleryMenu' 1332 }, 1333 1334 content: { 1335 'embed': 'embedContent', 1336 'edit-selection': 'editSelectionContent' 1337 }, 1338 1339 toolbar: { 1340 'main-insert': 'mainInsertToolbar', 1341 'main-gallery': 'mainGalleryToolbar', 1342 'gallery-edit': 'galleryEditToolbar', 1343 'gallery-add': 'galleryAddToolbar' 1344 } 1345 }; 1346 1347 _.each( handlers, function( regionHandlers, region ) { 1348 _.each( regionHandlers, function( callback, handler ) { 1349 this.on( region + ':render:' + handler, this[ callback ], this ); 1350 }, this ); 1351 }, this ); 1352 }, 1353 1354 // Menus 1355 mainMenu: function( view ) { 1356 view.set({ 1357 'library-separator': new media.View({ 1358 className: 'separator', 1359 priority: 100 1360 }) 1361 }); 1362 }, 1363 1364 galleryMenu: function( view ) { 1365 var lastState = this.lastState(), 1366 previous = lastState && lastState.id, 1367 frame = this; 1368 1369 view.set({ 1370 cancel: { 1371 text: l10n.cancelGalleryTitle, 1372 priority: 20, 1373 click: function() { 1374 if ( previous ) 1375 frame.setState( previous ); 1376 else 1377 frame.close(); 1378 } 1379 }, 1380 separateCancel: new media.View({ 1381 className: 'separator', 1382 priority: 40 1383 }) 1384 }); 1385 }, 1386 1387 // Content 1388 embedContent: function() { 1389 var view = new media.view.Embed({ 1390 controller: this, 1391 model: this.state() 1392 }).render(); 1393 1394 this.content.set( view ); 1395 view.url.focus(); 1396 }, 1397 1398 editSelectionContent: function() { 1399 var state = this.state(), 1400 selection = state.get('selection'), 1401 view; 1402 1403 view = new media.view.AttachmentsBrowser({ 1404 controller: this, 1405 collection: selection, 1406 selection: selection, 1407 model: state, 1408 sortable: true, 1409 search: false, 1410 dragInfo: true, 1411 1412 AttachmentView: media.view.Attachment.EditSelection 1413 }).render(); 1414 1415 view.toolbar.set( 'backToLibrary', { 1416 text: l10n.returnToLibrary, 1417 priority: -100, 1418 1419 click: function() { 1420 this.controller.content.mode('browse'); 1421 } 1422 }); 1423 1424 // Browse our library of attachments. 1425 this.content.set( view ); 1426 }, 1427 1428 // Toolbars 1429 selectionStatusToolbar: function( view ) { 1430 var editable = this.state().get('editable'); 1431 1432 view.set( 'selection', new media.view.Selection({ 1433 controller: this, 1434 collection: this.state().get('selection'), 1435 priority: -40, 1436 1437 // If the selection is editable, pass the callback to 1438 // switch the content mode. 1439 editable: editable && function() { 1440 this.controller.content.mode('edit-selection'); 1441 } 1442 }).render() ); 1443 }, 1444 1445 mainInsertToolbar: function( view ) { 1446 var controller = this; 1447 1448 this.selectionStatusToolbar( view ); 1449 1450 view.set( 'insert', { 1451 style: 'primary', 1452 priority: 80, 1453 text: l10n.insertIntoPost, 1454 requires: { selection: true }, 1455 1456 click: function() { 1457 var state = controller.state(), 1458 selection = state.get('selection'); 1459 1460 controller.close(); 1461 state.trigger( 'insert', selection ).reset(); 1462 } 1463 }); 1464 }, 1465 1466 mainGalleryToolbar: function( view ) { 1467 var controller = this; 1468 1469 this.selectionStatusToolbar( view ); 1470 1471 view.set( 'gallery', { 1472 style: 'primary', 1473 text: l10n.createNewGallery, 1474 priority: 60, 1475 requires: { selection: true }, 1476 1477 click: function() { 1478 var selection = controller.state().get('selection'), 1479 edit = controller.state('gallery-edit'), 1480 models = selection.where({ type: 'image' }); 1481 1482 edit.set( 'library', new media.model.Selection( models, { 1483 props: selection.props.toJSON(), 1484 multiple: true 1485 }) ); 1486 1487 this.controller.setState('gallery-edit'); 1488 } 1489 }); 1490 }, 1491 1492 featuredImageToolbar: function( toolbar ) { 1493 this.createSelectToolbar( toolbar, { 1494 text: l10n.setFeaturedImage, 1495 state: this.options.state 1496 }); 1497 }, 1498 1499 mainEmbedToolbar: function( toolbar ) { 1500 toolbar.view = new media.view.Toolbar.Embed({ 1501 controller: this 1502 }); 1503 }, 1504 1505 galleryEditToolbar: function() { 1506 var editing = this.state().get('editing'); 1507 this.toolbar.set( new media.view.Toolbar({ 1508 controller: this, 1509 items: { 1510 insert: { 1511 style: 'primary', 1512 text: editing ? l10n.updateGallery : l10n.insertGallery, 1513 priority: 80, 1514 requires: { library: true }, 1515 1516 click: function() { 1517 var controller = this.controller, 1518 state = controller.state(); 1519 1520 controller.close(); 1521 state.trigger( 'update', state.get('library') ); 1522 1523 // Restore and reset the default state. 1524 controller.setState( controller.options.state ); 1525 controller.reset(); 1526 } 1527 } 1528 } 1529 }) ); 1530 }, 1531 1532 galleryAddToolbar: function() { 1533 this.toolbar.set( new media.view.Toolbar({ 1534 controller: this, 1535 items: { 1536 insert: { 1537 style: 'primary', 1538 text: l10n.addToGallery, 1539 priority: 80, 1540 requires: { selection: true }, 1541 1542 click: function() { 1543 var controller = this.controller, 1544 state = controller.state(), 1545 edit = controller.state('gallery-edit'); 1546 1547 edit.get('library').add( state.get('selection').models ); 1548 state.trigger('reset'); 1549 controller.setState('gallery-edit'); 1550 } 1551 } 1552 } 1553 }) ); 1554 } 1555 }); 1556 1557 /** 1558 * wp.media.view.Modal 1559 */ 1560 media.view.Modal = media.View.extend({ 1561 tagName: 'div', 1562 template: media.template('media-modal'), 1563 1564 attributes: { 1565 tabindex: 0 1566 }, 1567 1568 events: { 1569 'click .media-modal-backdrop, .media-modal-close': 'escapeHandler', 1570 'keydown': 'keydown' 1571 }, 1572 1573 initialize: function() { 1574 _.defaults( this.options, { 1575 container: document.body, 1576 title: '', 1577 propagate: true, 1578 freeze: true 1579 }); 1580 }, 1581 1582 prepare: function() { 1583 return { 1584 title: this.options.title 1585 }; 1586 }, 1587 1588 attach: function() { 1589 if ( this.views.attached ) 1590 return this; 1591 1592 if ( ! this.views.rendered ) 1593 this.render(); 1594 1595 this.$el.appendTo( this.options.container ); 1596 1597 // Manually mark the view as attached and trigger ready. 1598 this.views.attached = true; 1599 this.views.ready(); 1600 1601 return this.propagate('attach'); 1602 }, 1603 1604 detach: function() { 1605 if ( this.$el.is(':visible') ) 1606 this.close(); 1607 1608 this.$el.detach(); 1609 this.views.attached = false; 1610 return this.propagate('detach'); 1611 }, 1612 1613 open: function() { 1614 var $el = this.$el, 1615 options = this.options; 1616 1617 if ( $el.is(':visible') ) 1618 return this; 1619 1620 if ( ! this.views.attached ) 1621 this.attach(); 1622 1623 // If the `freeze` option is set, record the window's scroll position. 1624 if ( options.freeze ) { 1625 this._freeze = { 1626 scrollTop: $( window ).scrollTop() 1627 }; 1628 } 1629 1630 $el.show().focus(); 1631 return this.propagate('open'); 1632 }, 1633 1634 close: function( options ) { 1635 var freeze = this._freeze; 1636 1637 if ( ! this.views.attached || ! this.$el.is(':visible') ) 1638 return this; 1639 1640 this.$el.hide(); 1641 this.propagate('close'); 1642 1643 // If the `freeze` option is set, restore the container's scroll position. 1644 if ( freeze ) { 1645 $( window ).scrollTop( freeze.scrollTop ); 1646 } 1647 1648 if ( options && options.escape ) 1649 this.propagate('escape'); 1650 1651 return this; 1652 }, 1653 1654 escape: function() { 1655 return this.close({ escape: true }); 1656 }, 1657 1658 escapeHandler: function( event ) { 1659 event.preventDefault(); 1660 this.escape(); 1661 }, 1662 1663 content: function( content ) { 1664 this.views.set( '.media-modal-content', content ); 1665 return this; 1666 }, 1667 1668 // Triggers a modal event and if the `propagate` option is set, 1669 // forwards events to the modal's controller. 1670 propagate: function( id ) { 1671 this.trigger( id ); 1672 1673 if ( this.options.propagate ) 1674 this.controller.trigger( id ); 1675 1676 return this; 1677 }, 1678 1679 keydown: function( event ) { 1680 // Close the modal when escape is pressed. 1681 if ( 27 === event.which ) { 1682 event.preventDefault(); 1683 this.escape(); 1684 return; 1685 } 1686 } 1687 }); 1688 1689 // wp.media.view.FocusManager 1690 // ---------------------------- 1691 media.view.FocusManager = media.View.extend({ 1692 events: { 1693 keydown: 'recordTab', 1694 focusin: 'updateIndex' 1695 }, 1696 1697 focus: function() { 1698 if ( _.isUndefined( this.index ) ) 1699 return; 1700 1701 // Update our collection of `$tabbables`. 1702 this.$tabbables = this.$(':tabbable'); 1703 1704 // If tab is saved, focus it. 1705 this.$tabbables.eq( this.index ).focus(); 1706 }, 1707 1708 recordTab: function( event ) { 1709 // Look for the tab key. 1710 if ( 9 !== event.keyCode ) 1711 return; 1712 1713 // First try to update the index. 1714 if ( _.isUndefined( this.index ) ) 1715 this.updateIndex( event ); 1716 1717 // If we still don't have an index, bail. 1718 if ( _.isUndefined( this.index ) ) 1719 return; 1720 1721 var index = this.index + ( event.shiftKey ? -1 : 1 ); 1722 1723 if ( index >= 0 && index < this.$tabbables.length ) 1724 this.index = index; 1725 else 1726 delete this.index; 1727 }, 1728 1729 updateIndex: function( event ) { 1730 this.$tabbables = this.$(':tabbable'); 1731 1732 var index = this.$tabbables.index( event.target ); 1733 1734 if ( -1 === index ) 1735 delete this.index; 1736 else 1737 this.index = index; 1738 } 1739 }); 1740 1741 // wp.media.view.UploaderWindow 1742 // ---------------------------- 1743 media.view.UploaderWindow = media.View.extend({ 1744 tagName: 'div', 1745 className: 'uploader-window', 1746 template: media.template('uploader-window'), 1747 1748 initialize: function() { 1749 var uploader; 1750 1751 this.$browser = $('<a href="#" class="browser" />').hide().appendTo('body'); 1752 1753 uploader = this.options.uploader = _.defaults( this.options.uploader || {}, { 1754 dropzone: this.$el, 1755 browser: this.$browser, 1756 params: {} 1757 }); 1758 1759 // Ensure the dropzone is a jQuery collection. 1760 if ( uploader.dropzone && ! (uploader.dropzone instanceof $) ) 1761 uploader.dropzone = $( uploader.dropzone ); 1762 1763 this.controller.on( 'activate', this.refresh, this ); 1764 }, 1765 1766 refresh: function() { 1767 if ( this.uploader ) 1768 this.uploader.refresh(); 1769 }, 1770 1771 ready: function() { 1772 var postId = media.view.settings.post.id, 1773 dropzone; 1774 1775 // If the uploader already exists, bail. 1776 if ( this.uploader ) 1777 return; 1778 1779 if ( postId ) 1780 this.options.uploader.params.post_id = postId; 1781 1782 this.uploader = new wp.Uploader( this.options.uploader ); 1783 1784 dropzone = this.uploader.dropzone; 1785 dropzone.on( 'dropzone:enter', _.bind( this.show, this ) ); 1786 dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) ); 1787 }, 1788 1789 show: function() { 1790 var $el = this.$el.show(); 1791 1792 // Ensure that the animation is triggered by waiting until 1793 // the transparent element is painted into the DOM. 1794 _.defer( function() { 1795 $el.css({ opacity: 1 }); 1796 }); 1797 }, 1798 1799 hide: function() { 1800 var $el = this.$el.css({ opacity: 0 }); 1801 1802 media.transition( $el ).done( function() { 1803 // Transition end events are subject to race conditions. 1804 // Make sure that the value is set as intended. 1805 if ( '0' === $el.css('opacity') ) 1806 $el.hide(); 1807 }); 1808 } 1809 }); 1810 1811 media.view.UploaderInline = media.View.extend({ 1812 tagName: 'div', 1813 className: 'uploader-inline', 1814 template: media.template('uploader-inline'), 1815 1816 initialize: function() { 1817 _.defaults( this.options, { 1818 message: '', 1819 status: true 1820 }); 1821 1822 if ( ! this.options.$browser && this.controller.uploader ) 1823 this.options.$browser = this.controller.uploader.$browser; 1824 1825 if ( _.isUndefined( this.options.postId ) ) 1826 this.options.postId = media.view.settings.post.id; 1827 1828 if ( this.options.status ) { 1829 this.views.set( '.upload-inline-status', new media.view.UploaderStatus({ 1830 controller: this.controller 1831 }) ); 1832 } 1833 }, 1834 1835 dispose: function() { 1836 if ( this.disposing ) 1837 return media.View.prototype.dispose.apply( this, arguments ); 1838 1839 // Run remove on `dispose`, so we can be sure to refresh the 1840 // uploader with a view-less DOM. Track whether we're disposing 1841 // so we don't trigger an infinite loop. 1842 this.disposing = true; 1843 return this.remove(); 1844 }, 1845 1846 remove: function() { 1847 var result = media.View.prototype.remove.apply( this, arguments ); 1848 1849 _.defer( _.bind( this.refresh, this ) ); 1850 return result; 1851 }, 1852 1853 refresh: function() { 1854 var uploader = this.controller.uploader; 1855 1856 if ( uploader ) 1857 uploader.refresh(); 1858 }, 1859 1860 ready: function() { 1861 var $browser = this.options.$browser, 1862 $placeholder; 1863 1864 if ( this.controller.uploader ) { 1865 $placeholder = this.$('.browser'); 1866 1867 // Check if we've already replaced the placeholder. 1868 if ( $placeholder[0] === $browser[0] ) 1869 return; 1870 1871 $browser.detach().text( $placeholder.text() ); 1872 $browser[0].className = $placeholder[0].className; 1873 $placeholder.replaceWith( $browser.show() ); 1874 } 1875 1876 this.refresh(); 1877 return this; 1878 } 1879 }); 1880 1881 /** 1882 * wp.media.view.UploaderStatus 1883 */ 1884 media.view.UploaderStatus = media.View.extend({ 1885 className: 'media-uploader-status', 1886 template: media.template('uploader-status'), 1887 1888 events: { 1889 'click .upload-dismiss-errors': 'dismiss' 1890 }, 1891 1892 initialize: function() { 1893 this.queue = wp.Uploader.queue; 1894 this.queue.on( 'add remove reset', this.visibility, this ); 1895 this.queue.on( 'add remove reset change:percent', this.progress, this ); 1896 this.queue.on( 'add remove reset change:uploading', this.info, this ); 1897 1898 this.errors = wp.Uploader.errors; 1899 this.errors.reset(); 1900 this.errors.on( 'add remove reset', this.visibility, this ); 1901 this.errors.on( 'add', this.error, this ); 1902 }, 1903 1904 dispose: function() { 1905 wp.Uploader.queue.off( null, null, this ); 1906 media.View.prototype.dispose.apply( this, arguments ); 1907 return this; 1908 }, 1909 1910 visibility: function() { 1911 this.$el.toggleClass( 'uploading', !! this.queue.length ); 1912 this.$el.toggleClass( 'errors', !! this.errors.length ); 1913 this.$el.toggle( !! this.queue.length || !! this.errors.length ); 1914 }, 1915 1916 ready: function() { 1917 _.each({ 1918 '$bar': '.media-progress-bar div', 1919 '$index': '.upload-index', 1920 '$total': '.upload-total', 1921 '$filename': '.upload-filename' 1922 }, function( selector, key ) { 1923 this[ key ] = this.$( selector ); 1924 }, this ); 1925 1926 this.visibility(); 1927 this.progress(); 1928 this.info(); 1929 }, 1930 1931 progress: function() { 1932 var queue = this.queue, 1933 $bar = this.$bar; 1934 1935 if ( ! $bar || ! queue.length ) 1936 return; 1937 1938 $bar.width( ( queue.reduce( function( memo, attachment ) { 1939 if ( ! attachment.get('uploading') ) 1940 return memo + 100; 1941 1942 var percent = attachment.get('percent'); 1943 return memo + ( _.isNumber( percent ) ? percent : 100 ); 1944 }, 0 ) / queue.length ) + '%' ); 1945 }, 1946 1947 info: function() { 1948 var queue = this.queue, 1949 index = 0, active; 1950 1951 if ( ! queue.length ) 1952 return; 1953 1954 active = this.queue.find( function( attachment, i ) { 1955 index = i; 1956 return attachment.get('uploading'); 1957 }); 1958 1959 this.$index.text( index + 1 ); 1960 this.$total.text( queue.length ); 1961 this.$filename.html( active ? this.filename( active.get('filename') ) : '' ); 1962 }, 1963 1964 filename: function( filename ) { 1965 return media.truncate( _.escape( filename ), 24 ); 1966 }, 1967 1968 error: function( error ) { 1969 this.views.add( '.upload-errors', new media.view.UploaderStatusError({ 1970 filename: this.filename( error.get('file').name ), 1971 message: error.get('message') 1972 }), { at: 0 }); 1973 }, 1974 1975 dismiss: function( event ) { 1976 var errors = this.views.get('.upload-errors'); 1977 1978 event.preventDefault(); 1979 1980 if ( errors ) 1981 _.invoke( errors, 'remove' ); 1982 wp.Uploader.errors.reset(); 1983 } 1984 }); 1985 1986 media.view.UploaderStatusError = media.View.extend({ 1987 className: 'upload-error', 1988 template: media.template('uploader-status-error') 1989 }); 1990 1991 /** 1992 * wp.media.view.Toolbar 1993 */ 1994 media.view.Toolbar = media.View.extend({ 1995 tagName: 'div', 1996 className: 'media-toolbar', 1997 1998 initialize: function() { 1999 var state = this.controller.state(), 2000 selection = this.selection = state.get('selection'), 2001 library = this.library = state.get('library'); 2002 2003 this._views = {}; 2004 2005 // The toolbar is composed of two `PriorityList` views. 2006 this.primary = new media.view.PriorityList(); 2007 this.secondary = new media.view.PriorityList(); 2008 this.primary.$el.addClass('media-toolbar-primary'); 2009 this.secondary.$el.addClass('media-toolbar-secondary'); 2010 2011 this.views.set([ this.secondary, this.primary ]); 2012 2013 if ( this.options.items ) 2014 this.set( this.options.items, { silent: true }); 2015 2016 if ( ! this.options.silent ) 2017 this.render(); 2018 2019 if ( selection ) 2020 selection.on( 'add remove reset', this.refresh, this ); 2021 if ( library ) 2022 library.on( 'add remove reset', this.refresh, this ); 2023 }, 2024 2025 dispose: function() { 2026 if ( this.selection ) 2027 this.selection.off( null, null, this ); 2028 if ( this.library ) 2029 this.library.off( null, null, this ); 2030 return media.View.prototype.dispose.apply( this, arguments ); 2031 }, 2032 2033 ready: function() { 2034 this.refresh(); 2035 }, 2036 2037 set: function( id, view, options ) { 2038 var list; 2039 options = options || {}; 2040 2041 // Accept an object with an `id` : `view` mapping. 2042 if ( _.isObject( id ) ) { 2043 _.each( id, function( view, id ) { 2044 this.set( id, view, { silent: true }); 2045 }, this ); 2046 2047 } else { 2048 if ( ! ( view instanceof Backbone.View ) ) { 2049 view.classes = [ 'media-button-' + id ].concat( view.classes || [] ); 2050 view = new media.view.Button( view ).render(); 2051 } 2052 2053 view.controller = view.controller || this.controller; 2054 2055 this._views[ id ] = view; 2056 2057 list = view.options.priority < 0 ? 'secondary' : 'primary'; 2058 this[ list ].set( id, view, options ); 2059 } 2060 2061 if ( ! options.silent ) 2062 this.refresh(); 2063 2064 return this; 2065 }, 2066 2067 get: function( id ) { 2068 return this._views[ id ]; 2069 }, 2070 2071 unset: function( id, options ) { 2072 delete this._views[ id ]; 2073 this.primary.unset( id, options ); 2074 this.secondary.unset( id, options ); 2075 2076 if ( ! options || ! options.silent ) 2077 this.refresh(); 2078 return this; 2079 }, 2080 2081 refresh: function() { 2082 var state = this.controller.state(), 2083 library = state.get('library'), 2084 selection = state.get('selection'); 2085 2086 _.each( this._views, function( button ) { 2087 if ( ! button.model || ! button.options || ! button.options.requires ) 2088 return; 2089 2090 var requires = button.options.requires, 2091 disabled = false; 2092 2093 // Prevent insertion of attachments if any of them are still uploading 2094 disabled = _.some( selection.models, function( attachment ) { 2095 return attachment.get('uploading') === true; 2096 }); 2097 2098 if ( requires.selection && selection && ! selection.length ) 2099 disabled = true; 2100 else if ( requires.library && library && ! library.length ) 2101 disabled = true; 2102 2103 button.model.set( 'disabled', disabled ); 2104 }); 2105 } 2106 }); 2107 2108 // wp.media.view.Toolbar.Select 2109 // ---------------------------- 2110 media.view.Toolbar.Select = media.view.Toolbar.extend({ 2111 initialize: function() { 2112 var options = this.options; 2113 2114 _.bindAll( this, 'clickSelect' ); 2115 2116 _.defaults( options, { 2117 event: 'select', 2118 state: false, 2119 reset: true, 2120 close: true, 2121 text: l10n.select, 2122 2123 // Does the button rely on the selection? 2124 requires: { 2125 selection: true 2126 } 2127 }); 2128 2129 options.items = _.defaults( options.items || {}, { 2130 select: { 2131 style: 'primary', 2132 text: options.text, 2133 priority: 80, 2134 click: this.clickSelect, 2135 requires: options.requires 2136 } 2137 }); 2138 2139 media.view.Toolbar.prototype.initialize.apply( this, arguments ); 2140 }, 2141 2142 clickSelect: function() { 2143 var options = this.options, 2144 controller = this.controller; 2145 2146 if ( options.close ) 2147 controller.close(); 2148 2149 if ( options.event ) 2150 controller.state().trigger( options.event ); 2151 2152 if ( options.state ) 2153 controller.setState( options.state ); 2154 2155 if ( options.reset ) 2156 controller.reset(); 2157 } 2158 }); 2159 2160 // wp.media.view.Toolbar.Embed 2161 // --------------------------- 2162 media.view.Toolbar.Embed = media.view.Toolbar.Select.extend({ 2163 initialize: function() { 2164 _.defaults( this.options, { 2165 text: l10n.insertIntoPost, 2166 requires: false 2167 }); 2168 2169 media.view.Toolbar.Select.prototype.initialize.apply( this, arguments ); 2170 }, 2171 2172 refresh: function() { 2173 var url = this.controller.state().props.get('url'); 2174 this.get('select').model.set( 'disabled', ! url || url === 'http://' ); 2175 2176 media.view.Toolbar.Select.prototype.refresh.apply( this, arguments ); 2177 } 2178 }); 2179 2180 /** 2181 * wp.media.view.Button 2182 */ 2183 media.view.Button = media.View.extend({ 2184 tagName: 'a', 2185 className: 'media-button', 2186 attributes: { href: '#' }, 2187 2188 events: { 2189 'click': 'click' 2190 }, 2191 2192 defaults: { 2193 text: '', 2194 style: '', 2195 size: 'large', 2196 disabled: false 2197 }, 2198 2199 initialize: function() { 2200 // Create a model with the provided `defaults`. 2201 this.model = new Backbone.Model( this.defaults ); 2202 2203 // If any of the `options` have a key from `defaults`, apply its 2204 // value to the `model` and remove it from the `options object. 2205 _.each( this.defaults, function( def, key ) { 2206 var value = this.options[ key ]; 2207 if ( _.isUndefined( value ) ) 2208 return; 2209 2210 this.model.set( key, value ); 2211 delete this.options[ key ]; 2212 }, this ); 2213 2214 this.model.on( 'change', this.render, this ); 2215 }, 2216 2217 render: function() { 2218 var classes = [ 'button', this.className ], 2219 model = this.model.toJSON(); 2220 2221 if ( model.style ) 2222 classes.push( 'button-' + model.style ); 2223 2224 if ( model.size ) 2225 classes.push( 'button-' + model.size ); 2226 2227 classes = _.uniq( classes.concat( this.options.classes ) ); 2228 this.el.className = classes.join(' '); 2229 2230 this.$el.attr( 'disabled', model.disabled ); 2231 this.$el.text( this.model.get('text') ); 2232 2233 return this; 2234 }, 2235 2236 click: function( event ) { 2237 if ( '#' === this.attributes.href ) 2238 event.preventDefault(); 2239 2240 if ( this.options.click && ! this.model.get('disabled') ) 2241 this.options.click.apply( this, arguments ); 2242 } 2243 }); 2244 2245 /** 2246 * wp.media.view.ButtonGroup 2247 */ 2248 media.view.ButtonGroup = media.View.extend({ 2249 tagName: 'div', 2250 className: 'button-group button-large media-button-group', 2251 2252 initialize: function() { 2253 this.buttons = _.map( this.options.buttons || [], function( button ) { 2254 if ( button instanceof Backbone.View ) 2255 return button; 2256 else 2257 return new media.view.Button( button ).render(); 2258 }); 2259 2260 delete this.options.buttons; 2261 2262 if ( this.options.classes ) 2263 this.$el.addClass( this.options.classes ); 2264 }, 2265 2266 render: function() { 2267 this.$el.html( $( _.pluck( this.buttons, 'el' ) ).detach() ); 2268 return this; 2269 } 2270 }); 2271 2272 /** 2273 * wp.media.view.PriorityList 2274 */ 2275 2276 media.view.PriorityList = media.View.extend({ 2277 tagName: 'div', 2278 2279 initialize: function() { 2280 this._views = {}; 2281 2282 this.set( _.extend( {}, this._views, this.options.views ), { silent: true }); 2283 delete this.options.views; 2284 2285 if ( ! this.options.silent ) 2286 this.render(); 2287 }, 2288 2289 set: function( id, view, options ) { 2290 var priority, views, index; 2291 2292 options = options || {}; 2293 2294 // Accept an object with an `id` : `view` mapping. 2295 if ( _.isObject( id ) ) { 2296 _.each( id, function( view, id ) { 2297 this.set( id, view ); 2298 }, this ); 2299 return this; 2300 } 2301 2302 if ( ! (view instanceof Backbone.View) ) 2303 view = this.toView( view, id, options ); 2304 2305 view.controller = view.controller || this.controller; 2306 2307 this.unset( id ); 2308 2309 priority = view.options.priority || 10; 2310 views = this.views.get() || []; 2311 2312 _.find( views, function( existing, i ) { 2313 if ( existing.options.priority > priority ) { 2314 index = i; 2315 return true; 2316 } 2317 }); 2318 2319 this._views[ id ] = view; 2320 this.views.add( view, { 2321 at: _.isNumber( index ) ? index : views.length || 0 2322 }); 2323 2324 return this; 2325 }, 2326 2327 get: function( id ) { 2328 return this._views[ id ]; 2329 }, 2330 2331 unset: function( id ) { 2332 var view = this.get( id ); 2333 2334 if ( view ) 2335 view.remove(); 2336 2337 delete this._views[ id ]; 2338 return this; 2339 }, 2340 2341 toView: function( options ) { 2342 return new media.View( options ); 2343 } 2344 }); 2345 2346 /** 2347 * wp.media.view.MenuItem 2348 */ 2349 media.view.MenuItem = media.View.extend({ 2350 tagName: 'a', 2351 className: 'media-menu-item', 2352 2353 attributes: { 2354 href: '#' 2355 }, 2356 2357 events: { 2358 'click': '_click' 2359 }, 2360 2361 _click: function( event ) { 2362 var clickOverride = this.options.click; 2363 2364 if ( event ) 2365 event.preventDefault(); 2366 2367 if ( clickOverride ) 2368 clickOverride.call( this ); 2369 else 2370 this.click(); 2371 }, 2372 2373 click: function() { 2374 var state = this.options.state; 2375 if ( state ) 2376 this.controller.setState( state ); 2377 }, 2378 2379 render: function() { 2380 var options = this.options; 2381 2382 if ( options.text ) 2383 this.$el.text( options.text ); 2384 else if ( options.html ) 2385 this.$el.html( options.html ); 2386 2387 return this; 2388 } 2389 }); 2390 2391 /** 2392 * wp.media.view.Menu 2393 */ 2394 media.view.Menu = media.view.PriorityList.extend({ 2395 tagName: 'div', 2396 className: 'media-menu', 2397 property: 'state', 2398 ItemView: media.view.MenuItem, 2399 region: 'menu', 2400 2401 toView: function( options, id ) { 2402 options = options || {}; 2403 options[ this.property ] = options[ this.property ] || id; 2404 return new this.ItemView( options ).render(); 2405 }, 2406 2407 ready: function() { 2408 media.view.PriorityList.prototype.ready.apply( this, arguments ); 2409 this.visibility(); 2410 }, 2411 2412 set: function() { 2413 media.view.PriorityList.prototype.set.apply( this, arguments ); 2414 this.visibility(); 2415 }, 2416 2417 unset: function() { 2418 media.view.PriorityList.prototype.unset.apply( this, arguments ); 2419 this.visibility(); 2420 }, 2421 2422 visibility: function() { 2423 var region = this.region, 2424 view = this.controller[ region ].get(), 2425 views = this.views.get(), 2426 hide = ! views || views.length < 2; 2427 2428 if ( this === view ) 2429 this.controller.$el.toggleClass( 'hide-' + region, hide ); 2430 }, 2431 2432 select: function( id ) { 2433 var view = this.get( id ); 2434 2435 if ( ! view ) 2436 return; 2437 2438 this.deselect(); 2439 view.$el.addClass('active'); 2440 }, 2441 2442 deselect: function() { 2443 this.$el.children().removeClass('active'); 2444 } 2445 }); 2446 2447 /** 2448 * wp.media.view.RouterItem 2449 */ 2450 media.view.RouterItem = media.view.MenuItem.extend({ 2451 click: function() { 2452 var contentMode = this.options.contentMode; 2453 if ( contentMode ) 2454 this.controller.content.mode( contentMode ); 2455 } 2456 }); 2457 2458 /** 2459 * wp.media.view.Router 2460 */ 2461 media.view.Router = media.view.Menu.extend({ 2462 tagName: 'div', 2463 className: 'media-router', 2464 property: 'contentMode', 2465 ItemView: media.view.RouterItem, 2466 region: 'router', 2467 2468 initialize: function() { 2469 this.controller.on( 'content:render', this.update, this ); 2470 media.view.Menu.prototype.initialize.apply( this, arguments ); 2471 }, 2472 2473 update: function() { 2474 var mode = this.controller.content.mode(); 2475 if ( mode ) 2476 this.select( mode ); 2477 } 2478 }); 2479 2480 2481 /** 2482 * wp.media.view.Sidebar 2483 */ 2484 media.view.Sidebar = media.view.PriorityList.extend({ 2485 className: 'media-sidebar' 2486 }); 2487 2488 /** 2489 * wp.media.view.Attachment 2490 */ 2491 media.view.Attachment = media.View.extend({ 2492 tagName: 'li', 2493 className: 'attachment', 2494 template: media.template('attachment'), 2495 2496 events: { 2497 'click .attachment-preview': 'toggleSelectionHandler', 2498 'change [data-setting]': 'updateSetting', 2499 'change [data-setting] input': 'updateSetting', 2500 'change [data-setting] select': 'updateSetting', 2501 'change [data-setting] textarea': 'updateSetting', 2502 'click .close': 'removeFromLibrary', 2503 'click .check': 'removeFromSelection', 2504 'click a': 'preventDefault' 2505 }, 2506 2507 buttons: {}, 2508 2509 initialize: function() { 2510 var selection = this.options.selection; 2511 2512 this.model.on( 'change:sizes change:uploading', this.render, this ); 2513 this.model.on( 'change:title', this._syncTitle, this ); 2514 this.model.on( 'change:caption', this._syncCaption, this ); 2515 this.model.on( 'change:percent', this.progress, this ); 2516 2517 // Update the selection. 2518 this.model.on( 'add', this.select, this ); 2519 this.model.on( 'remove', this.deselect, this ); 2520 if ( selection ) 2521 selection.on( 'reset', this.updateSelect, this ); 2522 2523 // Update the model's details view. 2524 this.model.on( 'selection:single selection:unsingle', this.details, this ); 2525 this.details( this.model, this.controller.state().get('selection') ); 2526 }, 2527 2528 dispose: function() { 2529 var selection = this.options.selection; 2530 2531 // Make sure all settings are saved before removing the view. 2532 this.updateAll(); 2533 2534 if ( selection ) 2535 selection.off( null, null, this ); 2536 2537 media.View.prototype.dispose.apply( this, arguments ); 2538 return this; 2539 }, 2540 2541 render: function() { 2542 var options = _.defaults( this.model.toJSON(), { 2543 orientation: 'landscape', 2544 uploading: false, 2545 type: '', 2546 subtype: '', 2547 icon: '', 2548 filename: '', 2549 caption: '', 2550 title: '', 2551 dateFormatted: '', 2552 width: '', 2553 height: '', 2554 compat: false, 2555 alt: '', 2556 description: '' 2557 }); 2558 2559 options.buttons = this.buttons; 2560 options.describe = this.controller.state().get('describe'); 2561 2562 if ( 'image' === options.type ) 2563 options.size = this.imageSize(); 2564 2565 options.can = {}; 2566 if ( options.nonces ) { 2567 options.can.remove = !! options.nonces['delete']; 2568 options.can.save = !! options.nonces.update; 2569 } 2570 2571 if ( this.controller.state().get('allowLocalEdits') ) 2572 options.allowLocalEdits = true; 2573 2574 this.views.detach(); 2575 this.$el.html( this.template( options ) ); 2576 2577 this.$el.toggleClass( 'uploading', options.uploading ); 2578 if ( options.uploading ) 2579 this.$bar = this.$('.media-progress-bar div'); 2580 else 2581 delete this.$bar; 2582 2583 // Check if the model is selected. 2584 this.updateSelect(); 2585 2586 // Update the save status. 2587 this.updateSave(); 2588 2589 this.views.render(); 2590 2591 return this; 2592 }, 2593 2594 progress: function() { 2595 if ( this.$bar && this.$bar.length ) 2596 this.$bar.width( this.model.get('percent') + '%' ); 2597 }, 2598 2599 toggleSelectionHandler: function( event ) { 2600 var method; 2601 2602 if ( event.shiftKey ) 2603 method = 'between'; 2604 else if ( event.ctrlKey || event.metaKey ) 2605 method = 'toggle'; 2606 2607 this.toggleSelection({ 2608 method: method 2609 }); 2610 }, 2611 2612 toggleSelection: function( options ) { 2613 var collection = this.collection, 2614 selection = this.options.selection, 2615 model = this.model, 2616 method = options && options.method, 2617 single, models, singleIndex, modelIndex; 2618 2619 if ( ! selection ) 2620 return; 2621 2622 single = selection.single(); 2623 method = _.isUndefined( method ) ? selection.multiple : method; 2624 2625 // If the `method` is set to `between`, select all models that 2626 // exist between the current and the selected model. 2627 if ( 'between' === method && single && selection.multiple ) { 2628 // If the models are the same, short-circuit. 2629 if ( single === model ) 2630 return; 2631 2632 singleIndex = collection.indexOf( single ); 2633 modelIndex = collection.indexOf( this.model ); 2634 2635 if ( singleIndex < modelIndex ) 2636 models = collection.models.slice( singleIndex, modelIndex + 1 ); 2637 else 2638 models = collection.models.slice( modelIndex, singleIndex + 1 ); 2639 2640 selection.add( models ).single( model ); 2641 return; 2642 2643 // If the `method` is set to `toggle`, just flip the selection 2644 // status, regardless of whether the model is the single model. 2645 } else if ( 'toggle' === method ) { 2646 selection[ this.selected() ? 'remove' : 'add' ]( model ).single( model ); 2647 return; 2648 } 2649 2650 if ( method !== 'add' ) 2651 method = 'reset'; 2652 2653 if ( this.selected() ) { 2654 // If the model is the single model, remove it. 2655 // If it is not the same as the single model, 2656 // it now becomes the single model. 2657 selection[ single === model ? 'remove' : 'single' ]( model ); 2658 } else { 2659 // If the model is not selected, run the `method` on the 2660 // selection. By default, we `reset` the selection, but the 2661 // `method` can be set to `add` the model to the selection. 2662 selection[ method ]( model ).single( model ); 2663 } 2664 }, 2665 2666 updateSelect: function() { 2667 this[ this.selected() ? 'select' : 'deselect' ](); 2668 }, 2669 2670 selected: function() { 2671 var selection = this.options.selection; 2672 if ( selection ) 2673 return !! selection.get( this.model.cid ); 2674 }, 2675 2676 select: function( model, collection ) { 2677 var selection = this.options.selection; 2678 2679 // Check if a selection exists and if it's the collection provided. 2680 // If they're not the same collection, bail; we're in another 2681 // selection's event loop. 2682 if ( ! selection || ( collection && collection !== selection ) ) 2683 return; 2684 2685 this.$el.addClass('selected'); 2686 }, 2687 2688 deselect: function( model, collection ) { 2689 var selection = this.options.selection; 2690 2691 // Check if a selection exists and if it's the collection provided. 2692 // If they're not the same collection, bail; we're in another 2693 // selection's event loop. 2694 if ( ! selection || ( collection && collection !== selection ) ) 2695 return; 2696 2697 this.$el.removeClass('selected'); 2698 }, 2699 2700 details: function( model, collection ) { 2701 var selection = this.options.selection, 2702 details; 2703 2704 if ( selection !== collection ) 2705 return; 2706 2707 details = selection.single(); 2708 this.$el.toggleClass( 'details', details === this.model ); 2709 }, 2710 2711 preventDefault: function( event ) { 2712 event.preventDefault(); 2713 }, 2714 2715 imageSize: function( size ) { 2716 var sizes = this.model.get('sizes'); 2717 2718 size = size || 'medium'; 2719 2720 // Use the provided image size if possible. 2721 if ( sizes && sizes[ size ] ) { 2722 return _.clone( sizes[ size ] ); 2723 } else { 2724 return { 2725 url: this.model.get('url'), 2726 width: this.model.get('width'), 2727 height: this.model.get('height'), 2728 orientation: this.model.get('orientation') 2729 }; 2730 } 2731 }, 2732 2733 updateSetting: function( event ) { 2734 var $setting = $( event.target ).closest('[data-setting]'), 2735 setting, value; 2736 2737 if ( ! $setting.length ) 2738 return; 2739 2740 setting = $setting.data('setting'); 2741 value = event.target.value; 2742 2743 if ( this.model.get( setting ) !== value ) 2744 this.save( setting, value ); 2745 }, 2746 2747 // Pass all the arguments to the model's save method. 2748 // 2749 // Records the aggregate status of all save requests and updates the 2750 // view's classes accordingly. 2751 save: function() { 2752 var view = this, 2753 save = this._save = this._save || { status: 'ready' }, 2754 request = this.model.save.apply( this.model, arguments ), 2755 requests = save.requests ? $.when( request, save.requests ) : request; 2756 2757 // If we're waiting to remove 'Saved.', stop. 2758 if ( save.savedTimer ) 2759 clearTimeout( save.savedTimer ); 2760 2761 this.updateSave('waiting'); 2762 save.requests = requests; 2763 requests.always( function() { 2764 // If we've performed another request since this one, bail. 2765 if ( save.requests !== requests ) 2766 return; 2767 2768 view.updateSave( requests.state() === 'resolved' ? 'complete' : 'error' ); 2769 save.savedTimer = setTimeout( function() { 2770 view.updateSave('ready'); 2771 delete save.savedTimer; 2772 }, 2000 ); 2773 }); 2774 2775 }, 2776 2777 updateSave: function( status ) { 2778 var save = this._save = this._save || { status: 'ready' }; 2779 2780 if ( status && status !== save.status ) { 2781 this.$el.removeClass( 'save-' + save.status ); 2782 save.status = status; 2783 } 2784 2785 this.$el.addClass( 'save-' + save.status ); 2786 return this; 2787 }, 2788 2789 updateAll: function() { 2790 var $settings = this.$('[data-setting]'), 2791 model = this.model, 2792 changed; 2793 2794 changed = _.chain( $settings ).map( function( el ) { 2795 var $input = $('input, textarea, select, [value]', el ), 2796 setting, value; 2797 2798 if ( ! $input.length ) 2799 return; 2800 2801 setting = $(el).data('setting'); 2802 value = $input.val(); 2803 2804 // Record the value if it changed. 2805 if ( model.get( setting ) !== value ) 2806 return [ setting, value ]; 2807 }).compact().object().value(); 2808 2809 if ( ! _.isEmpty( changed ) ) 2810 model.save( changed ); 2811 }, 2812 2813 removeFromLibrary: function( event ) { 2814 // Stop propagation so the model isn't selected. 2815 event.stopPropagation(); 2816 2817 this.collection.remove( this.model ); 2818 }, 2819 2820 removeFromSelection: function( event ) { 2821 var selection = this.options.selection; 2822 if ( ! selection ) 2823 return; 2824 2825 // Stop propagation so the model isn't selected. 2826 event.stopPropagation(); 2827 2828 selection.remove( this.model ); 2829 } 2830 }); 2831 2832 // Ensure settings remain in sync between attachment views. 2833 _.each({ 2834 caption: '_syncCaption', 2835 title: '_syncTitle' 2836 }, function( method, setting ) { 2837 media.view.Attachment.prototype[ method ] = function( model, value ) { 2838 var $setting = this.$('[data-setting="' + setting + '"]'); 2839 2840 if ( ! $setting.length ) 2841 return this; 2842 2843 // If the updated value is in sync with the value in the DOM, there 2844 // is no need to re-render. If we're currently editing the value, 2845 // it will automatically be in sync, suppressing the re-render for 2846 // the view we're editing, while updating any others. 2847 if ( value === $setting.find('input, textarea, select, [value]').val() ) 2848 return this; 2849 2850 return this.render(); 2851 }; 2852 }); 2853 2854 /** 2855 * wp.media.view.Attachment.Library 2856 */ 2857 media.view.Attachment.Library = media.view.Attachment.extend({ 2858 buttons: { 2859 check: true 2860 } 2861 }); 2862 2863 /** 2864 * wp.media.view.Attachment.EditLibrary 2865 */ 2866 media.view.Attachment.EditLibrary = media.view.Attachment.extend({ 2867 buttons: { 2868 close: true 2869 } 2870 }); 2871 2872 /** 2873 * wp.media.view.Attachments 2874 */ 2875 media.view.Attachments = media.View.extend({ 2876 tagName: 'ul', 2877 className: 'attachments', 2878 2879 cssTemplate: media.template('attachments-css'), 2880 2881 events: { 2882 'scroll': 'scroll' 2883 }, 2884 2885 initialize: function() { 2886 this.el.id = _.uniqueId('__attachments-view-'); 2887 2888 _.defaults( this.options, { 2889 refreshSensitivity: 200, 2890 refreshThreshold: 3, 2891 AttachmentView: media.view.Attachment, 2892 sortable: false, 2893 resize: true 2894 }); 2895 2896 this._viewsByCid = {}; 2897 2898 this.collection.on( 'add', function( attachment ) { 2899 this.views.add( this.createAttachmentView( attachment ), { 2900 at: this.collection.indexOf( attachment ) 2901 }); 2902 }, this ); 2903 2904 this.collection.on( 'remove', function( attachment ) { 2905 var view = this._viewsByCid[ attachment.cid ]; 2906 delete this._viewsByCid[ attachment.cid ]; 2907 2908 if ( view ) 2909 view.remove(); 2910 }, this ); 2911 2912 this.collection.on( 'reset', this.render, this ); 2913 2914 // Throttle the scroll handler. 2915 this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value(); 2916 2917 this.initSortable(); 2918 2919 _.bindAll( this, 'css' ); 2920 this.model.on( 'change:edge change:gutter', this.css, this ); 2921 this._resizeCss = _.debounce( _.bind( this.css, this ), this.refreshSensitivity ); 2922 if ( this.options.resize ) 2923 $(window).on( 'resize.attachments', this._resizeCss ); 2924 this.css(); 2925 }, 2926 2927 dispose: function() { 2928 this.collection.props.off( null, null, this ); 2929 $(window).off( 'resize.attachments', this._resizeCss ); 2930 media.View.prototype.dispose.apply( this, arguments ); 2931 }, 2932 2933 css: function() { 2934 var $css = $( '#' + this.el.id + '-css' ); 2935 2936 if ( $css.length ) 2937 $css.remove(); 2938 2939 media.view.Attachments.$head().append( this.cssTemplate({ 2940 id: this.el.id, 2941 edge: this.edge(), 2942 gutter: this.model.get('gutter') 2943 }) ); 2944 }, 2945 2946 edge: function() { 2947 var edge = this.model.get('edge'), 2948 gutter, width, columns; 2949 2950 if ( ! this.$el.is(':visible') ) 2951 return edge; 2952 2953 gutter = this.model.get('gutter') * 2; 2954 width = this.$el.width() - gutter; 2955 columns = Math.ceil( width / ( edge + gutter ) ); 2956 edge = Math.floor( ( width - ( columns * gutter ) ) / columns ); 2957 return edge; 2958 }, 2959 2960 initSortable: function() { 2961 var collection = this.collection; 2962 2963 if ( ! this.options.sortable || ! $.fn.sortable ) 2964 return; 2965 2966 this.$el.sortable( _.extend({ 2967 // If the `collection` has a `comparator`, disable sorting. 2968 disabled: !! collection.comparator, 2969 2970 // Prevent attachments from being dragged outside the bounding 2971 // box of the list. 2972 containment: this.$el, 2973 2974 // Change the position of the attachment as soon as the 2975 // mouse pointer overlaps a thumbnail. 2976 tolerance: 'pointer', 2977 2978 // Record the initial `index` of the dragged model. 2979 start: function( event, ui ) { 2980 ui.item.data('sortableIndexStart', ui.item.index()); 2981 }, 2982 2983 // Update the model's index in the collection. 2984 // Do so silently, as the view is already accurate. 2985 update: function( event, ui ) { 2986 var model = collection.at( ui.item.data('sortableIndexStart') ), 2987 comparator = collection.comparator; 2988 2989 // Temporarily disable the comparator to prevent `add` 2990 // from re-sorting. 2991 delete collection.comparator; 2992 2993 // Silently shift the model to its new index. 2994 collection.remove( model, { 2995 silent: true 2996 }).add( model, { 2997 silent: true, 2998 at: ui.item.index() 2999 }); 3000 3001 // Restore the comparator. 3002 collection.comparator = comparator; 3003 3004 // Fire the `reset` event to ensure other collections sync. 3005 collection.trigger( 'reset', collection ); 3006 3007 // If the collection is sorted by menu order, 3008 // update the menu order. 3009 collection.saveMenuOrder(); 3010 } 3011 }, this.options.sortable ) ); 3012 3013 // If the `orderby` property is changed on the `collection`, 3014 // check to see if we have a `comparator`. If so, disable sorting. 3015 collection.props.on( 'change:orderby', function() { 3016 this.$el.sortable( 'option', 'disabled', !! collection.comparator ); 3017 }, this ); 3018 3019 this.collection.props.on( 'change:orderby', this.refreshSortable, this ); 3020 this.refreshSortable(); 3021 }, 3022 3023 refreshSortable: function() { 3024 if ( ! this.options.sortable || ! $.fn.sortable ) 3025 return; 3026 3027 // If the `collection` has a `comparator`, disable sorting. 3028 var collection = this.collection, 3029 orderby = collection.props.get('orderby'), 3030 enabled = 'menuOrder' === orderby || ! collection.comparator; 3031 3032 this.$el.sortable( 'option', 'disabled', ! enabled ); 3033 }, 3034 3035 createAttachmentView: function( attachment ) { 3036 var view = new this.options.AttachmentView({ 3037 controller: this.controller, 3038 model: attachment, 3039 collection: this.collection, 3040 selection: this.options.selection 3041 }); 3042 3043 return this._viewsByCid[ attachment.cid ] = view; 3044 }, 3045 3046 prepare: function() { 3047 // Create all of the Attachment views, and replace 3048 // the list in a single DOM operation. 3049 if ( this.collection.length ) { 3050 this.views.set( this.collection.map( this.createAttachmentView, this ) ); 3051 3052 // If there are no elements, clear the views and load some. 3053 } else { 3054 this.views.unset(); 3055 this.collection.more().done( this.scroll ); 3056 } 3057 }, 3058 3059 ready: function() { 3060 // Trigger the scroll event to check if we're within the 3061 // threshold to query for additional attachments. 3062 this.scroll(); 3063 }, 3064 3065 scroll: function() { 3066 // @todo: is this still necessary? 3067 if ( ! this.$el.is(':visible') ) 3068 return; 3069 3070 if ( this.collection.hasMore() && this.el.scrollHeight < this.el.scrollTop + ( this.el.clientHeight * this.options.refreshThreshold ) ) { 3071 this.collection.more().done( this.scroll ); 3072 } 3073 } 3074 }, { 3075 $head: (function() { 3076 var $head; 3077 return function() { 3078 return $head = $head || $('head'); 3079 }; 3080 }()) 3081 }); 3082 3083 /** 3084 * wp.media.view.Search 3085 */ 3086 media.view.Search = media.View.extend({ 3087 tagName: 'input', 3088 className: 'search', 3089 3090 attributes: { 3091 type: 'search', 3092 placeholder: l10n.search 3093 }, 3094 3095 events: { 3096 'input': 'search', 3097 'keyup': 'search', 3098 'change': 'search', 3099 'search': 'search' 3100 }, 3101 3102 render: function() { 3103 this.el.value = this.model.escape('search'); 3104 return this; 3105 }, 3106 3107 search: function( event ) { 3108 if ( event.target.value ) 3109 this.model.set( 'search', event.target.value ); 3110 else 3111 this.model.unset('search'); 3112 } 3113 }); 3114 3115 /** 3116 * wp.media.view.AttachmentFilters 3117 */ 3118 media.view.AttachmentFilters = media.View.extend({ 3119 tagName: 'select', 3120 className: 'attachment-filters', 3121 3122 events: { 3123 change: 'change' 3124 }, 3125 3126 keys: [], 3127 3128 initialize: function() { 3129 this.createFilters(); 3130 _.extend( this.filters, this.options.filters ); 3131 3132 // Build `<option>` elements. 3133 this.$el.html( _.chain( this.filters ).map( function( filter, value ) { 3134 return { 3135 el: $('<option></option>').val(value).text(filter.text)[0], 3136 priority: filter.priority || 50 3137 }; 3138 }, this ).sortBy('priority').pluck('el').value() ); 3139 3140 this.model.on( 'change', this.select, this ); 3141 this.select(); 3142 }, 3143 3144 createFilters: function() { 3145 this.filters = {}; 3146 }, 3147 3148 change: function() { 3149 var filter = this.filters[ this.el.value ]; 3150 3151 if ( filter ) 3152 this.model.set( filter.props ); 3153 }, 3154 3155 select: function() { 3156 var model = this.model, 3157 value = 'all', 3158 props = model.toJSON(); 3159 3160 _.find( this.filters, function( filter, id ) { 3161 var equal = _.all( filter.props, function( prop, key ) { 3162 return prop === ( _.isUndefined( props[ key ] ) ? null : props[ key ] ); 3163 }); 3164 3165 if ( equal ) 3166 return value = id; 3167 }); 3168 3169 this.$el.val( value ); 3170 } 3171 }); 3172 3173 media.view.AttachmentFilters.Uploaded = media.view.AttachmentFilters.extend({ 3174 createFilters: function() { 3175 var type = this.model.get('type'), 3176 types = media.view.settings.mimeTypes, 3177 text; 3178 3179 if ( types && type ) 3180 text = types[ type ]; 3181 3182 this.filters = { 3183 all: { 3184 text: text || l10n.allMediaItems, 3185 props: { 3186 uploadedTo: null, 3187 orderby: 'date', 3188 order: 'DESC' 3189 }, 3190 priority: 10 3191 }, 3192 3193 uploaded: { 3194 text: l10n.uploadedToThisPost, 3195 props: { 3196 uploadedTo: media.view.settings.post.id, 3197 orderby: 'menuOrder', 3198 order: 'ASC' 3199 }, 3200 priority: 20 3201 } 3202 }; 3203 } 3204 }); 3205 3206 media.view.AttachmentFilters.All = media.view.AttachmentFilters.extend({ 3207 createFilters: function() { 3208 var filters = {}; 3209 3210 _.each( media.view.settings.mimeTypes || {}, function( text, key ) { 3211 filters[ key ] = { 3212 text: text, 3213 props: { 3214 type: key, 3215 uploadedTo: null, 3216 orderby: 'date', 3217 order: 'DESC' 3218 } 3219 }; 3220 }); 3221 3222 filters.all = { 3223 text: l10n.allMediaItems, 3224 props: { 3225 type: null, 3226 uploadedTo: null, 3227 orderby: 'date', 3228 order: 'DESC' 3229 }, 3230 priority: 10 3231 }; 3232 3233 filters.uploaded = { 3234 text: l10n.uploadedToThisPost, 3235 props: { 3236 type: null, 3237 uploadedTo: media.view.settings.post.id, 3238 orderby: 'menuOrder', 3239 order: 'ASC' 3240 }, 3241 priority: 20 3242 }; 3243 3244 this.filters = filters; 3245 } 3246 }); 3247 3248 3249 3250 /** 3251 * wp.media.view.AttachmentsBrowser 3252 */ 3253 media.view.AttachmentsBrowser = media.View.extend({ 3254 tagName: 'div', 3255 className: 'attachments-browser', 3256 3257 initialize: function() { 3258 _.defaults( this.options, { 3259 filters: false, 3260 search: true, 3261 display: false, 3262 3263 AttachmentView: media.view.Attachment.Library 3264 }); 3265 3266 this.createToolbar(); 3267 this.updateContent(); 3268 this.createSidebar(); 3269 3270 this.collection.on( 'add remove reset', this.updateContent, this ); 3271 }, 3272 3273 dispose: function() { 3274 this.options.selection.off( null, null, this ); 3275 media.View.prototype.dispose.apply( this, arguments ); 3276 return this; 3277 }, 3278 3279 createToolbar: function() { 3280 var filters, FiltersConstructor; 3281 3282 this.toolbar = new media.view.Toolbar({ 3283 controller: this.controller 3284 }); 3285 3286 this.views.add( this.toolbar ); 3287 3288 filters = this.options.filters; 3289 if ( 'uploaded' === filters ) 3290 FiltersConstructor = media.view.AttachmentFilters.Uploaded; 3291 else if ( 'all' === filters ) 3292 FiltersConstructor = media.view.AttachmentFilters.All; 3293 3294 if ( FiltersConstructor ) { 3295 this.toolbar.set( 'filters', new FiltersConstructor({ 3296 controller: this.controller, 3297 model: this.collection.props, 3298 priority: -80 3299 }).render() ); 3300 } 3301 3302 if ( this.options.search ) { 3303 this.toolbar.set( 'search', new media.view.Search({ 3304 controller: this.controller, 3305 model: this.collection.props, 3306 priority: 60 3307 }).render() ); 3308 } 3309 3310 if ( this.options.dragInfo ) { 3311 this.toolbar.set( 'dragInfo', new media.View({ 3312 el: $( '<div class="instructions">' + l10n.dragInfo + '</div>' )[0], 3313 priority: -40 3314 }) ); 3315 } 3316 }, 3317 3318 updateContent: function() { 3319 var view = this; 3320 3321 if( ! this.attachments ) 3322 this.createAttachments(); 3323 3324 if ( ! this.collection.length ) { 3325 this.collection.more().done( function() { 3326 if ( ! view.collection.length ) 3327 view.createUploader(); 3328 }); 3329 } 3330 }, 3331 3332 removeContent: function() { 3333 _.each(['attachments','uploader'], function( key ) { 3334 if ( this[ key ] ) { 3335 this[ key ].remove(); 3336 delete this[ key ]; 3337 } 3338 }, this ); 3339 }, 3340 3341 createUploader: function() { 3342 this.removeContent(); 3343 3344 this.uploader = new media.view.UploaderInline({ 3345 controller: this.controller, 3346 status: false, 3347 message: l10n.noItemsFound 3348 }); 3349 3350 this.views.add( this.uploader ); 3351 }, 3352 3353 createAttachments: function() { 3354 this.removeContent(); 3355 3356 this.attachments = new media.view.Attachments({ 3357 controller: this.controller, 3358 collection: this.collection, 3359 selection: this.options.selection, 3360 model: this.model, 3361 sortable: this.options.sortable, 3362 3363 // The single `Attachment` view to be used in the `Attachments` view. 3364 AttachmentView: this.options.AttachmentView 3365 }); 3366 3367 this.views.add( this.attachments ); 3368 }, 3369 3370 createSidebar: function() { 3371 var options = this.options, 3372 selection = options.selection, 3373 sidebar = this.sidebar = new media.view.Sidebar({ 3374 controller: this.controller 3375 }); 3376 3377 this.views.add( sidebar ); 3378 3379 if ( this.controller.uploader ) { 3380 sidebar.set( 'uploads', new media.view.UploaderStatus({ 3381 controller: this.controller, 3382 priority: 40 3383 }) ); 3384 } 3385 3386 selection.on( 'selection:single', this.createSingle, this ); 3387 selection.on( 'selection:unsingle', this.disposeSingle, this ); 3388 3389 if ( selection.single() ) 3390 this.createSingle(); 3391 }, 3392 3393 createSingle: function() { 3394 var sidebar = this.sidebar, 3395 single = this.options.selection.single(); 3396 3397 sidebar.set( 'details', new media.view.Attachment.Details({ 3398 controller: this.controller, 3399 model: single, 3400 priority: 80 3401 }) ); 3402 3403 sidebar.set( 'compat', new media.view.AttachmentCompat({ 3404 controller: this.controller, 3405 model: single, 3406 priority: 120 3407 }) ); 3408 3409 if ( this.options.display ) { 3410 sidebar.set( 'display', new media.view.Settings.AttachmentDisplay({ 3411 controller: this.controller, 3412 model: this.model.display( single ), 3413 attachment: single, 3414 priority: 160, 3415 userSettings: this.model.get('displayUserSettings') 3416 }) ); 3417 } 3418 }, 3419 3420 disposeSingle: function() { 3421 var sidebar = this.sidebar; 3422 sidebar.unset('details'); 3423 sidebar.unset('compat'); 3424 sidebar.unset('display'); 3425 } 3426 }); 3427 3428 /** 3429 * wp.media.view.Selection 3430 */ 3431 media.view.Selection = media.View.extend({ 3432 tagName: 'div', 3433 className: 'media-selection', 3434 template: media.template('media-selection'), 3435 3436 events: { 3437 'click .edit-selection': 'edit', 3438 'click .clear-selection': 'clear' 3439 }, 3440 3441 initialize: function() { 3442 _.defaults( this.options, { 3443 editable: false, 3444 clearable: true 3445 }); 3446 3447 this.attachments = new media.view.Attachments.Selection({ 3448 controller: this.controller, 3449 collection: this.collection, 3450 selection: this.collection, 3451 model: new Backbone.Model({ 3452 edge: 40, 3453 gutter: 5 3454 }) 3455 }); 3456 3457 this.views.set( '.selection-view', this.attachments ); 3458 this.collection.on( 'add remove reset', this.refresh, this ); 3459 this.controller.on( 'content:activate', this.refresh, this ); 3460 }, 3461 3462 ready: function() { 3463 this.refresh(); 3464 }, 3465 3466 refresh: function() { 3467 // If the selection hasn't been rendered, bail. 3468 if ( ! this.$el.children().length ) 3469 return; 3470 3471 var collection = this.collection, 3472 editing = 'edit-selection' === this.controller.content.mode(); 3473 3474 // If nothing is selected, display nothing. 3475 this.$el.toggleClass( 'empty', ! collection.length ); 3476 this.$el.toggleClass( 'one', 1 === collection.length ); 3477 this.$el.toggleClass( 'editing', editing ); 3478 3479 this.$('.count').text( l10n.selected.replace('%d', collection.length) ); 3480 }, 3481 3482 edit: function( event ) { 3483 event.preventDefault(); 3484 if ( this.options.editable ) 3485 this.options.editable.call( this, this.collection ); 3486 }, 3487 3488 clear: function( event ) { 3489 event.preventDefault(); 3490 this.collection.reset(); 3491 } 3492 }); 3493 3494 3495 /** 3496 * wp.media.view.Attachment.Selection 3497 */ 3498 media.view.Attachment.Selection = media.view.Attachment.extend({ 3499 className: 'attachment selection', 3500 3501 // On click, just select the model, instead of removing the model from 3502 // the selection. 3503 toggleSelection: function() { 3504 this.options.selection.single( this.model ); 3505 } 3506 }); 3507 3508 /** 3509 * wp.media.view.Attachments.Selection 3510 */ 3511 media.view.Attachments.Selection = media.view.Attachments.extend({ 3512 events: {}, 3513 initialize: function() { 3514 _.defaults( this.options, { 3515 sortable: true, 3516 resize: false, 3517 3518 // The single `Attachment` view to be used in the `Attachments` view. 3519 AttachmentView: media.view.Attachment.Selection 3520 }); 3521 return media.view.Attachments.prototype.initialize.apply( this, arguments ); 3522 } 3523 }); 3524 3525 /** 3526 * wp.media.view.Attachments.EditSelection 3527 */ 3528 media.view.Attachment.EditSelection = media.view.Attachment.Selection.extend({ 3529 buttons: { 3530 close: true 3531 } 3532 }); 3533 3534 3535 /** 3536 * wp.media.view.Settings 3537 */ 3538 media.view.Settings = media.View.extend({ 3539 events: { 3540 'click button': 'updateHandler', 3541 'change input': 'updateHandler', 3542 'change select': 'updateHandler', 3543 'change textarea': 'updateHandler' 3544 }, 3545 3546 initialize: function() { 3547 this.model = this.model || new Backbone.Model(); 3548 this.model.on( 'change', this.updateChanges, this ); 3549 }, 3550 3551 prepare: function() { 3552 return _.defaults({ 3553 model: this.model.toJSON() 3554 }, this.options ); 3555 }, 3556 3557 render: function() { 3558 media.View.prototype.render.apply( this, arguments ); 3559 // Select the correct values. 3560 _( this.model.attributes ).chain().keys().each( this.update, this ); 3561 return this; 3562 }, 3563 3564 update: function( key ) { 3565 var value = this.model.get( key ), 3566 $setting = this.$('[data-setting="' + key + '"]'), 3567 $buttons, $value; 3568 3569 // Bail if we didn't find a matching setting. 3570 if ( ! $setting.length ) 3571 return; 3572 3573 // Attempt to determine how the setting is rendered and update 3574 // the selected value. 3575 3576 // Handle dropdowns. 3577 if ( $setting.is('select') ) { 3578 $value = $setting.find('[value="' + value + '"]'); 3579 3580 if ( $value.length ) { 3581 $setting.find('option').prop( 'selected', false ); 3582 $value.prop( 'selected', true ); 3583 } else { 3584 // If we can't find the desired value, record what *is* selected. 3585 this.model.set( key, $setting.find(':selected').val() ); 3586 } 3587 3588 3589 // Handle button groups. 3590 } else if ( $setting.hasClass('button-group') ) { 3591 $buttons = $setting.find('button').removeClass('active'); 3592 $buttons.filter( '[value="' + value + '"]' ).addClass('active'); 3593 3594 // Handle text inputs and textareas. 3595 } else if ( $setting.is('input[type="text"], textarea') ) { 3596 if ( ! $setting.is(':focus') ) 3597 $setting.val( value ); 3598 3599 // Handle checkboxes. 3600 } else if ( $setting.is('input[type="checkbox"]') ) { 3601 $setting.attr( 'checked', !! value ); 3602 } 3603 }, 3604 3605 updateHandler: function( event ) { 3606 var $setting = $( event.target ).closest('[data-setting]'), 3607 value = event.target.value, 3608 userSetting; 3609 3610 event.preventDefault(); 3611 3612 if ( ! $setting.length ) 3613 return; 3614 3615 // Use the correct value for checkboxes. 3616 if ( $setting.is('input[type="checkbox"]') ) 3617 value = $setting[0].checked; 3618 3619 // Update the corresponding setting. 3620 this.model.set( $setting.data('setting'), value ); 3621 3622 // If the setting has a corresponding user setting, 3623 // update that as well. 3624 if ( userSetting = $setting.data('userSetting') ) 3625 setUserSetting( userSetting, value ); 3626 }, 3627 3628 updateChanges: function( model ) { 3629 if ( model.hasChanged() ) 3630 _( model.changed ).chain().keys().each( this.update, this ); 3631 } 3632 }); 3633 3634 /** 3635 * wp.media.view.Settings.AttachmentDisplay 3636 */ 3637 media.view.Settings.AttachmentDisplay = media.view.Settings.extend({ 3638 className: 'attachment-display-settings', 3639 template: media.template('attachment-display-settings'), 3640 3641 initialize: function() { 3642 var attachment = this.options.attachment; 3643 3644 _.defaults( this.options, { 3645 userSettings: false 3646 }); 3647 3648 media.view.Settings.prototype.initialize.apply( this, arguments ); 3649 this.model.on( 'change:link', this.updateLinkTo, this ); 3650 3651 if ( attachment ) 3652 attachment.on( 'change:uploading', this.render, this ); 3653 }, 3654 3655 dispose: function() { 3656 var attachment = this.options.attachment; 3657 if ( attachment ) 3658 attachment.off( null, null, this ); 3659 3660 media.view.Settings.prototype.dispose.apply( this, arguments ); 3661 }, 3662 3663 render: function() { 3664 var attachment = this.options.attachment; 3665 if ( attachment ) { 3666 _.extend( this.options, { 3667 sizes: attachment.get('sizes'), 3668 type: attachment.get('type') 3669 }); 3670 } 3671 3672 media.view.Settings.prototype.render.call( this ); 3673 this.updateLinkTo(); 3674 return this; 3675 }, 3676 3677 updateLinkTo: function() { 3678 var linkTo = this.model.get('link'), 3679 $input = this.$('.link-to-custom'), 3680 attachment = this.options.attachment; 3681 3682 if ( 'none' === linkTo || 'embed' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) { 3683 $input.hide(); 3684 return; 3685 } 3686 3687 if ( attachment ) { 3688 if ( 'post' === linkTo ) { 3689 $input.val( attachment.get('link') ); 3690 } else if ( 'file' === linkTo ) { 3691 $input.val( attachment.get('url') ); 3692 } else if ( ! this.model.get('linkUrl') ) { 3693 $input.val('http://'); 3694 } 3695 3696 $input.prop( 'readonly', 'custom' !== linkTo ); 3697 } 3698 3699 $input.show(); 3700 3701 // If the input is visible, focus and select its contents. 3702 if ( $input.is(':visible') ) 3703 $input.focus()[0].select(); 3704 } 3705 }); 3706 3707 /** 3708 * wp.media.view.Settings.Gallery 3709 */ 3710 media.view.Settings.Gallery = media.view.Settings.extend({ 3711 className: 'gallery-settings', 3712 template: media.template('gallery-settings') 3713 }); 3714 3715 /** 3716 * wp.media.view.Attachment.Details 3717 */ 3718 media.view.Attachment.Details = media.view.Attachment.extend({ 3719 tagName: 'div', 3720 className: 'attachment-details', 3721 template: media.template('attachment-details'), 3722 3723 events: { 3724 'change [data-setting]': 'updateSetting', 3725 'change [data-setting] input': 'updateSetting', 3726 'change [data-setting] select': 'updateSetting', 3727 'change [data-setting] textarea': 'updateSetting', 3728 'click .delete-attachment': 'deleteAttachment', 3729 'click .edit-attachment': 'editAttachment', 3730 'click .refresh-attachment': 'refreshAttachment' 3731 }, 3732 3733 initialize: function() { 3734 this.focusManager = new media.view.FocusManager({ 3735 el: this.el 3736 }); 3737 3738 media.view.Attachment.prototype.initialize.apply( this, arguments ); 3739 }, 3740 3741 render: function() { 3742 media.view.Attachment.prototype.render.apply( this, arguments ); 3743 this.focusManager.focus(); 3744 return this; 3745 }, 3746 3747 deleteAttachment: function( event ) { 3748 event.preventDefault(); 3749 3750 if ( confirm( l10n.warnDelete ) ) 3751 this.model.destroy(); 3752 }, 3753 3754 editAttachment: function() { 3755 this.$el.addClass('needs-refresh'); 3756 }, 3757 3758 refreshAttachment: function( event ) { 3759 this.$el.removeClass('needs-refresh'); 3760 event.preventDefault(); 3761 this.model.fetch(); 3762 } 3763 }); 3764 3765 /** 3766 * wp.media.view.AttachmentCompat 3767 */ 3768 media.view.AttachmentCompat = media.View.extend({ 3769 tagName: 'form', 3770 className: 'compat-item', 3771 3772 events: { 3773 'submit': 'preventDefault', 3774 'change input': 'save', 3775 'change select': 'save', 3776 'change textarea': 'save' 3777 }, 3778 3779 initialize: function() { 3780 this.focusManager = new media.view.FocusManager({ 3781 el: this.el 3782 }); 3783 3784 this.model.on( 'change:compat', this.render, this ); 3785 }, 3786 3787 dispose: function() { 3788 if ( this.$(':focus').length ) 3789 this.save(); 3790 3791 return media.View.prototype.dispose.apply( this, arguments ); 3792 }, 3793 3794 render: function() { 3795 var compat = this.model.get('compat'); 3796 if ( ! compat || ! compat.item ) 3797 return; 3798 3799 this.views.detach(); 3800 this.$el.html( compat.item ); 3801 this.views.render(); 3802 3803 this.focusManager.focus(); 3804 return this; 3805 }, 3806 3807 preventDefault: function( event ) { 3808 event.preventDefault(); 3809 }, 3810 3811 save: function( event ) { 3812 var data = {}; 3813 3814 if ( event ) 3815 event.preventDefault(); 3816 3817 _.each( this.$el.serializeArray(), function( pair ) { 3818 data[ pair.name ] = pair.value; 3819 }); 3820 3821 this.model.saveCompat( data ); 3822 } 3823 }); 3824 3825 /** 3826 * wp.media.view.Iframe 3827 */ 3828 media.view.Iframe = media.View.extend({ 3829 className: 'media-iframe', 3830 3831 render: function() { 3832 this.views.detach(); 3833 this.$el.html( '<iframe src="' + this.controller.state().get('src') + '" />' ); 3834 this.views.render(); 3835 return this; 3836 } 3837 }); 3838 3839 /** 3840 * wp.media.view.Embed 3841 */ 3842 media.view.Embed = media.View.extend({ 3843 className: 'media-embed', 3844 3845 initialize: function() { 3846 this.url = new media.view.EmbedUrl({ 3847 controller: this.controller, 3848 model: this.model.props 3849 }).render(); 3850 3851 this.views.set([ this.url ]); 3852 this.refresh(); 3853 this.model.on( 'change:type', this.refresh, this ); 3854 this.model.on( 'change:loading', this.loading, this ); 3855 }, 3856 3857 settings: function( view ) { 3858 if ( this._settings ) 3859 this._settings.remove(); 3860 this._settings = view; 3861 this.views.add( view ); 3862 }, 3863 3864 refresh: function() { 3865 var type = this.model.get('type'), 3866 constructor; 3867 3868 if ( 'image' === type ) 3869 constructor = media.view.EmbedImage; 3870 else if ( 'link' === type ) 3871 constructor = media.view.EmbedLink; 3872 else 3873 return; 3874 3875 this.settings( new constructor({ 3876 controller: this.controller, 3877 model: this.model.props, 3878 priority: 40 3879 }) ); 3880 }, 3881 3882 loading: function() { 3883 this.$el.toggleClass( 'embed-loading', this.model.get('loading') ); 3884 } 3885 }); 3886 3887 /** 3888 * wp.media.view.EmbedUrl 3889 */ 3890 media.view.EmbedUrl = media.View.extend({ 3891 tagName: 'label', 3892 className: 'embed-url', 3893 3894 events: { 3895 'input': 'url', 3896 'keyup': 'url', 3897 'change': 'url' 3898 }, 3899 3900 initialize: function() { 3901 this.$input = $('<input/>').attr( 'type', 'text' ).val( this.model.get('url') ); 3902 this.input = this.$input[0]; 3903 3904 this.spinner = $('<span class="spinner" />')[0]; 3905 this.$el.append([ this.input, this.spinner ]); 3906 3907 this.model.on( 'change:url', this.render, this ); 3908 }, 3909 3910 render: function() { 3911 var $input = this.$input; 3912 3913 if ( $input.is(':focus') ) 3914 return; 3915 3916 this.input.value = this.model.get('url') || 'http://'; 3917 media.View.prototype.render.apply( this, arguments ); 3918 return this; 3919 }, 3920 3921 ready: function() { 3922 this.focus(); 3923 }, 3924 3925 url: function( event ) { 3926 this.model.set( 'url', event.target.value ); 3927 }, 3928 3929 focus: function() { 3930 var $input = this.$input; 3931 // If the input is visible, focus and select its contents. 3932 if ( $input.is(':visible') ) 3933 $input.focus()[0].select(); 3934 } 3935 }); 3936 3937 /** 3938 * wp.media.view.EmbedLink 3939 */ 3940 media.view.EmbedLink = media.view.Settings.extend({ 3941 className: 'embed-link-settings', 3942 template: media.template('embed-link-settings') 3943 }); 3944 3945 /** 3946 * wp.media.view.EmbedImage 3947 */ 3948 media.view.EmbedImage = media.view.Settings.AttachmentDisplay.extend({ 3949 className: 'embed-image-settings', 3950 template: media.template('embed-image-settings'), 3951 3952 initialize: function() { 3953 media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments ); 3954 this.model.on( 'change:url', this.updateImage, this ); 3955 }, 3956 3957 updateImage: function() { 3958 this.$('img').attr( 'src', this.model.get('url') ); 3959 } 3960 }); 3961 }(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 |