| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 /** 2 * @output wp-admin/js/widgets/media-widgets.js 3 */ 4 5 /* eslint consistent-this: [ "error", "control" ] */ 6 7 /** 8 * @namespace wp.mediaWidgets 9 * @memberOf wp 10 * 11 * @param {JQueryStatic} $ The jQuery object. 12 */ 13 wp.mediaWidgets = ( function( $ ) { 14 'use strict'; 15 16 var component = {}; 17 18 /** 19 * Widget control (view) constructors, mapping widget id_base to subclass of MediaWidgetControl. 20 * 21 * Media widgets register themselves by assigning subclasses of MediaWidgetControl onto this object by widget ID base. 22 * 23 * @memberOf wp.mediaWidgets 24 * 25 * @type {Object.<string, wp.mediaWidgets.MediaWidgetModel>} 26 */ 27 component.controlConstructors = {}; 28 29 /** 30 * Widget model constructors, mapping widget id_base to subclass of MediaWidgetModel. 31 * 32 * Media widgets register themselves by assigning subclasses of MediaWidgetControl onto this object by widget ID base. 33 * 34 * @memberOf wp.mediaWidgets 35 * 36 * @type {Object.<string, wp.mediaWidgets.MediaWidgetModel>} 37 */ 38 component.modelConstructors = {}; 39 40 component.PersistentDisplaySettingsLibrary = wp.media.controller.Library.extend(/** @lends wp.mediaWidgets.PersistentDisplaySettingsLibrary.prototype */{ 41 42 /** 43 * Library which persists the customized display settings across selections. 44 * 45 * @constructs wp.mediaWidgets.PersistentDisplaySettingsLibrary 46 * @augments wp.media.controller.Library 47 * 48 * @param {Object} options - Options. 49 * 50 * @return {void} 51 */ 52 initialize: function initialize( options ) { 53 _.bindAll( this, 'handleDisplaySettingChange' ); 54 wp.media.controller.Library.prototype.initialize.call( this, options ); 55 }, 56 57 /** 58 * Sync changes to the current display settings back into the current customized. 59 * 60 * @param {Backbone.Model} displaySettings - Modified display settings. 61 * @return {void} 62 */ 63 handleDisplaySettingChange: function handleDisplaySettingChange( displaySettings ) { 64 this.get( 'selectedDisplaySettings' ).set( displaySettings.attributes ); 65 }, 66 67 /** 68 * Get the display settings model. 69 * 70 * Model returned is updated with the current customized display settings, 71 * and an event listener is added so that changes made to the settings 72 * will sync back into the model storing the session's customized display 73 * settings. 74 * 75 * @param {Backbone.Model} model - Display settings model. 76 * @return {Backbone.Model} Display settings model. 77 */ 78 display: function getDisplaySettingsModel( model ) { 79 var display, selectedDisplaySettings = this.get( 'selectedDisplaySettings' ); 80 display = wp.media.controller.Library.prototype.display.call( this, model ); 81 82 display.off( 'change', this.handleDisplaySettingChange ); // Prevent duplicated event handlers. 83 display.set( selectedDisplaySettings.attributes ); 84 if ( 'custom' === selectedDisplaySettings.get( 'link_type' ) ) { 85 display.linkUrl = selectedDisplaySettings.get( 'link_url' ); 86 } 87 display.on( 'change', this.handleDisplaySettingChange ); 88 return display; 89 } 90 }); 91 92 /** 93 * Extended view for managing the embed UI. 94 * 95 * @class wp.mediaWidgets.MediaEmbedView 96 * @augments wp.media.view.Embed 97 */ 98 component.MediaEmbedView = wp.media.view.Embed.extend(/** @lends wp.mediaWidgets.MediaEmbedView.prototype */{ 99 100 /** 101 * Initialize. 102 * 103 * @since 4.9.0 104 * 105 * @param {Object} options - Options. 106 * @return {void} 107 */ 108 initialize: function( options ) { 109 var view = this, embedController; // eslint-disable-line consistent-this 110 wp.media.view.Embed.prototype.initialize.call( view, options ); 111 if ( 'image' !== view.controller.options.mimeType ) { 112 embedController = view.controller.states.get( 'embed' ); 113 embedController.off( 'scan', embedController.scanImage, embedController ); 114 } 115 }, 116 117 /** 118 * Refresh embed view. 119 * 120 * Forked override of {wp.media.view.Embed#refresh()} to suppress irrelevant "link text" field. 121 * 122 * @return {void} 123 */ 124 refresh: function refresh() { 125 /** 126 * @class wp.mediaWidgets~Constructor 127 */ 128 var Constructor; 129 130 if ( 'image' === this.controller.options.mimeType ) { 131 Constructor = wp.media.view.EmbedImage; 132 } else { 133 134 // This should be eliminated once #40450 lands of when this is merged into core. 135 Constructor = wp.media.view.EmbedLink.extend(/** @lends wp.mediaWidgets~Constructor.prototype */{ 136 137 /** 138 * Set the disabled state on the Add to Widget button. 139 * 140 * @param {boolean} disabled - Disabled. 141 * @return {void} 142 */ 143 setAddToWidgetButtonDisabled: function setAddToWidgetButtonDisabled( disabled ) { 144 this.views.parent.views.parent.views.get( '.media-frame-toolbar' )[0].$el.find( '.media-button-select' ).prop( 'disabled', disabled ); 145 }, 146 147 /** 148 * Set or clear an error notice. 149 * 150 * @param {string} notice - Notice. 151 * @return {void} 152 */ 153 setErrorNotice: function setErrorNotice( notice ) { 154 var embedLinkView = this, noticeContainer; // eslint-disable-line consistent-this 155 156 noticeContainer = embedLinkView.views.parent.$el.find( '> .notice:first-child' ); 157 if ( ! notice ) { 158 if ( noticeContainer.length ) { 159 noticeContainer.slideUp( 'fast' ); 160 } 161 } else { 162 if ( ! noticeContainer.length ) { 163 noticeContainer = $( '<div class="media-widget-embed-notice notice notice-error notice-alt" role="alert"></div>' ); 164 noticeContainer.hide(); 165 embedLinkView.views.parent.$el.prepend( noticeContainer ); 166 } 167 noticeContainer.empty(); 168 noticeContainer.append( $( '<p>', { 169 html: notice 170 })); 171 noticeContainer.slideDown( 'fast' ); 172 } 173 }, 174 175 /** 176 * Update oEmbed. 177 * 178 * @since 4.9.0 179 * 180 * @return {void} 181 */ 182 updateoEmbed: function() { 183 var embedLinkView = this, url; // eslint-disable-line consistent-this 184 185 url = embedLinkView.model.get( 'url' ); 186 187 // Abort if the URL field was emptied out. 188 if ( ! url ) { 189 embedLinkView.setErrorNotice( '' ); 190 embedLinkView.setAddToWidgetButtonDisabled( true ); 191 return; 192 } 193 194 if ( ! url.match( /^(http|https):\/\/.+\// ) ) { 195 embedLinkView.controller.$el.find( '#embed-url-field' ).addClass( 'invalid' ); 196 embedLinkView.setAddToWidgetButtonDisabled( true ); 197 } 198 199 wp.media.view.EmbedLink.prototype.updateoEmbed.call( embedLinkView ); 200 }, 201 202 /** 203 * Fetch media. 204 * 205 * @return {void} 206 */ 207 fetch: function() { 208 var embedLinkView = this, fetchSuccess, matches, fileExt, urlParser, url, re, youTubeEmbedMatch; // eslint-disable-line consistent-this 209 url = embedLinkView.model.get( 'url' ); 210 211 if ( embedLinkView.dfd && 'pending' === embedLinkView.dfd.state() ) { 212 embedLinkView.dfd.abort(); 213 } 214 215 fetchSuccess = function( response ) { 216 embedLinkView.renderoEmbed({ 217 data: { 218 body: response 219 } 220 }); 221 222 embedLinkView.controller.$el.find( '#embed-url-field' ).removeClass( 'invalid' ); 223 embedLinkView.setErrorNotice( '' ); 224 embedLinkView.setAddToWidgetButtonDisabled( false ); 225 }; 226 227 urlParser = document.createElement( 'a' ); 228 urlParser.href = url; 229 matches = urlParser.pathname.toLowerCase().match( /\.(\w+)$/ ); 230 if ( matches ) { 231 fileExt = matches[1]; 232 if ( ! wp.media.view.settings.embedMimes[ fileExt ] ) { 233 embedLinkView.renderFail(); 234 } else if ( 0 !== wp.media.view.settings.embedMimes[ fileExt ].indexOf( embedLinkView.controller.options.mimeType ) ) { 235 embedLinkView.renderFail(); 236 } else { 237 fetchSuccess( '<!--success-->' ); 238 } 239 return; 240 } 241 242 // Support YouTube embed links. 243 re = /https?:\/\/www\.youtube\.com\/embed\/([^/]+)/; 244 youTubeEmbedMatch = re.exec( url ); 245 if ( youTubeEmbedMatch ) { 246 url = 'https://www.youtube.com/watch?v=' + youTubeEmbedMatch[ 1 ]; 247 // silently change url to proper oembed-able version. 248 embedLinkView.model.attributes.url = url; 249 } 250 251 embedLinkView.dfd = wp.apiRequest({ 252 url: wp.media.view.settings.oEmbedProxyUrl, 253 data: { 254 url: url, 255 maxwidth: embedLinkView.model.get( 'width' ), 256 maxheight: embedLinkView.model.get( 'height' ), 257 discover: false 258 }, 259 type: 'GET', 260 dataType: 'json', 261 context: embedLinkView 262 }); 263 264 embedLinkView.dfd.done( function( response ) { 265 if ( embedLinkView.controller.options.mimeType !== response.type ) { 266 embedLinkView.renderFail(); 267 return; 268 } 269 fetchSuccess( response.html ); 270 }); 271 embedLinkView.dfd.fail( _.bind( embedLinkView.renderFail, embedLinkView ) ); 272 }, 273 274 /** 275 * Handle render failure. 276 * 277 * Overrides the {EmbedLink#renderFail()} method to prevent showing the "Link Text" field. 278 * The element is getting display:none in the stylesheet, but the underlying method uses 279 * uses {jQuery.fn.show()} which adds an inline style. This avoids the need for !important. 280 * 281 * @return {void} 282 */ 283 renderFail: function renderFail() { 284 var embedLinkView = this; // eslint-disable-line consistent-this 285 embedLinkView.controller.$el.find( '#embed-url-field' ).addClass( 'invalid' ); 286 embedLinkView.setErrorNotice( embedLinkView.controller.options.invalidEmbedTypeError || 'ERROR' ); 287 embedLinkView.setAddToWidgetButtonDisabled( true ); 288 } 289 }); 290 } 291 292 this.settings( new Constructor({ 293 controller: this.controller, 294 model: this.model.props, 295 priority: 40 296 })); 297 } 298 }); 299 300 /** 301 * Custom media frame for selecting uploaded media or providing media by URL. 302 * 303 * @class wp.mediaWidgets.MediaFrameSelect 304 * @augments wp.media.view.MediaFrame.Post 305 */ 306 component.MediaFrameSelect = wp.media.view.MediaFrame.Post.extend(/** @lends wp.mediaWidgets.MediaFrameSelect.prototype */{ 307 308 /** 309 * Create the default states. 310 * 311 * @return {void} 312 */ 313 createStates: function createStates() { 314 var mime = this.options.mimeType, specificMimes = []; 315 _.each( wp.media.view.settings.embedMimes, function( embedMime ) { 316 if ( 0 === embedMime.indexOf( mime ) ) { 317 specificMimes.push( embedMime ); 318 } 319 }); 320 if ( specificMimes.length > 0 ) { 321 mime = specificMimes; 322 } 323 324 this.states.add([ 325 326 // Main states. 327 new component.PersistentDisplaySettingsLibrary({ 328 id: 'insert', 329 title: this.options.title, 330 selection: this.options.selection, 331 priority: 20, 332 toolbar: 'main-insert', 333 filterable: 'dates', 334 library: wp.media.query({ 335 type: mime 336 }), 337 multiple: false, 338 editable: true, 339 340 selectedDisplaySettings: this.options.selectedDisplaySettings, 341 displaySettings: _.isUndefined( this.options.showDisplaySettings ) ? true : this.options.showDisplaySettings, 342 displayUserSettings: false // We use the display settings from the current/default widget instance props. 343 }), 344 345 new wp.media.controller.EditImage({ model: this.options.editImage }), 346 347 // Embed states. 348 new wp.media.controller.Embed({ 349 metadata: this.options.metadata, 350 type: 'image' === this.options.mimeType ? 'image' : 'link', 351 invalidEmbedTypeError: this.options.invalidEmbedTypeError 352 }) 353 ]); 354 }, 355 356 /** 357 * Main insert toolbar. 358 * 359 * Forked override of {wp.media.view.MediaFrame.Post#mainInsertToolbar()} to override text. 360 * 361 * @param {wp.Backbone.View} view - Toolbar view. 362 * @this {wp.media.controller.Library} 363 * @return {void} 364 */ 365 mainInsertToolbar: function mainInsertToolbar( view ) { 366 var controller = this; // eslint-disable-line consistent-this 367 view.set( 'insert', { 368 style: 'primary', 369 priority: 80, 370 text: controller.options.text, // The whole reason for the fork. 371 requires: { selection: true }, 372 373 /** 374 * Handle click. 375 * 376 * @ignore 377 * 378 * @fires wp.media.controller.State#insert() 379 * @return {void} 380 */ 381 click: function onClick() { 382 var state = controller.state(), 383 selection = state.get( 'selection' ); 384 385 controller.close(); 386 state.trigger( 'insert', selection ).reset(); 387 } 388 }); 389 }, 390 391 /** 392 * Main embed toolbar. 393 * 394 * Forked override of {wp.media.view.MediaFrame.Post#mainEmbedToolbar()} to override text. 395 * 396 * @param {wp.Backbone.View} toolbar - Toolbar view. 397 * @this {wp.media.controller.Library} 398 * @return {void} 399 */ 400 mainEmbedToolbar: function mainEmbedToolbar( toolbar ) { 401 toolbar.view = new wp.media.view.Toolbar.Embed({ 402 controller: this, 403 text: this.options.text, 404 event: 'insert' 405 }); 406 }, 407 408 /** 409 * Embed content. 410 * 411 * Forked override of {wp.media.view.MediaFrame.Post#embedContent()} to suppress irrelevant "link text" field. 412 * 413 * @return {void} 414 */ 415 embedContent: function embedContent() { 416 var view = new component.MediaEmbedView({ 417 controller: this, 418 model: this.state() 419 }).render(); 420 421 this.content.set( view ); 422 } 423 }); 424 425 component.MediaWidgetControl = Backbone.View.extend(/** @lends wp.mediaWidgets.MediaWidgetControl.prototype */{ 426 427 /** 428 * Translation strings. 429 * 430 * The mapping of translation strings is handled by media widget subclasses, 431 * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts(). 432 * 433 * @type {Object} 434 */ 435 l10n: { 436 add_to_widget: '{{add_to_widget}}', 437 add_media: '{{add_media}}' 438 }, 439 440 /** 441 * Widget ID base. 442 * 443 * This may be defined by the subclass. It may be exported from PHP to JS 444 * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts(). If not, 445 * it will attempt to be discovered by looking to see if this control 446 * instance extends each member of component.controlConstructors, and if 447 * it does extend one, will use the key as the id_base. 448 * 449 * @type {string} 450 */ 451 id_base: '', 452 453 /** 454 * Mime type. 455 * 456 * This must be defined by the subclass. It may be exported from PHP to JS 457 * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts(). 458 * 459 * @type {string} 460 */ 461 mime_type: '', 462 463 /** 464 * View events. 465 * 466 * @type {Object} 467 */ 468 events: { 469 'click .notice-missing-attachment a': 'handleMediaLibraryLinkClick', 470 'click .select-media': 'selectMedia', 471 'click .placeholder': 'selectMedia', 472 'click .edit-media': 'editMedia' 473 }, 474 475 /** 476 * Show display settings. 477 * 478 * @type {boolean} 479 */ 480 showDisplaySettings: true, 481 482 /** 483 * Media Widget Control. 484 * 485 * @constructs wp.mediaWidgets.MediaWidgetControl 486 * @augments Backbone.View 487 * @abstract 488 * 489 * @param {Object} options - Options. 490 * @param {Backbone.Model} options.model - Model. 491 * @param {jQuery} options.el - Control field container element. 492 * @param {jQuery} options.syncContainer - Container element where fields are synced for the server. 493 * 494 * @return {void} 495 */ 496 initialize: function initialize( options ) { 497 var control = this; 498 499 Backbone.View.prototype.initialize.call( control, options ); 500 501 if ( ! ( control.model instanceof component.MediaWidgetModel ) ) { 502 throw new Error( 'Missing options.model' ); 503 } 504 if ( ! options.el ) { 505 throw new Error( 'Missing options.el' ); 506 } 507 if ( ! options.syncContainer ) { 508 throw new Error( 'Missing options.syncContainer' ); 509 } 510 511 control.syncContainer = options.syncContainer; 512 513 control.$el.addClass( 'media-widget-control' ); 514 515 // Allow methods to be passed in with control context preserved. 516 _.bindAll( control, 'syncModelToInputs', 'render', 'updateSelectedAttachment', 'renderPreview' ); 517 518 if ( ! control.id_base ) { 519 _.find( component.controlConstructors, function( Constructor, idBase ) { 520 if ( control instanceof Constructor ) { 521 control.id_base = idBase; 522 return true; 523 } 524 return false; 525 }); 526 if ( ! control.id_base ) { 527 throw new Error( 'Missing id_base.' ); 528 } 529 } 530 531 // Track attributes needed to renderPreview in it's own model. 532 control.previewTemplateProps = new Backbone.Model( control.mapModelToPreviewTemplateProps() ); 533 534 // Re-render the preview when the attachment changes. 535 control.selectedAttachment = new wp.media.model.Attachment(); 536 control.renderPreview = _.debounce( control.renderPreview ); 537 control.listenTo( control.previewTemplateProps, 'change', control.renderPreview ); 538 539 // Make sure a copy of the selected attachment is always fetched. 540 control.model.on( 'change:attachment_id', control.updateSelectedAttachment ); 541 control.model.on( 'change:url', control.updateSelectedAttachment ); 542 control.updateSelectedAttachment(); 543 544 /* 545 * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state. 546 * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model 547 * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>. 548 */ 549 control.listenTo( control.model, 'change', control.syncModelToInputs ); 550 control.listenTo( control.model, 'change', control.syncModelToPreviewProps ); 551 control.listenTo( control.model, 'change', control.render ); 552 553 // Update the title. 554 control.$el.on( 'input change', '.title', function updateTitle() { 555 control.model.set({ 556 title: $( this ).val().trim() 557 }); 558 }); 559 560 // Update link_url attribute. 561 control.$el.on( 'input change', '.link', function updateLinkUrl() { 562 var linkUrl = $( this ).val().trim(), linkType = 'custom'; 563 if ( control.selectedAttachment.get( 'linkUrl' ) === linkUrl || control.selectedAttachment.get( 'link' ) === linkUrl ) { 564 linkType = 'post'; 565 } else if ( control.selectedAttachment.get( 'url' ) === linkUrl ) { 566 linkType = 'file'; 567 } 568 control.model.set( { 569 link_url: linkUrl, 570 link_type: linkType 571 }); 572 573 // Update display settings for the next time the user opens to select from the media library. 574 control.displaySettings.set( { 575 link: linkType, 576 linkUrl: linkUrl 577 }); 578 }); 579 580 /* 581 * Copy current display settings from the widget model to serve as basis 582 * of customized display settings for the current media frame session. 583 * Changes to display settings will be synced into this model, and 584 * when a new selection is made, the settings from this will be synced 585 * into that AttachmentDisplay's model to persist the setting changes. 586 */ 587 control.displaySettings = new Backbone.Model( _.pick( 588 control.mapModelToMediaFrameProps( 589 _.extend( control.model.defaults(), control.model.toJSON() ) 590 ), 591 _.keys( wp.media.view.settings.defaultProps ) 592 ) ); 593 }, 594 595 /** 596 * Update the selected attachment if necessary. 597 * 598 * @return {void} 599 */ 600 updateSelectedAttachment: function updateSelectedAttachment() { 601 var control = this, attachment; 602 603 if ( 0 === control.model.get( 'attachment_id' ) ) { 604 control.selectedAttachment.clear(); 605 control.model.set( 'error', false ); 606 } else if ( control.model.get( 'attachment_id' ) !== control.selectedAttachment.get( 'id' ) ) { 607 attachment = new wp.media.model.Attachment({ 608 id: control.model.get( 'attachment_id' ) 609 }); 610 attachment.fetch() 611 .done( function done() { 612 control.model.set( 'error', false ); 613 control.selectedAttachment.set( attachment.toJSON() ); 614 }) 615 .fail( function fail() { 616 control.model.set( 'error', 'missing_attachment' ); 617 }); 618 } 619 }, 620 621 /** 622 * Sync the model attributes to the hidden inputs, and update previewTemplateProps. 623 * 624 * @return {void} 625 */ 626 syncModelToPreviewProps: function syncModelToPreviewProps() { 627 var control = this; 628 control.previewTemplateProps.set( control.mapModelToPreviewTemplateProps() ); 629 }, 630 631 /** 632 * Sync the model attributes to the hidden inputs, and update previewTemplateProps. 633 * 634 * @return {void} 635 */ 636 syncModelToInputs: function syncModelToInputs() { 637 var control = this; 638 control.syncContainer.find( '.media-widget-instance-property' ).each( function() { 639 var input = $( this ), value, propertyName; 640 propertyName = input.data( 'property' ); 641 value = control.model.get( propertyName ); 642 if ( _.isUndefined( value ) ) { 643 return; 644 } 645 646 if ( 'array' === control.model.schema[ propertyName ].type && _.isArray( value ) ) { 647 value = value.join( ',' ); 648 } else if ( 'boolean' === control.model.schema[ propertyName ].type ) { 649 value = value ? '1' : ''; // Because in PHP, strval( true ) === '1' && strval( false ) === ''. 650 } else { 651 value = String( value ); 652 } 653 654 if ( input.val() !== value ) { 655 input.val( value ); 656 input.trigger( 'change' ); 657 } 658 }); 659 }, 660 661 /** 662 * Get template. 663 * 664 * @return {Function} Template. 665 */ 666 template: function template() { 667 var control = this; 668 if ( ! $( '#tmpl-widget-media-' + control.id_base + '-control' ).length ) { 669 throw new Error( 'Missing widget control template for ' + control.id_base ); 670 } 671 return wp.template( 'widget-media-' + control.id_base + '-control' ); 672 }, 673 674 /** 675 * Render template. 676 * 677 * @return {void} 678 */ 679 render: function render() { 680 var control = this, titleInput; 681 682 if ( ! control.templateRendered ) { 683 control.$el.html( control.template()( control.model.toJSON() ) ); 684 control.renderPreview(); // Hereafter it will re-render when control.selectedAttachment changes. 685 control.templateRendered = true; 686 } 687 688 titleInput = control.$el.find( '.title' ); 689 if ( ! titleInput.is( document.activeElement ) ) { 690 titleInput.val( control.model.get( 'title' ) ); 691 } 692 693 control.$el.toggleClass( 'selected', control.isSelected() ); 694 }, 695 696 /** 697 * Render media preview. 698 * 699 * @abstract 700 * @return {void} 701 */ 702 renderPreview: function renderPreview() { 703 throw new Error( 'renderPreview must be implemented' ); 704 }, 705 706 /** 707 * Whether a media item is selected. 708 * 709 * @return {boolean} Whether selected and no error. 710 */ 711 isSelected: function isSelected() { 712 var control = this; 713 714 if ( control.model.get( 'error' ) ) { 715 return false; 716 } 717 718 return Boolean( control.model.get( 'attachment_id' ) || control.model.get( 'url' ) ); 719 }, 720 721 /** 722 * Handle click on link to Media Library to open modal, such as the link that appears when in the missing attachment error notice. 723 * 724 * @param {jQuery.Event} event - Event. 725 * @return {void} 726 */ 727 handleMediaLibraryLinkClick: function handleMediaLibraryLinkClick( event ) { 728 var control = this; 729 event.preventDefault(); 730 control.selectMedia(); 731 }, 732 733 /** 734 * Open the media select frame to chose an item. 735 * 736 * @return {void} 737 */ 738 selectMedia: function selectMedia() { 739 var control = this, selection, mediaFrame, defaultSync, mediaFrameProps, selectionModels = []; 740 741 if ( control.isSelected() && 0 !== control.model.get( 'attachment_id' ) ) { 742 selectionModels.push( control.selectedAttachment ); 743 } 744 745 selection = new wp.media.model.Selection( selectionModels, { multiple: false } ); 746 747 mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() ); 748 if ( mediaFrameProps.size ) { 749 control.displaySettings.set( 'size', mediaFrameProps.size ); 750 } 751 752 mediaFrame = new component.MediaFrameSelect({ 753 title: control.l10n.add_media, 754 frame: 'post', 755 text: control.l10n.add_to_widget, 756 selection: selection, 757 mimeType: control.mime_type, 758 selectedDisplaySettings: control.displaySettings, 759 showDisplaySettings: control.showDisplaySettings, 760 metadata: mediaFrameProps, 761 state: control.isSelected() && 0 === control.model.get( 'attachment_id' ) ? 'embed' : 'insert', 762 invalidEmbedTypeError: control.l10n.unsupported_file_type 763 }); 764 wp.media.frame = mediaFrame; // See wp.media(). 765 766 // Handle selection of a media item. 767 mediaFrame.on( 'insert', function onInsert() { 768 var attachment = {}, state = mediaFrame.state(); 769 770 // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview. 771 if ( 'embed' === state.get( 'id' ) ) { 772 _.extend( attachment, { id: 0 }, state.props.toJSON() ); 773 } else { 774 _.extend( attachment, state.get( 'selection' ).first().toJSON() ); 775 } 776 777 control.selectedAttachment.set( attachment ); 778 control.model.set( 'error', false ); 779 780 // Update widget instance. 781 control.model.set( control.getModelPropsFromMediaFrame( mediaFrame ) ); 782 }); 783 784 // Disable syncing of attachment changes back to server (except for deletions). See <https://core.trac.wordpress.org/ticket/40403>. 785 defaultSync = wp.media.model.Attachment.prototype.sync; 786 wp.media.model.Attachment.prototype.sync = function( method ) { 787 if ( 'delete' === method ) { 788 return defaultSync.apply( this, arguments ); 789 } else { 790 return $.Deferred().rejectWith( this ).promise(); 791 } 792 }; 793 mediaFrame.on( 'close', function onClose() { 794 wp.media.model.Attachment.prototype.sync = defaultSync; 795 }); 796 797 mediaFrame.$el.addClass( 'media-widget' ); 798 mediaFrame.open(); 799 800 // Clear the selected attachment when it is deleted in the media select frame. 801 if ( selection ) { 802 selection.on( 'destroy', function onDestroy( attachment ) { 803 if ( control.model.get( 'attachment_id' ) === attachment.get( 'id' ) ) { 804 control.model.set({ 805 attachment_id: 0, 806 url: '' 807 }); 808 } 809 }); 810 } 811 812 /* 813 * Make sure focus is set inside of modal so that hitting Esc will close 814 * the modal and not inadvertently cause the widget to collapse in the customizer. 815 */ 816 mediaFrame.$el.find( '.media-frame-menu .media-menu-item.active' ).focus(); 817 }, 818 819 /** 820 * Get the instance props from the media selection frame. 821 * 822 * @param {wp.media.view.MediaFrame.Select} mediaFrame - Select frame. 823 * @return {Object} Props. 824 */ 825 getModelPropsFromMediaFrame: function getModelPropsFromMediaFrame( mediaFrame ) { 826 var control = this, state, mediaFrameProps, modelProps; 827 828 state = mediaFrame.state(); 829 if ( 'insert' === state.get( 'id' ) ) { 830 mediaFrameProps = state.get( 'selection' ).first().toJSON(); 831 mediaFrameProps.postUrl = mediaFrameProps.link; 832 833 if ( control.showDisplaySettings ) { 834 _.extend( 835 mediaFrameProps, 836 mediaFrame.content.get( '.attachments-browser' ).sidebar.get( 'display' ).model.toJSON() 837 ); 838 } 839 if ( mediaFrameProps.sizes && mediaFrameProps.size && mediaFrameProps.sizes[ mediaFrameProps.size ] ) { 840 mediaFrameProps.url = mediaFrameProps.sizes[ mediaFrameProps.size ].url; 841 } 842 } else if ( 'embed' === state.get( 'id' ) ) { 843 mediaFrameProps = _.extend( 844 state.props.toJSON(), 845 { attachment_id: 0 }, // Because some media frames use `attachment_id` not `id`. 846 control.model.getEmbedResetProps() 847 ); 848 } else { 849 throw new Error( 'Unexpected state: ' + state.get( 'id' ) ); 850 } 851 852 if ( mediaFrameProps.id ) { 853 mediaFrameProps.attachment_id = mediaFrameProps.id; 854 } 855 856 modelProps = control.mapMediaToModelProps( mediaFrameProps ); 857 858 // Clear the extension prop so sources will be reset for video and audio media. 859 _.each( wp.media.view.settings.embedExts, function( ext ) { 860 if ( ext in control.model.schema && modelProps.url !== modelProps[ ext ] ) { 861 modelProps[ ext ] = ''; 862 } 863 }); 864 865 return modelProps; 866 }, 867 868 /** 869 * Map media frame props to model props. 870 * 871 * @param {Object} mediaFrameProps - Media frame props. 872 * @return {Object} Model props. 873 */ 874 mapMediaToModelProps: function mapMediaToModelProps( mediaFrameProps ) { 875 var control = this, mediaFramePropToModelPropMap = {}, modelProps = {}, extension; 876 _.each( control.model.schema, function( fieldSchema, modelProp ) { 877 878 // Ignore widget title attribute. 879 if ( 'title' === modelProp ) { 880 return; 881 } 882 mediaFramePropToModelPropMap[ fieldSchema.media_prop || modelProp ] = modelProp; 883 }); 884 885 _.each( mediaFrameProps, function( value, mediaProp ) { 886 var propName = mediaFramePropToModelPropMap[ mediaProp ] || mediaProp; 887 if ( control.model.schema[ propName ] ) { 888 modelProps[ propName ] = value; 889 } 890 }); 891 892 if ( 'custom' === mediaFrameProps.size ) { 893 modelProps.width = mediaFrameProps.customWidth; 894 modelProps.height = mediaFrameProps.customHeight; 895 } 896 897 if ( 'post' === mediaFrameProps.link ) { 898 modelProps.link_url = mediaFrameProps.postUrl || mediaFrameProps.linkUrl; 899 } else if ( 'file' === mediaFrameProps.link ) { 900 modelProps.link_url = mediaFrameProps.url; 901 } 902 903 // Because some media frames use `id` instead of `attachment_id`. 904 if ( ! mediaFrameProps.attachment_id && mediaFrameProps.id ) { 905 modelProps.attachment_id = mediaFrameProps.id; 906 } 907 908 if ( mediaFrameProps.url ) { 909 extension = mediaFrameProps.url.replace( /#.*$/, '' ).replace( /\?.*$/, '' ).split( '.' ).pop().toLowerCase(); 910 if ( extension in control.model.schema ) { 911 modelProps[ extension ] = mediaFrameProps.url; 912 } 913 } 914 915 // Always omit the titles derived from mediaFrameProps. 916 return _.omit( modelProps, 'title' ); 917 }, 918 919 /** 920 * Map model props to media frame props. 921 * 922 * @param {Object} modelProps - Model props. 923 * @return {Object} Media frame props. 924 */ 925 mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) { 926 var control = this, mediaFrameProps = {}; 927 928 _.each( modelProps, function( value, modelProp ) { 929 var fieldSchema = control.model.schema[ modelProp ] || {}; 930 mediaFrameProps[ fieldSchema.media_prop || modelProp ] = value; 931 }); 932 933 // Some media frames use attachment_id. 934 mediaFrameProps.attachment_id = mediaFrameProps.id; 935 936 if ( 'custom' === mediaFrameProps.size ) { 937 mediaFrameProps.customWidth = control.model.get( 'width' ); 938 mediaFrameProps.customHeight = control.model.get( 'height' ); 939 } 940 941 return mediaFrameProps; 942 }, 943 944 /** 945 * Map model props to previewTemplateProps. 946 * 947 * @return {Object} Preview Template Props. 948 */ 949 mapModelToPreviewTemplateProps: function mapModelToPreviewTemplateProps() { 950 var control = this, previewTemplateProps = {}; 951 _.each( control.model.schema, function( value, prop ) { 952 if ( ! value.hasOwnProperty( 'should_preview_update' ) || value.should_preview_update ) { 953 previewTemplateProps[ prop ] = control.model.get( prop ); 954 } 955 }); 956 957 // Templates need to be aware of the error. 958 previewTemplateProps.error = control.model.get( 'error' ); 959 return previewTemplateProps; 960 }, 961 962 /** 963 * Open the media frame to modify the selected item. 964 * 965 * @abstract 966 * @return {void} 967 */ 968 editMedia: function editMedia() { 969 throw new Error( 'editMedia not implemented' ); 970 } 971 }); 972 973 /** 974 * Media widget model. 975 * 976 * @class wp.mediaWidgets.MediaWidgetModel 977 * @augments Backbone.Model 978 */ 979 component.MediaWidgetModel = Backbone.Model.extend(/** @lends wp.mediaWidgets.MediaWidgetModel.prototype */{ 980 981 /** 982 * Id attribute. 983 * 984 * @type {string} 985 */ 986 idAttribute: 'widget_id', 987 988 /** 989 * Instance schema. 990 * 991 * This adheres to JSON Schema and subclasses should have their schema 992 * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts(). 993 * 994 * @type {Object.<string, Object>} 995 */ 996 schema: { 997 title: { 998 type: 'string', 999 'default': '' 1000 }, 1001 attachment_id: { 1002 type: 'integer', 1003 'default': 0 1004 }, 1005 url: { 1006 type: 'string', 1007 'default': '' 1008 } 1009 }, 1010 1011 /** 1012 * Get default attribute values. 1013 * 1014 * @return {Object} Mapping of property names to their default values. 1015 */ 1016 defaults: function() { 1017 var defaults = {}; 1018 _.each( this.schema, function( fieldSchema, field ) { 1019 defaults[ field ] = fieldSchema['default']; 1020 }); 1021 return defaults; 1022 }, 1023 1024 /** 1025 * Set attribute value(s). 1026 * 1027 * This is a wrapped version of Backbone.Model#set() which allows us to 1028 * cast the attribute values from the hidden inputs' string values into 1029 * the appropriate data types (integers or booleans). 1030 * 1031 * @param {string|Object} key - Attribute name or attribute pairs. 1032 * @param {mixed|Object} [val] - Attribute value or options object. 1033 * @param {Object} [options] - Options when attribute name and value are passed separately. 1034 * @return {wp.mediaWidgets.MediaWidgetModel} This model. 1035 */ 1036 set: function set( key, val, options ) { 1037 var model = this, attrs, opts, castedAttrs; // eslint-disable-line consistent-this 1038 if ( null === key ) { 1039 return model; 1040 } 1041 if ( 'object' === typeof key ) { 1042 attrs = key; 1043 opts = val; 1044 } else { 1045 attrs = {}; 1046 attrs[ key ] = val; 1047 opts = options; 1048 } 1049 1050 castedAttrs = {}; 1051 _.each( attrs, function( value, name ) { 1052 var type; 1053 if ( ! model.schema[ name ] ) { 1054 castedAttrs[ name ] = value; 1055 return; 1056 } 1057 type = model.schema[ name ].type; 1058 if ( 'array' === type ) { 1059 castedAttrs[ name ] = value; 1060 if ( ! _.isArray( castedAttrs[ name ] ) ) { 1061 castedAttrs[ name ] = castedAttrs[ name ].split( /,/ ); // Good enough for parsing an ID list. 1062 } 1063 if ( model.schema[ name ].items && 'integer' === model.schema[ name ].items.type ) { 1064 castedAttrs[ name ] = _.filter( 1065 _.map( castedAttrs[ name ], function( id ) { 1066 return parseInt( id, 10 ); 1067 }, 1068 function( id ) { 1069 return 'number' === typeof id; 1070 } 1071 ) ); 1072 } 1073 } else if ( 'integer' === type ) { 1074 castedAttrs[ name ] = parseInt( value, 10 ); 1075 } else if ( 'boolean' === type ) { 1076 castedAttrs[ name ] = ! ( ! value || '0' === value || 'false' === value ); 1077 } else { 1078 castedAttrs[ name ] = value; 1079 } 1080 }); 1081 1082 return Backbone.Model.prototype.set.call( this, castedAttrs, opts ); 1083 }, 1084 1085 /** 1086 * Get props which are merged on top of the model when an embed is chosen (as opposed to an attachment). 1087 * 1088 * @return {Object} Reset/override props. 1089 */ 1090 getEmbedResetProps: function getEmbedResetProps() { 1091 return { 1092 id: 0 1093 }; 1094 } 1095 }); 1096 1097 /** 1098 * Collection of all widget model instances. 1099 * 1100 * @memberOf wp.mediaWidgets 1101 * 1102 * @type {Backbone.Collection} 1103 */ 1104 component.modelCollection = new ( Backbone.Collection.extend( { 1105 model: component.MediaWidgetModel 1106 }) )(); 1107 1108 /** 1109 * Mapping of widget ID to instances of MediaWidgetControl subclasses. 1110 * 1111 * @memberOf wp.mediaWidgets 1112 * 1113 * @type {Object.<string, wp.mediaWidgets.MediaWidgetControl>} 1114 */ 1115 component.widgetControls = {}; 1116 1117 /** 1118 * Handle widget being added or initialized for the first time at the widget-added event. 1119 * 1120 * @memberOf wp.mediaWidgets 1121 * 1122 * @param {jQuery.Event} event - Event. 1123 * @param {jQuery} widgetContainer - Widget container element. 1124 * 1125 * @return {void} 1126 */ 1127 component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) { 1128 var fieldContainer, syncContainer, widgetForm, idBase, ControlConstructor, ModelConstructor, modelAttributes, widgetControl, widgetModel, widgetId, animatedCheckDelay = 50, renderWhenAnimationDone; 1129 widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen. 1130 idBase = widgetForm.find( '> .id_base' ).val(); 1131 widgetId = widgetForm.find( '> .widget-id' ).val(); 1132 1133 // Prevent initializing already-added widgets. 1134 if ( component.widgetControls[ widgetId ] ) { 1135 return; 1136 } 1137 1138 ControlConstructor = component.controlConstructors[ idBase ]; 1139 if ( ! ControlConstructor ) { 1140 return; 1141 } 1142 1143 ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel; 1144 1145 /* 1146 * Create a container element for the widget control (Backbone.View). 1147 * This is inserted into the DOM immediately before the .widget-content 1148 * element because the contents of this element are essentially "managed" 1149 * by PHP, where each widget update cause the entire element to be emptied 1150 * and replaced with the rendered output of WP_Widget::form() which is 1151 * sent back in Ajax request made to save/update the widget instance. 1152 * To prevent a "flash of replaced DOM elements and re-initialized JS 1153 * components", the JS template is rendered outside of the normal form 1154 * container. 1155 */ 1156 fieldContainer = $( '<div></div>' ); 1157 syncContainer = widgetContainer.find( '.widget-content:first' ); 1158 syncContainer.before( fieldContainer ); 1159 1160 /* 1161 * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state. 1162 * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model 1163 * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>. 1164 */ 1165 modelAttributes = {}; 1166 syncContainer.find( '.media-widget-instance-property' ).each( function() { 1167 var input = $( this ); 1168 modelAttributes[ input.data( 'property' ) ] = input.val(); 1169 }); 1170 modelAttributes.widget_id = widgetId; 1171 1172 widgetModel = new ModelConstructor( modelAttributes ); 1173 1174 widgetControl = new ControlConstructor({ 1175 el: fieldContainer, 1176 syncContainer: syncContainer, 1177 model: widgetModel 1178 }); 1179 1180 /* 1181 * Render the widget once the widget parent's container finishes animating, 1182 * as the widget-added event fires with a slideDown of the container. 1183 * This ensures that the container's dimensions are fixed so that ME.js 1184 * can initialize with the proper dimensions. 1185 */ 1186 renderWhenAnimationDone = function() { 1187 if ( ! widgetContainer.hasClass( 'open' ) ) { 1188 setTimeout( renderWhenAnimationDone, animatedCheckDelay ); 1189 } else { 1190 widgetControl.render(); 1191 } 1192 }; 1193 renderWhenAnimationDone(); 1194 1195 /* 1196 * Note that the model and control currently won't ever get garbage-collected 1197 * when a widget gets removed/deleted because there is no widget-removed event. 1198 */ 1199 component.modelCollection.add( [ widgetModel ] ); 1200 component.widgetControls[ widgetModel.get( 'widget_id' ) ] = widgetControl; 1201 }; 1202 1203 /** 1204 * Setup widget in accessibility mode. 1205 * 1206 * @memberOf wp.mediaWidgets 1207 * 1208 * @return {void} 1209 */ 1210 component.setupAccessibleMode = function setupAccessibleMode() { 1211 var widgetForm, widgetId, idBase, widgetControl, ControlConstructor, ModelConstructor, modelAttributes, fieldContainer, syncContainer; 1212 widgetForm = $( '.editwidget > form' ); 1213 if ( 0 === widgetForm.length ) { 1214 return; 1215 } 1216 1217 idBase = widgetForm.find( '.id_base' ).val(); 1218 1219 ControlConstructor = component.controlConstructors[ idBase ]; 1220 if ( ! ControlConstructor ) { 1221 return; 1222 } 1223 1224 widgetId = widgetForm.find( '> .widget-control-actions > .widget-id' ).val(); 1225 1226 ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel; 1227 fieldContainer = $( '<div></div>' ); 1228 syncContainer = widgetForm.find( '> .widget-inside' ); 1229 syncContainer.before( fieldContainer ); 1230 1231 modelAttributes = {}; 1232 syncContainer.find( '.media-widget-instance-property' ).each( function() { 1233 var input = $( this ); 1234 modelAttributes[ input.data( 'property' ) ] = input.val(); 1235 }); 1236 modelAttributes.widget_id = widgetId; 1237 1238 widgetControl = new ControlConstructor({ 1239 el: fieldContainer, 1240 syncContainer: syncContainer, 1241 model: new ModelConstructor( modelAttributes ) 1242 }); 1243 1244 component.modelCollection.add( [ widgetControl.model ] ); 1245 component.widgetControls[ widgetControl.model.get( 'widget_id' ) ] = widgetControl; 1246 1247 widgetControl.render(); 1248 }; 1249 1250 /** 1251 * Sync widget instance data sanitized from server back onto widget model. 1252 * 1253 * This gets called via the 'widget-updated' event when saving a widget from 1254 * the widgets admin screen and also via the 'widget-synced' event when making 1255 * a change to a widget in the customizer. 1256 * 1257 * @memberOf wp.mediaWidgets 1258 * 1259 * @param {jQuery.Event} event - Event. 1260 * @param {jQuery} widgetContainer - Widget container element. 1261 * 1262 * @return {void} 1263 */ 1264 component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) { 1265 var widgetForm, widgetContent, widgetId, widgetControl, attributes = {}; 1266 widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); 1267 widgetId = widgetForm.find( '> .widget-id' ).val(); 1268 1269 widgetControl = component.widgetControls[ widgetId ]; 1270 if ( ! widgetControl ) { 1271 return; 1272 } 1273 1274 // Make sure the server-sanitized values get synced back into the model. 1275 widgetContent = widgetForm.find( '> .widget-content' ); 1276 widgetContent.find( '.media-widget-instance-property' ).each( function() { 1277 var property = $( this ).data( 'property' ); 1278 attributes[ property ] = $( this ).val(); 1279 }); 1280 1281 // Suspend syncing model back to inputs when syncing from inputs to model, preventing infinite loop. 1282 widgetControl.stopListening( widgetControl.model, 'change', widgetControl.syncModelToInputs ); 1283 widgetControl.model.set( attributes ); 1284 widgetControl.listenTo( widgetControl.model, 'change', widgetControl.syncModelToInputs ); 1285 }; 1286 1287 /** 1288 * Initialize functionality. 1289 * 1290 * This function exists to prevent the JS file from having to boot itself. 1291 * When WordPress enqueues this script, it should have an inline script 1292 * attached which calls wp.mediaWidgets.init(). 1293 * 1294 * @memberOf wp.mediaWidgets 1295 * 1296 * @return {void} 1297 */ 1298 component.init = function init() { 1299 var $document = $( document ); 1300 $document.on( 'widget-added', component.handleWidgetAdded ); 1301 $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated ); 1302 1303 /* 1304 * Manually trigger widget-added events for media widgets on the admin 1305 * screen once they are expanded. The widget-added event is not triggered 1306 * for each pre-existing widget on the widgets admin screen like it is 1307 * on the customizer. Likewise, the customizer only triggers widget-added 1308 * when the widget is expanded to just-in-time construct the widget form 1309 * when it is actually going to be displayed. So the following implements 1310 * the same for the widgets admin screen, to invoke the widget-added 1311 * handler when a pre-existing media widget is expanded. 1312 */ 1313 $( function initializeExistingWidgetContainers() { 1314 var widgetContainers; 1315 if ( 'widgets' !== window.pagenow ) { 1316 return; 1317 } 1318 widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' ); 1319 widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() { 1320 var widgetContainer = $( this ); 1321 component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer ); 1322 }); 1323 1324 // Accessibility mode. 1325 if ( document.readyState === 'complete' ) { 1326 // Page is fully loaded. 1327 component.setupAccessibleMode(); 1328 } else { 1329 // Page is still loading. 1330 $( window ).on( 'load', function() { 1331 component.setupAccessibleMode(); 1332 }); 1333 } 1334 }); 1335 }; 1336 1337 return component; 1338 })( jQuery );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sun Sep 6 08:20:27 2026 | Cross-referenced by PHPXref |