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