summaryrefslogtreecommitdiffstats
path: root/wp-admin/js/widgets
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 07:56:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 07:56:49 +0000
commita415c29efee45520ae252d2aa28f1083a521cd7b (patch)
treef4ade4b6668ecc0765de7e1424f7c1427ad433ff /wp-admin/js/widgets
parentInitial commit. (diff)
downloadwordpress-a415c29efee45520ae252d2aa28f1083a521cd7b.tar.xz
wordpress-a415c29efee45520ae252d2aa28f1083a521cd7b.zip
Adding upstream version 6.4.3+dfsg1.upstream/6.4.3+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'wp-admin/js/widgets')
-rw-r--r--wp-admin/js/widgets/custom-html-widgets.js462
-rw-r--r--wp-admin/js/widgets/custom-html-widgets.min.js2
-rw-r--r--wp-admin/js/widgets/media-audio-widget.js154
-rw-r--r--wp-admin/js/widgets/media-audio-widget.min.js2
-rw-r--r--wp-admin/js/widgets/media-gallery-widget.js341
-rw-r--r--wp-admin/js/widgets/media-gallery-widget.min.js2
-rw-r--r--wp-admin/js/widgets/media-image-widget.js170
-rw-r--r--wp-admin/js/widgets/media-image-widget.min.js2
-rw-r--r--wp-admin/js/widgets/media-video-widget.js256
-rw-r--r--wp-admin/js/widgets/media-video-widget.min.js2
-rw-r--r--wp-admin/js/widgets/media-widgets.js1336
-rw-r--r--wp-admin/js/widgets/media-widgets.min.js2
-rw-r--r--wp-admin/js/widgets/text-widgets.js550
-rw-r--r--wp-admin/js/widgets/text-widgets.min.js2
14 files changed, 3283 insertions, 0 deletions
diff --git a/wp-admin/js/widgets/custom-html-widgets.js b/wp-admin/js/widgets/custom-html-widgets.js
new file mode 100644
index 0000000..e36d115
--- /dev/null
+++ b/wp-admin/js/widgets/custom-html-widgets.js
@@ -0,0 +1,462 @@
+/**
+ * @output wp-admin/js/widgets/custom-html-widgets.js
+ */
+
+/* global wp */
+/* eslint consistent-this: [ "error", "control" ] */
+/* eslint no-magic-numbers: ["error", { "ignore": [0,1,-1] }] */
+
+/**
+ * @namespace wp.customHtmlWidget
+ * @memberOf wp
+ */
+wp.customHtmlWidgets = ( function( $ ) {
+ 'use strict';
+
+ var component = {
+ idBases: [ 'custom_html' ],
+ codeEditorSettings: {},
+ l10n: {
+ errorNotice: {
+ singular: '',
+ plural: ''
+ }
+ }
+ };
+
+ component.CustomHtmlWidgetControl = Backbone.View.extend(/** @lends wp.customHtmlWidgets.CustomHtmlWidgetControl.prototype */{
+
+ /**
+ * View events.
+ *
+ * @type {Object}
+ */
+ events: {},
+
+ /**
+ * Text widget control.
+ *
+ * @constructs wp.customHtmlWidgets.CustomHtmlWidgetControl
+ * @augments Backbone.View
+ * @abstract
+ *
+ * @param {Object} options - Options.
+ * @param {jQuery} options.el - Control field container element.
+ * @param {jQuery} options.syncContainer - Container element where fields are synced for the server.
+ *
+ * @return {void}
+ */
+ initialize: function initialize( options ) {
+ var control = this;
+
+ if ( ! options.el ) {
+ throw new Error( 'Missing options.el' );
+ }
+ if ( ! options.syncContainer ) {
+ throw new Error( 'Missing options.syncContainer' );
+ }
+
+ Backbone.View.prototype.initialize.call( control, options );
+ control.syncContainer = options.syncContainer;
+ control.widgetIdBase = control.syncContainer.parent().find( '.id_base' ).val();
+ control.widgetNumber = control.syncContainer.parent().find( '.widget_number' ).val();
+ control.customizeSettingId = 'widget_' + control.widgetIdBase + '[' + String( control.widgetNumber ) + ']';
+
+ control.$el.addClass( 'custom-html-widget-fields' );
+ control.$el.html( wp.template( 'widget-custom-html-control-fields' )( { codeEditorDisabled: component.codeEditorSettings.disabled } ) );
+
+ control.errorNoticeContainer = control.$el.find( '.code-editor-error-container' );
+ control.currentErrorAnnotations = [];
+ control.saveButton = control.syncContainer.add( control.syncContainer.parent().find( '.widget-control-actions' ) ).find( '.widget-control-save, #savewidget' );
+ control.saveButton.addClass( 'custom-html-widget-save-button' ); // To facilitate style targeting.
+
+ control.fields = {
+ title: control.$el.find( '.title' ),
+ content: control.$el.find( '.content' )
+ };
+
+ // Sync input fields to hidden sync fields which actually get sent to the server.
+ _.each( control.fields, function( fieldInput, fieldName ) {
+ fieldInput.on( 'input change', function updateSyncField() {
+ var syncInput = control.syncContainer.find( '.sync-input.' + fieldName );
+ if ( syncInput.val() !== fieldInput.val() ) {
+ syncInput.val( fieldInput.val() );
+ syncInput.trigger( 'change' );
+ }
+ });
+
+ // Note that syncInput cannot be re-used because it will be destroyed with each widget-updated event.
+ fieldInput.val( control.syncContainer.find( '.sync-input.' + fieldName ).val() );
+ });
+ },
+
+ /**
+ * Update input fields from the sync fields.
+ *
+ * This function is called at the widget-updated and widget-synced events.
+ * A field will only be updated if it is not currently focused, to avoid
+ * overwriting content that the user is entering.
+ *
+ * @return {void}
+ */
+ updateFields: function updateFields() {
+ var control = this, syncInput;
+
+ if ( ! control.fields.title.is( document.activeElement ) ) {
+ syncInput = control.syncContainer.find( '.sync-input.title' );
+ control.fields.title.val( syncInput.val() );
+ }
+
+ /*
+ * Prevent updating content when the editor is focused or if there are current error annotations,
+ * to prevent the editor's contents from getting sanitized as soon as a user removes focus from
+ * the editor. This is particularly important for users who cannot unfiltered_html.
+ */
+ control.contentUpdateBypassed = control.fields.content.is( document.activeElement ) || control.editor && control.editor.codemirror.state.focused || 0 !== control.currentErrorAnnotations.length;
+ if ( ! control.contentUpdateBypassed ) {
+ syncInput = control.syncContainer.find( '.sync-input.content' );
+ control.fields.content.val( syncInput.val() );
+ }
+ },
+
+ /**
+ * Show linting error notice.
+ *
+ * @param {Array} errorAnnotations - Error annotations.
+ * @return {void}
+ */
+ updateErrorNotice: function( errorAnnotations ) {
+ var control = this, errorNotice, message = '', customizeSetting;
+
+ if ( 1 === errorAnnotations.length ) {
+ message = component.l10n.errorNotice.singular.replace( '%d', '1' );
+ } else if ( errorAnnotations.length > 1 ) {
+ message = component.l10n.errorNotice.plural.replace( '%d', String( errorAnnotations.length ) );
+ }
+
+ if ( control.fields.content[0].setCustomValidity ) {
+ control.fields.content[0].setCustomValidity( message );
+ }
+
+ if ( wp.customize && wp.customize.has( control.customizeSettingId ) ) {
+ customizeSetting = wp.customize( control.customizeSettingId );
+ customizeSetting.notifications.remove( 'htmlhint_error' );
+ if ( 0 !== errorAnnotations.length ) {
+ customizeSetting.notifications.add( 'htmlhint_error', new wp.customize.Notification( 'htmlhint_error', {
+ message: message,
+ type: 'error'
+ } ) );
+ }
+ } else if ( 0 !== errorAnnotations.length ) {
+ errorNotice = $( '<div class="inline notice notice-error notice-alt"></div>' );
+ errorNotice.append( $( '<p></p>', {
+ text: message
+ } ) );
+ control.errorNoticeContainer.empty();
+ control.errorNoticeContainer.append( errorNotice );
+ control.errorNoticeContainer.slideDown( 'fast' );
+ wp.a11y.speak( message );
+ } else {
+ control.errorNoticeContainer.slideUp( 'fast' );
+ }
+ },
+
+ /**
+ * Initialize editor.
+ *
+ * @return {void}
+ */
+ initializeEditor: function initializeEditor() {
+ var control = this, settings;
+
+ if ( component.codeEditorSettings.disabled ) {
+ return;
+ }
+
+ settings = _.extend( {}, component.codeEditorSettings, {
+
+ /**
+ * Handle tabbing to the field before the editor.
+ *
+ * @ignore
+ *
+ * @return {void}
+ */
+ onTabPrevious: function onTabPrevious() {
+ control.fields.title.focus();
+ },
+
+ /**
+ * Handle tabbing to the field after the editor.
+ *
+ * @ignore
+ *
+ * @return {void}
+ */
+ onTabNext: function onTabNext() {
+ var tabbables = control.syncContainer.add( control.syncContainer.parent().find( '.widget-position, .widget-control-actions' ) ).find( ':tabbable' );
+ tabbables.first().focus();
+ },
+
+ /**
+ * Disable save button and store linting errors for use in updateFields.
+ *
+ * @ignore
+ *
+ * @param {Array} errorAnnotations - Error notifications.
+ * @return {void}
+ */
+ onChangeLintingErrors: function onChangeLintingErrors( errorAnnotations ) {
+ control.currentErrorAnnotations = errorAnnotations;
+ },
+
+ /**
+ * Update error notice.
+ *
+ * @ignore
+ *
+ * @param {Array} errorAnnotations - Error annotations.
+ * @return {void}
+ */
+ onUpdateErrorNotice: function onUpdateErrorNotice( errorAnnotations ) {
+ control.saveButton.toggleClass( 'validation-blocked disabled', errorAnnotations.length > 0 );
+ control.updateErrorNotice( errorAnnotations );
+ }
+ });
+
+ control.editor = wp.codeEditor.initialize( control.fields.content, settings );
+
+ // Improve the editor accessibility.
+ $( control.editor.codemirror.display.lineDiv )
+ .attr({
+ role: 'textbox',
+ 'aria-multiline': 'true',
+ 'aria-labelledby': control.fields.content[0].id + '-label',
+ 'aria-describedby': 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4'
+ });
+
+ // Focus the editor when clicking on its label.
+ $( '#' + control.fields.content[0].id + '-label' ).on( 'click', function() {
+ control.editor.codemirror.focus();
+ });
+
+ control.fields.content.on( 'change', function() {
+ if ( this.value !== control.editor.codemirror.getValue() ) {
+ control.editor.codemirror.setValue( this.value );
+ }
+ });
+ control.editor.codemirror.on( 'change', function() {
+ var value = control.editor.codemirror.getValue();
+ if ( value !== control.fields.content.val() ) {
+ control.fields.content.val( value ).trigger( 'change' );
+ }
+ });
+
+ // Make sure the editor gets updated if the content was updated on the server (sanitization) but not updated in the editor since it was focused.
+ control.editor.codemirror.on( 'blur', function() {
+ if ( control.contentUpdateBypassed ) {
+ control.syncContainer.find( '.sync-input.content' ).trigger( 'change' );
+ }
+ });
+
+ // Prevent hitting Esc from collapsing the widget control.
+ if ( wp.customize ) {
+ control.editor.codemirror.on( 'keydown', function onKeydown( codemirror, event ) {
+ var escKeyCode = 27;
+ if ( escKeyCode === event.keyCode ) {
+ event.stopPropagation();
+ }
+ });
+ }
+ }
+ });
+
+ /**
+ * Mapping of widget ID to instances of CustomHtmlWidgetControl subclasses.
+ *
+ * @alias wp.customHtmlWidgets.widgetControls
+ *
+ * @type {Object.<string, wp.textWidgets.CustomHtmlWidgetControl>}
+ */
+ component.widgetControls = {};
+
+ /**
+ * Handle widget being added or initialized for the first time at the widget-added event.
+ *
+ * @alias wp.customHtmlWidgets.handleWidgetAdded
+ *
+ * @param {jQuery.Event} event - Event.
+ * @param {jQuery} widgetContainer - Widget container element.
+ *
+ * @return {void}
+ */
+ component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) {
+ var widgetForm, idBase, widgetControl, widgetId, animatedCheckDelay = 50, renderWhenAnimationDone, fieldContainer, syncContainer;
+ widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen.
+
+ idBase = widgetForm.find( '> .id_base' ).val();
+ if ( -1 === component.idBases.indexOf( idBase ) ) {
+ return;
+ }
+
+ // Prevent initializing already-added widgets.
+ widgetId = widgetForm.find( '.widget-id' ).val();
+ if ( component.widgetControls[ widgetId ] ) {
+ return;
+ }
+
+ /*
+ * Create a container element for the widget control fields.
+ * This is inserted into the DOM immediately before the the .widget-content
+ * element because the contents of this element are essentially "managed"
+ * by PHP, where each widget update cause the entire element to be emptied
+ * and replaced with the rendered output of WP_Widget::form() which is
+ * sent back in Ajax request made to save/update the widget instance.
+ * To prevent a "flash of replaced DOM elements and re-initialized JS
+ * components", the JS template is rendered outside of the normal form
+ * container.
+ */
+ fieldContainer = $( '<div></div>' );
+ syncContainer = widgetContainer.find( '.widget-content:first' );
+ syncContainer.before( fieldContainer );
+
+ widgetControl = new component.CustomHtmlWidgetControl({
+ el: fieldContainer,
+ syncContainer: syncContainer
+ });
+
+ component.widgetControls[ widgetId ] = widgetControl;
+
+ /*
+ * Render the widget once the widget parent's container finishes animating,
+ * as the widget-added event fires with a slideDown of the container.
+ * This ensures that the textarea is visible and the editor can be initialized.
+ */
+ renderWhenAnimationDone = function() {
+ if ( ! ( wp.customize ? widgetContainer.parent().hasClass( 'expanded' ) : widgetContainer.hasClass( 'open' ) ) ) { // Core merge: The wp.customize condition can be eliminated with this change being in core: https://github.com/xwp/wordpress-develop/pull/247/commits/5322387d
+ setTimeout( renderWhenAnimationDone, animatedCheckDelay );
+ } else {
+ widgetControl.initializeEditor();
+ }
+ };
+ renderWhenAnimationDone();
+ };
+
+ /**
+ * Setup widget in accessibility mode.
+ *
+ * @alias wp.customHtmlWidgets.setupAccessibleMode
+ *
+ * @return {void}
+ */
+ component.setupAccessibleMode = function setupAccessibleMode() {
+ var widgetForm, idBase, widgetControl, fieldContainer, syncContainer;
+ widgetForm = $( '.editwidget > form' );
+ if ( 0 === widgetForm.length ) {
+ return;
+ }
+
+ idBase = widgetForm.find( '.id_base' ).val();
+ if ( -1 === component.idBases.indexOf( idBase ) ) {
+ return;
+ }
+
+ fieldContainer = $( '<div></div>' );
+ syncContainer = widgetForm.find( '> .widget-inside' );
+ syncContainer.before( fieldContainer );
+
+ widgetControl = new component.CustomHtmlWidgetControl({
+ el: fieldContainer,
+ syncContainer: syncContainer
+ });
+
+ widgetControl.initializeEditor();
+ };
+
+ /**
+ * Sync widget instance data sanitized from server back onto widget model.
+ *
+ * This gets called via the 'widget-updated' event when saving a widget from
+ * the widgets admin screen and also via the 'widget-synced' event when making
+ * a change to a widget in the customizer.
+ *
+ * @alias wp.customHtmlWidgets.handleWidgetUpdated
+ *
+ * @param {jQuery.Event} event - Event.
+ * @param {jQuery} widgetContainer - Widget container element.
+ * @return {void}
+ */
+ component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) {
+ var widgetForm, widgetId, widgetControl, idBase;
+ widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
+
+ idBase = widgetForm.find( '> .id_base' ).val();
+ if ( -1 === component.idBases.indexOf( idBase ) ) {
+ return;
+ }
+
+ widgetId = widgetForm.find( '> .widget-id' ).val();
+ widgetControl = component.widgetControls[ widgetId ];
+ if ( ! widgetControl ) {
+ return;
+ }
+
+ widgetControl.updateFields();
+ };
+
+ /**
+ * Initialize functionality.
+ *
+ * This function exists to prevent the JS file from having to boot itself.
+ * When WordPress enqueues this script, it should have an inline script
+ * attached which calls wp.textWidgets.init().
+ *
+ * @alias wp.customHtmlWidgets.init
+ *
+ * @param {Object} settings - Options for code editor, exported from PHP.
+ *
+ * @return {void}
+ */
+ component.init = function init( settings ) {
+ var $document = $( document );
+ _.extend( component.codeEditorSettings, settings );
+
+ $document.on( 'widget-added', component.handleWidgetAdded );
+ $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated );
+
+ /*
+ * Manually trigger widget-added events for media widgets on the admin
+ * screen once they are expanded. The widget-added event is not triggered
+ * for each pre-existing widget on the widgets admin screen like it is
+ * on the customizer. Likewise, the customizer only triggers widget-added
+ * when the widget is expanded to just-in-time construct the widget form
+ * when it is actually going to be displayed. So the following implements
+ * the same for the widgets admin screen, to invoke the widget-added
+ * handler when a pre-existing media widget is expanded.
+ */
+ $( function initializeExistingWidgetContainers() {
+ var widgetContainers;
+ if ( 'widgets' !== window.pagenow ) {
+ return;
+ }
+ widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' );
+ widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() {
+ var widgetContainer = $( this );
+ component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer );
+ });
+
+ // Accessibility mode.
+ if ( document.readyState === 'complete' ) {
+ // Page is fully loaded.
+ component.setupAccessibleMode();
+ } else {
+ // Page is still loading.
+ $( window ).on( 'load', function() {
+ component.setupAccessibleMode();
+ });
+ }
+ });
+ };
+
+ return component;
+})( jQuery );
diff --git a/wp-admin/js/widgets/custom-html-widgets.min.js b/wp-admin/js/widgets/custom-html-widgets.min.js
new file mode 100644
index 0000000..8baa98f
--- /dev/null
+++ b/wp-admin/js/widgets/custom-html-widgets.min.js
@@ -0,0 +1,2 @@
+/*! This file is auto-generated */
+wp.customHtmlWidgets=function(a){"use strict";var s={idBases:["custom_html"],codeEditorSettings:{},l10n:{errorNotice:{singular:"",plural:""}}};return s.CustomHtmlWidgetControl=Backbone.View.extend({events:{},initialize:function(e){var n=this;if(!e.el)throw new Error("Missing options.el");if(!e.syncContainer)throw new Error("Missing options.syncContainer");Backbone.View.prototype.initialize.call(n,e),n.syncContainer=e.syncContainer,n.widgetIdBase=n.syncContainer.parent().find(".id_base").val(),n.widgetNumber=n.syncContainer.parent().find(".widget_number").val(),n.customizeSettingId="widget_"+n.widgetIdBase+"["+String(n.widgetNumber)+"]",n.$el.addClass("custom-html-widget-fields"),n.$el.html(wp.template("widget-custom-html-control-fields")({codeEditorDisabled:s.codeEditorSettings.disabled})),n.errorNoticeContainer=n.$el.find(".code-editor-error-container"),n.currentErrorAnnotations=[],n.saveButton=n.syncContainer.add(n.syncContainer.parent().find(".widget-control-actions")).find(".widget-control-save, #savewidget"),n.saveButton.addClass("custom-html-widget-save-button"),n.fields={title:n.$el.find(".title"),content:n.$el.find(".content")},_.each(n.fields,function(t,i){t.on("input change",function(){var e=n.syncContainer.find(".sync-input."+i);e.val()!==t.val()&&(e.val(t.val()),e.trigger("change"))}),t.val(n.syncContainer.find(".sync-input."+i).val())})},updateFields:function(){var e,t=this;t.fields.title.is(document.activeElement)||(e=t.syncContainer.find(".sync-input.title"),t.fields.title.val(e.val())),t.contentUpdateBypassed=t.fields.content.is(document.activeElement)||t.editor&&t.editor.codemirror.state.focused||0!==t.currentErrorAnnotations.length,t.contentUpdateBypassed||(e=t.syncContainer.find(".sync-input.content"),t.fields.content.val(e.val()))},updateErrorNotice:function(e){var t,i=this,n="";1===e.length?n=s.l10n.errorNotice.singular.replace("%d","1"):1<e.length&&(n=s.l10n.errorNotice.plural.replace("%d",String(e.length))),i.fields.content[0].setCustomValidity&&i.fields.content[0].setCustomValidity(n),wp.customize&&wp.customize.has(i.customizeSettingId)?((t=wp.customize(i.customizeSettingId)).notifications.remove("htmlhint_error"),0!==e.length&&t.notifications.add("htmlhint_error",new wp.customize.Notification("htmlhint_error",{message:n,type:"error"}))):0!==e.length?((t=a('<div class="inline notice notice-error notice-alt"></div>')).append(a("<p></p>",{text:n})),i.errorNoticeContainer.empty(),i.errorNoticeContainer.append(t),i.errorNoticeContainer.slideDown("fast"),wp.a11y.speak(n)):i.errorNoticeContainer.slideUp("fast")},initializeEditor:function(){var e,t=this;s.codeEditorSettings.disabled||(e=_.extend({},s.codeEditorSettings,{onTabPrevious:function(){t.fields.title.focus()},onTabNext:function(){t.syncContainer.add(t.syncContainer.parent().find(".widget-position, .widget-control-actions")).find(":tabbable").first().focus()},onChangeLintingErrors:function(e){t.currentErrorAnnotations=e},onUpdateErrorNotice:function(e){t.saveButton.toggleClass("validation-blocked disabled",0<e.length),t.updateErrorNotice(e)}}),t.editor=wp.codeEditor.initialize(t.fields.content,e),a(t.editor.codemirror.display.lineDiv).attr({role:"textbox","aria-multiline":"true","aria-labelledby":t.fields.content[0].id+"-label","aria-describedby":"editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4"}),a("#"+t.fields.content[0].id+"-label").on("click",function(){t.editor.codemirror.focus()}),t.fields.content.on("change",function(){this.value!==t.editor.codemirror.getValue()&&t.editor.codemirror.setValue(this.value)}),t.editor.codemirror.on("change",function(){var e=t.editor.codemirror.getValue();e!==t.fields.content.val()&&t.fields.content.val(e).trigger("change")}),t.editor.codemirror.on("blur",function(){t.contentUpdateBypassed&&t.syncContainer.find(".sync-input.content").trigger("change")}),wp.customize&&t.editor.codemirror.on("keydown",function(e,t){27===t.keyCode&&t.stopPropagation()}))}}),s.widgetControls={},s.handleWidgetAdded=function(e,t){var i,n,o,d=t.find("> .widget-inside > .form, > .widget-inside > form"),r=d.find("> .id_base").val();-1===s.idBases.indexOf(r)||(r=d.find(".widget-id").val(),s.widgetControls[r])||(d=a("<div></div>"),(o=t.find(".widget-content:first")).before(d),i=new s.CustomHtmlWidgetControl({el:d,syncContainer:o}),s.widgetControls[r]=i,(n=function(){(wp.customize?t.parent().hasClass("expanded"):t.hasClass("open"))?i.initializeEditor():setTimeout(n,50)})())},s.setupAccessibleMode=function(){var e,t=a(".editwidget > form");0!==t.length&&(e=t.find(".id_base").val(),-1!==s.idBases.indexOf(e))&&(e=a("<div></div>"),(t=t.find("> .widget-inside")).before(e),new s.CustomHtmlWidgetControl({el:e,syncContainer:t}).initializeEditor())},s.handleWidgetUpdated=function(e,t){var t=t.find("> .widget-inside > .form, > .widget-inside > form"),i=t.find("> .id_base").val();-1!==s.idBases.indexOf(i)&&(i=t.find("> .widget-id").val(),t=s.widgetControls[i])&&t.updateFields()},s.init=function(e){var t=a(document);_.extend(s.codeEditorSettings,e),t.on("widget-added",s.handleWidgetAdded),t.on("widget-synced widget-updated",s.handleWidgetUpdated),a(function(){"widgets"===window.pagenow&&(a(".widgets-holder-wrap:not(#available-widgets)").find("div.widget").one("click.toggle-widget-expanded",function(){var e=a(this);s.handleWidgetAdded(new jQuery.Event("widget-added"),e)}),"complete"===document.readyState?s.setupAccessibleMode():a(window).on("load",function(){s.setupAccessibleMode()}))})},s}(jQuery); \ No newline at end of file
diff --git a/wp-admin/js/widgets/media-audio-widget.js b/wp-admin/js/widgets/media-audio-widget.js
new file mode 100644
index 0000000..a579253
--- /dev/null
+++ b/wp-admin/js/widgets/media-audio-widget.js
@@ -0,0 +1,154 @@
+/**
+ * @output wp-admin/js/widgets/media-audio-widget.js
+ */
+
+/* eslint consistent-this: [ "error", "control" ] */
+(function( component ) {
+ 'use strict';
+
+ var AudioWidgetModel, AudioWidgetControl, AudioDetailsMediaFrame;
+
+ /**
+ * Custom audio details frame that removes the replace-audio state.
+ *
+ * @class wp.mediaWidgets.controlConstructors~AudioDetailsMediaFrame
+ * @augments wp.media.view.MediaFrame.AudioDetails
+ */
+ AudioDetailsMediaFrame = wp.media.view.MediaFrame.AudioDetails.extend(/** @lends wp.mediaWidgets.controlConstructors~AudioDetailsMediaFrame.prototype */{
+
+ /**
+ * Create the default states.
+ *
+ * @return {void}
+ */
+ createStates: function createStates() {
+ this.states.add([
+ new wp.media.controller.AudioDetails({
+ media: this.media
+ }),
+
+ new wp.media.controller.MediaLibrary({
+ type: 'audio',
+ id: 'add-audio-source',
+ title: wp.media.view.l10n.audioAddSourceTitle,
+ toolbar: 'add-audio-source',
+ media: this.media,
+ menu: false
+ })
+ ]);
+ }
+ });
+
+ /**
+ * Audio widget model.
+ *
+ * See WP_Widget_Audio::enqueue_admin_scripts() for amending prototype from PHP exports.
+ *
+ * @class wp.mediaWidgets.modelConstructors.media_audio
+ * @augments wp.mediaWidgets.MediaWidgetModel
+ */
+ AudioWidgetModel = component.MediaWidgetModel.extend({});
+
+ /**
+ * Audio widget control.
+ *
+ * See WP_Widget_Audio::enqueue_admin_scripts() for amending prototype from PHP exports.
+ *
+ * @class wp.mediaWidgets.controlConstructors.media_audio
+ * @augments wp.mediaWidgets.MediaWidgetControl
+ */
+ AudioWidgetControl = component.MediaWidgetControl.extend(/** @lends wp.mediaWidgets.controlConstructors.media_audio.prototype */{
+
+ /**
+ * Show display settings.
+ *
+ * @type {boolean}
+ */
+ showDisplaySettings: false,
+
+ /**
+ * Map model props to media frame props.
+ *
+ * @param {Object} modelProps - Model props.
+ * @return {Object} Media frame props.
+ */
+ mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) {
+ var control = this, mediaFrameProps;
+ mediaFrameProps = component.MediaWidgetControl.prototype.mapModelToMediaFrameProps.call( control, modelProps );
+ mediaFrameProps.link = 'embed';
+ return mediaFrameProps;
+ },
+
+ /**
+ * Render preview.
+ *
+ * @return {void}
+ */
+ renderPreview: function renderPreview() {
+ var control = this, previewContainer, previewTemplate, attachmentId, attachmentUrl;
+ attachmentId = control.model.get( 'attachment_id' );
+ attachmentUrl = control.model.get( 'url' );
+
+ if ( ! attachmentId && ! attachmentUrl ) {
+ return;
+ }
+
+ previewContainer = control.$el.find( '.media-widget-preview' );
+ previewTemplate = wp.template( 'wp-media-widget-audio-preview' );
+
+ previewContainer.html( previewTemplate({
+ model: {
+ attachment_id: control.model.get( 'attachment_id' ),
+ src: attachmentUrl
+ },
+ error: control.model.get( 'error' )
+ }));
+ wp.mediaelement.initialize();
+ },
+
+ /**
+ * Open the media audio-edit frame to modify the selected item.
+ *
+ * @return {void}
+ */
+ editMedia: function editMedia() {
+ var control = this, mediaFrame, metadata, updateCallback;
+
+ metadata = control.mapModelToMediaFrameProps( control.model.toJSON() );
+
+ // Set up the media frame.
+ mediaFrame = new AudioDetailsMediaFrame({
+ frame: 'audio',
+ state: 'audio-details',
+ metadata: metadata
+ });
+ wp.media.frame = mediaFrame;
+ mediaFrame.$el.addClass( 'media-widget' );
+
+ updateCallback = function( mediaFrameProps ) {
+
+ // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview.
+ control.selectedAttachment.set( mediaFrameProps );
+
+ control.model.set( _.extend(
+ control.model.defaults(),
+ control.mapMediaToModelProps( mediaFrameProps ),
+ { error: false }
+ ) );
+ };
+
+ mediaFrame.state( 'audio-details' ).on( 'update', updateCallback );
+ mediaFrame.state( 'replace-audio' ).on( 'replace', updateCallback );
+ mediaFrame.on( 'close', function() {
+ mediaFrame.detach();
+ });
+
+ mediaFrame.open();
+ }
+ });
+
+ // Exports.
+ component.controlConstructors.media_audio = AudioWidgetControl;
+ component.modelConstructors.media_audio = AudioWidgetModel;
+
+})( wp.mediaWidgets );
diff --git a/wp-admin/js/widgets/media-audio-widget.min.js b/wp-admin/js/widgets/media-audio-widget.min.js
new file mode 100644
index 0000000..e26f4f8
--- /dev/null
+++ b/wp-admin/js/widgets/media-audio-widget.min.js
@@ -0,0 +1,2 @@
+/*! This file is auto-generated */
+!function(t){"use strict";var a=wp.media.view.MediaFrame.AudioDetails.extend({createStates:function(){this.states.add([new wp.media.controller.AudioDetails({media:this.media}),new wp.media.controller.MediaLibrary({type:"audio",id:"add-audio-source",title:wp.media.view.l10n.audioAddSourceTitle,toolbar:"add-audio-source",media:this.media,menu:!1})])}}),e=t.MediaWidgetModel.extend({}),d=t.MediaWidgetControl.extend({showDisplaySettings:!1,mapModelToMediaFrameProps:function(e){e=t.MediaWidgetControl.prototype.mapModelToMediaFrameProps.call(this,e);return e.link="embed",e},renderPreview:function(){var e,t=this,d=t.model.get("attachment_id"),a=t.model.get("url");(d||a)&&(d=t.$el.find(".media-widget-preview"),e=wp.template("wp-media-widget-audio-preview"),d.html(e({model:{attachment_id:t.model.get("attachment_id"),src:a},error:t.model.get("error")})),wp.mediaelement.initialize())},editMedia:function(){var t=this,e=t.mapModelToMediaFrameProps(t.model.toJSON()),d=new a({frame:"audio",state:"audio-details",metadata:e});(wp.media.frame=d).$el.addClass("media-widget"),e=function(e){t.selectedAttachment.set(e),t.model.set(_.extend(t.model.defaults(),t.mapMediaToModelProps(e),{error:!1}))},d.state("audio-details").on("update",e),d.state("replace-audio").on("replace",e),d.on("close",function(){d.detach()}),d.open()}});t.controlConstructors.media_audio=d,t.modelConstructors.media_audio=e}(wp.mediaWidgets); \ No newline at end of file
diff --git a/wp-admin/js/widgets/media-gallery-widget.js b/wp-admin/js/widgets/media-gallery-widget.js
new file mode 100644
index 0000000..020e978
--- /dev/null
+++ b/wp-admin/js/widgets/media-gallery-widget.js
@@ -0,0 +1,341 @@
+/**
+ * @output wp-admin/js/widgets/media-gallery-widget.js
+ */
+
+/* eslint consistent-this: [ "error", "control" ] */
+(function( component ) {
+ 'use strict';
+
+ var GalleryWidgetModel, GalleryWidgetControl, GalleryDetailsMediaFrame;
+
+ /**
+ * Custom gallery details frame.
+ *
+ * @since 4.9.0
+ * @class wp.mediaWidgets~GalleryDetailsMediaFrame
+ * @augments wp.media.view.MediaFrame.Post
+ */
+ GalleryDetailsMediaFrame = wp.media.view.MediaFrame.Post.extend(/** @lends wp.mediaWidgets~GalleryDetailsMediaFrame.prototype */{
+
+ /**
+ * Create the default states.
+ *
+ * @since 4.9.0
+ * @return {void}
+ */
+ createStates: function createStates() {
+ this.states.add([
+ new wp.media.controller.Library({
+ id: 'gallery',
+ title: wp.media.view.l10n.createGalleryTitle,
+ priority: 40,
+ toolbar: 'main-gallery',
+ filterable: 'uploaded',
+ multiple: 'add',
+ editable: true,
+
+ library: wp.media.query( _.defaults({
+ type: 'image'
+ }, this.options.library ) )
+ }),
+
+ // Gallery states.
+ new wp.media.controller.GalleryEdit({
+ library: this.options.selection,
+ editing: this.options.editing,
+ menu: 'gallery'
+ }),
+
+ new wp.media.controller.GalleryAdd()
+ ]);
+ }
+ } );
+
+ /**
+ * Gallery widget model.
+ *
+ * See WP_Widget_Gallery::enqueue_admin_scripts() for amending prototype from PHP exports.
+ *
+ * @since 4.9.0
+ *
+ * @class wp.mediaWidgets.modelConstructors.media_gallery
+ * @augments wp.mediaWidgets.MediaWidgetModel
+ */
+ GalleryWidgetModel = component.MediaWidgetModel.extend(/** @lends wp.mediaWidgets.modelConstructors.media_gallery.prototype */{} );
+
+ GalleryWidgetControl = component.MediaWidgetControl.extend(/** @lends wp.mediaWidgets.controlConstructors.media_gallery.prototype */{
+
+ /**
+ * View events.
+ *
+ * @since 4.9.0
+ * @type {object}
+ */
+ events: _.extend( {}, component.MediaWidgetControl.prototype.events, {
+ 'click .media-widget-gallery-preview': 'editMedia'
+ } ),
+
+ /**
+ * Gallery widget control.
+ *
+ * See WP_Widget_Gallery::enqueue_admin_scripts() for amending prototype from PHP exports.
+ *
+ * @constructs wp.mediaWidgets.controlConstructors.media_gallery
+ * @augments wp.mediaWidgets.MediaWidgetControl
+ *
+ * @since 4.9.0
+ * @param {Object} options - Options.
+ * @param {Backbone.Model} options.model - Model.
+ * @param {jQuery} options.el - Control field container element.
+ * @param {jQuery} options.syncContainer - Container element where fields are synced for the server.
+ * @return {void}
+ */
+ initialize: function initialize( options ) {
+ var control = this;
+
+ component.MediaWidgetControl.prototype.initialize.call( control, options );
+
+ _.bindAll( control, 'updateSelectedAttachments', 'handleAttachmentDestroy' );
+ control.selectedAttachments = new wp.media.model.Attachments();
+ control.model.on( 'change:ids', control.updateSelectedAttachments );
+ control.selectedAttachments.on( 'change', control.renderPreview );
+ control.selectedAttachments.on( 'reset', control.renderPreview );
+ control.updateSelectedAttachments();
+
+ /*
+ * Refresh a Gallery widget partial when the user modifies one of the selected attachments.
+ * This ensures that when an attachment's caption is updated in the media modal the Gallery
+ * widget in the preview will then be refreshed to show the change. Normally doing this
+ * would not be necessary because all of the state should be contained inside the changeset,
+ * as everything done in the Customizer should not make a change to the site unless the
+ * changeset itself is published. Attachments are a current exception to this rule.
+ * For a proposal to include attachments in the customized state, see #37887.
+ */
+ if ( wp.customize && wp.customize.previewer ) {
+ control.selectedAttachments.on( 'change', function() {
+ wp.customize.previewer.send( 'refresh-widget-partial', control.model.get( 'widget_id' ) );
+ } );
+ }
+ },
+
+ /**
+ * Update the selected attachments if necessary.
+ *
+ * @since 4.9.0
+ * @return {void}
+ */
+ updateSelectedAttachments: function updateSelectedAttachments() {
+ var control = this, newIds, oldIds, removedIds, addedIds, addedQuery;
+
+ newIds = control.model.get( 'ids' );
+ oldIds = _.pluck( control.selectedAttachments.models, 'id' );
+
+ removedIds = _.difference( oldIds, newIds );
+ _.each( removedIds, function( removedId ) {
+ control.selectedAttachments.remove( control.selectedAttachments.get( removedId ) );
+ });
+
+ addedIds = _.difference( newIds, oldIds );
+ if ( addedIds.length ) {
+ addedQuery = wp.media.query({
+ order: 'ASC',
+ orderby: 'post__in',
+ perPage: -1,
+ post__in: newIds,
+ query: true,
+ type: 'image'
+ });
+ addedQuery.more().done( function() {
+ control.selectedAttachments.reset( addedQuery.models );
+ });
+ }
+ },
+
+ /**
+ * Render preview.
+ *
+ * @since 4.9.0
+ * @return {void}
+ */
+ renderPreview: function renderPreview() {
+ var control = this, previewContainer, previewTemplate, data;
+
+ previewContainer = control.$el.find( '.media-widget-preview' );
+ previewTemplate = wp.template( 'wp-media-widget-gallery-preview' );
+
+ data = control.previewTemplateProps.toJSON();
+ data.attachments = {};
+ control.selectedAttachments.each( function( attachment ) {
+ data.attachments[ attachment.id ] = attachment.toJSON();
+ } );
+
+ previewContainer.html( previewTemplate( data ) );
+ },
+
+ /**
+ * Determine whether there are selected attachments.
+ *
+ * @since 4.9.0
+ * @return {boolean} Selected.
+ */
+ isSelected: function isSelected() {
+ var control = this;
+
+ if ( control.model.get( 'error' ) ) {
+ return false;
+ }
+
+ return control.model.get( 'ids' ).length > 0;
+ },
+
+ /**
+ * Open the media select frame to edit images.
+ *
+ * @since 4.9.0
+ * @return {void}
+ */
+ editMedia: function editMedia() {
+ var control = this, selection, mediaFrame, mediaFrameProps;
+
+ selection = new wp.media.model.Selection( control.selectedAttachments.models, {
+ multiple: true
+ });
+
+ mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() );
+ selection.gallery = new Backbone.Model( mediaFrameProps );
+ if ( mediaFrameProps.size ) {
+ control.displaySettings.set( 'size', mediaFrameProps.size );
+ }
+ mediaFrame = new GalleryDetailsMediaFrame({
+ frame: 'manage',
+ text: control.l10n.add_to_widget,
+ selection: selection,
+ mimeType: control.mime_type,
+ selectedDisplaySettings: control.displaySettings,
+ showDisplaySettings: control.showDisplaySettings,
+ metadata: mediaFrameProps,
+ editing: true,
+ multiple: true,
+ state: 'gallery-edit'
+ });
+ wp.media.frame = mediaFrame; // See wp.media().
+
+ // Handle selection of a media item.
+ mediaFrame.on( 'update', function onUpdate( newSelection ) {
+ var state = mediaFrame.state(), resultSelection;
+
+ resultSelection = newSelection || state.get( 'selection' );
+ if ( ! resultSelection ) {
+ return;
+ }
+
+ // Copy orderby_random from gallery state.
+ if ( resultSelection.gallery ) {
+ control.model.set( control.mapMediaToModelProps( resultSelection.gallery.toJSON() ) );
+ }
+
+ // Directly update selectedAttachments to prevent needing to do additional request.
+ control.selectedAttachments.reset( resultSelection.models );
+
+ // Update models in the widget instance.
+ control.model.set( {
+ ids: _.pluck( resultSelection.models, 'id' )
+ } );
+ } );
+
+ mediaFrame.$el.addClass( 'media-widget' );
+ mediaFrame.open();
+
+ if ( selection ) {
+ selection.on( 'destroy', control.handleAttachmentDestroy );
+ }
+ },
+
+ /**
+ * Open the media select frame to chose an item.
+ *
+ * @since 4.9.0
+ * @return {void}
+ */
+ selectMedia: function selectMedia() {
+ var control = this, selection, mediaFrame, mediaFrameProps;
+ selection = new wp.media.model.Selection( control.selectedAttachments.models, {
+ multiple: true
+ });
+
+ mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() );
+ if ( mediaFrameProps.size ) {
+ control.displaySettings.set( 'size', mediaFrameProps.size );
+ }
+ mediaFrame = new GalleryDetailsMediaFrame({
+ frame: 'select',
+ text: control.l10n.add_to_widget,
+ selection: selection,
+ mimeType: control.mime_type,
+ selectedDisplaySettings: control.displaySettings,
+ showDisplaySettings: control.showDisplaySettings,
+ metadata: mediaFrameProps,
+ state: 'gallery'
+ });
+ wp.media.frame = mediaFrame; // See wp.media().
+
+ // Handle selection of a media item.
+ mediaFrame.on( 'update', function onUpdate( newSelection ) {
+ var state = mediaFrame.state(), resultSelection;
+
+ resultSelection = newSelection || state.get( 'selection' );
+ if ( ! resultSelection ) {
+ return;
+ }
+
+ // Copy orderby_random from gallery state.
+ if ( resultSelection.gallery ) {
+ control.model.set( control.mapMediaToModelProps( resultSelection.gallery.toJSON() ) );
+ }
+
+ // Directly update selectedAttachments to prevent needing to do additional request.
+ control.selectedAttachments.reset( resultSelection.models );
+
+ // Update widget instance.
+ control.model.set( {
+ ids: _.pluck( resultSelection.models, 'id' )
+ } );
+ } );
+
+ mediaFrame.$el.addClass( 'media-widget' );
+ mediaFrame.open();
+
+ if ( selection ) {
+ selection.on( 'destroy', control.handleAttachmentDestroy );
+ }
+
+ /*
+ * Make sure focus is set inside of modal so that hitting Esc will close
+ * the modal and not inadvertently cause the widget to collapse in the customizer.
+ */
+ mediaFrame.$el.find( ':focusable:first' ).focus();
+ },
+
+ /**
+ * Clear the selected attachment when it is deleted in the media select frame.
+ *
+ * @since 4.9.0
+ * @param {wp.media.models.Attachment} attachment - Attachment.
+ * @return {void}
+ */
+ handleAttachmentDestroy: function handleAttachmentDestroy( attachment ) {
+ var control = this;
+ control.model.set( {
+ ids: _.difference(
+ control.model.get( 'ids' ),
+ [ attachment.id ]
+ )
+ } );
+ }
+ } );
+
+ // Exports.
+ component.controlConstructors.media_gallery = GalleryWidgetControl;
+ component.modelConstructors.media_gallery = GalleryWidgetModel;
+
+})( wp.mediaWidgets );
diff --git a/wp-admin/js/widgets/media-gallery-widget.min.js b/wp-admin/js/widgets/media-gallery-widget.min.js
new file mode 100644
index 0000000..69734a9
--- /dev/null
+++ b/wp-admin/js/widgets/media-gallery-widget.min.js
@@ -0,0 +1,2 @@
+/*! This file is auto-generated */
+!function(i){"use strict";var a=wp.media.view.MediaFrame.Post.extend({createStates:function(){this.states.add([new wp.media.controller.Library({id:"gallery",title:wp.media.view.l10n.createGalleryTitle,priority:40,toolbar:"main-gallery",filterable:"uploaded",multiple:"add",editable:!0,library:wp.media.query(_.defaults({type:"image"},this.options.library))}),new wp.media.controller.GalleryEdit({library:this.options.selection,editing:this.options.editing,menu:"gallery"}),new wp.media.controller.GalleryAdd])}}),e=i.MediaWidgetModel.extend({}),t=i.MediaWidgetControl.extend({events:_.extend({},i.MediaWidgetControl.prototype.events,{"click .media-widget-gallery-preview":"editMedia"}),initialize:function(e){var t=this;i.MediaWidgetControl.prototype.initialize.call(t,e),_.bindAll(t,"updateSelectedAttachments","handleAttachmentDestroy"),t.selectedAttachments=new wp.media.model.Attachments,t.model.on("change:ids",t.updateSelectedAttachments),t.selectedAttachments.on("change",t.renderPreview),t.selectedAttachments.on("reset",t.renderPreview),t.updateSelectedAttachments(),wp.customize&&wp.customize.previewer&&t.selectedAttachments.on("change",function(){wp.customize.previewer.send("refresh-widget-partial",t.model.get("widget_id"))})},updateSelectedAttachments:function(){var e,t=this,i=t.model.get("ids"),d=_.pluck(t.selectedAttachments.models,"id"),a=_.difference(d,i);_.each(a,function(e){t.selectedAttachments.remove(t.selectedAttachments.get(e))}),_.difference(i,d).length&&(e=wp.media.query({order:"ASC",orderby:"post__in",perPage:-1,post__in:i,query:!0,type:"image"})).more().done(function(){t.selectedAttachments.reset(e.models)})},renderPreview:function(){var e=this,t=e.$el.find(".media-widget-preview"),i=wp.template("wp-media-widget-gallery-preview"),d=e.previewTemplateProps.toJSON();d.attachments={},e.selectedAttachments.each(function(e){d.attachments[e.id]=e.toJSON()}),t.html(i(d))},isSelected:function(){return!this.model.get("error")&&0<this.model.get("ids").length},editMedia:function(){var i,d=this,e=new wp.media.model.Selection(d.selectedAttachments.models,{multiple:!0}),t=d.mapModelToMediaFrameProps(d.model.toJSON());e.gallery=new Backbone.Model(t),t.size&&d.displaySettings.set("size",t.size),i=new a({frame:"manage",text:d.l10n.add_to_widget,selection:e,mimeType:d.mime_type,selectedDisplaySettings:d.displaySettings,showDisplaySettings:d.showDisplaySettings,metadata:t,editing:!0,multiple:!0,state:"gallery-edit"}),(wp.media.frame=i).on("update",function(e){var t=i.state(),e=e||t.get("selection");e&&(e.gallery&&d.model.set(d.mapMediaToModelProps(e.gallery.toJSON())),d.selectedAttachments.reset(e.models),d.model.set({ids:_.pluck(e.models,"id")}))}),i.$el.addClass("media-widget"),i.open(),e&&e.on("destroy",d.handleAttachmentDestroy)},selectMedia:function(){var i,d=this,e=new wp.media.model.Selection(d.selectedAttachments.models,{multiple:!0}),t=d.mapModelToMediaFrameProps(d.model.toJSON());t.size&&d.displaySettings.set("size",t.size),i=new a({frame:"select",text:d.l10n.add_to_widget,selection:e,mimeType:d.mime_type,selectedDisplaySettings:d.displaySettings,showDisplaySettings:d.showDisplaySettings,metadata:t,state:"gallery"}),(wp.media.frame=i).on("update",function(e){var t=i.state(),e=e||t.get("selection");e&&(e.gallery&&d.model.set(d.mapMediaToModelProps(e.gallery.toJSON())),d.selectedAttachments.reset(e.models),d.model.set({ids:_.pluck(e.models,"id")}))}),i.$el.addClass("media-widget"),i.open(),e&&e.on("destroy",d.handleAttachmentDestroy),i.$el.find(":focusable:first").focus()},handleAttachmentDestroy:function(e){this.model.set({ids:_.difference(this.model.get("ids"),[e.id])})}});i.controlConstructors.media_gallery=t,i.modelConstructors.media_gallery=e}(wp.mediaWidgets); \ No newline at end of file
diff --git a/wp-admin/js/widgets/media-image-widget.js b/wp-admin/js/widgets/media-image-widget.js
new file mode 100644
index 0000000..7d15eff
--- /dev/null
+++ b/wp-admin/js/widgets/media-image-widget.js
@@ -0,0 +1,170 @@
+/**
+ * @output wp-admin/js/widgets/media-image-widget.js
+ */
+
+/* eslint consistent-this: [ "error", "control" ] */
+(function( component, $ ) {
+ 'use strict';
+
+ var ImageWidgetModel, ImageWidgetControl;
+
+ /**
+ * Image widget model.
+ *
+ * See WP_Widget_Media_Image::enqueue_admin_scripts() for amending prototype from PHP exports.
+ *
+ * @class wp.mediaWidgets.modelConstructors.media_image
+ * @augments wp.mediaWidgets.MediaWidgetModel
+ */
+ ImageWidgetModel = component.MediaWidgetModel.extend({});
+
+ /**
+ * Image widget control.
+ *
+ * See WP_Widget_Media_Image::enqueue_admin_scripts() for amending prototype from PHP exports.
+ *
+ * @class wp.mediaWidgets.controlConstructors.media_audio
+ * @augments wp.mediaWidgets.MediaWidgetControl
+ */
+ ImageWidgetControl = component.MediaWidgetControl.extend(/** @lends wp.mediaWidgets.controlConstructors.media_image.prototype */{
+
+ /**
+ * View events.
+ *
+ * @type {object}
+ */
+ events: _.extend( {}, component.MediaWidgetControl.prototype.events, {
+ 'click .media-widget-preview.populated': 'editMedia'
+ } ),
+
+ /**
+ * Render preview.
+ *
+ * @return {void}
+ */
+ renderPreview: function renderPreview() {
+ var control = this, previewContainer, previewTemplate, fieldsContainer, fieldsTemplate, linkInput;
+ if ( ! control.model.get( 'attachment_id' ) && ! control.model.get( 'url' ) ) {
+ return;
+ }
+
+ previewContainer = control.$el.find( '.media-widget-preview' );
+ previewTemplate = wp.template( 'wp-media-widget-image-preview' );
+ previewContainer.html( previewTemplate( control.previewTemplateProps.toJSON() ) );
+ previewContainer.addClass( 'populated' );
+
+ linkInput = control.$el.find( '.link' );
+ if ( ! linkInput.is( document.activeElement ) ) {
+ fieldsContainer = control.$el.find( '.media-widget-fields' );
+ fieldsTemplate = wp.template( 'wp-media-widget-image-fields' );
+ fieldsContainer.html( fieldsTemplate( control.previewTemplateProps.toJSON() ) );
+ }
+ },
+
+ /**
+ * Open the media image-edit frame to modify the selected item.
+ *
+ * @return {void}
+ */
+ editMedia: function editMedia() {
+ var control = this, mediaFrame, updateCallback, defaultSync, metadata;
+
+ metadata = control.mapModelToMediaFrameProps( control.model.toJSON() );
+
+ // Needed or else none will not be selected if linkUrl is not also empty.
+ if ( 'none' === metadata.link ) {
+ metadata.linkUrl = '';
+ }
+
+ // Set up the media frame.
+ mediaFrame = wp.media({
+ frame: 'image',
+ state: 'image-details',
+ metadata: metadata
+ });
+ mediaFrame.$el.addClass( 'media-widget' );
+
+ updateCallback = function() {
+ var mediaProps, linkType;
+
+ // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview.
+ mediaProps = mediaFrame.state().attributes.image.toJSON();
+ linkType = mediaProps.link;
+ mediaProps.link = mediaProps.linkUrl;
+ control.selectedAttachment.set( mediaProps );
+ control.displaySettings.set( 'link', linkType );
+
+ control.model.set( _.extend(
+ control.mapMediaToModelProps( mediaProps ),
+ { error: false }
+ ) );
+ };
+
+ mediaFrame.state( 'image-details' ).on( 'update', updateCallback );
+ mediaFrame.state( 'replace-image' ).on( 'replace', updateCallback );
+
+ // Disable syncing of attachment changes back to server. See <https://core.trac.wordpress.org/ticket/40403>.
+ defaultSync = wp.media.model.Attachment.prototype.sync;
+ wp.media.model.Attachment.prototype.sync = function rejectedSync() {
+ return $.Deferred().rejectWith( this ).promise();
+ };
+ mediaFrame.on( 'close', function onClose() {
+ mediaFrame.detach();
+ wp.media.model.Attachment.prototype.sync = defaultSync;
+ });
+
+ mediaFrame.open();
+ },
+
+ /**
+ * Get props which are merged on top of the model when an embed is chosen (as opposed to an attachment).
+ *
+ * @return {Object} Reset/override props.
+ */
+ getEmbedResetProps: function getEmbedResetProps() {
+ return _.extend(
+ component.MediaWidgetControl.prototype.getEmbedResetProps.call( this ),
+ {
+ size: 'full',
+ width: 0,
+ height: 0
+ }
+ );
+ },
+
+ /**
+ * Get the instance props from the media selection frame.
+ *
+ * Prevent the image_title attribute from being initially set when adding an image from the media library.
+ *
+ * @param {wp.media.view.MediaFrame.Select} mediaFrame - Select frame.
+ * @return {Object} Props.
+ */
+ getModelPropsFromMediaFrame: function getModelPropsFromMediaFrame( mediaFrame ) {
+ var control = this;
+ return _.omit(
+ component.MediaWidgetControl.prototype.getModelPropsFromMediaFrame.call( control, mediaFrame ),
+ 'image_title'
+ );
+ },
+
+ /**
+ * Map model props to preview template props.
+ *
+ * @return {Object} Preview template props.
+ */
+ mapModelToPreviewTemplateProps: function mapModelToPreviewTemplateProps() {
+ var control = this, previewTemplateProps, url;
+ url = control.model.get( 'url' );
+ previewTemplateProps = component.MediaWidgetControl.prototype.mapModelToPreviewTemplateProps.call( control );
+ previewTemplateProps.currentFilename = url ? url.replace( /\?.*$/, '' ).replace( /^.+\//, '' ) : '';
+ previewTemplateProps.link_url = control.model.get( 'link_url' );
+ return previewTemplateProps;
+ }
+ });
+
+ // Exports.
+ component.controlConstructors.media_image = ImageWidgetControl;
+ component.modelConstructors.media_image = ImageWidgetModel;
+
+})( wp.mediaWidgets, jQuery );
diff --git a/wp-admin/js/widgets/media-image-widget.min.js b/wp-admin/js/widgets/media-image-widget.min.js
new file mode 100644
index 0000000..fd3f5eb
--- /dev/null
+++ b/wp-admin/js/widgets/media-image-widget.min.js
@@ -0,0 +1,2 @@
+/*! This file is auto-generated */
+!function(a,o){"use strict";var e=a.MediaWidgetModel.extend({}),t=a.MediaWidgetControl.extend({events:_.extend({},a.MediaWidgetControl.prototype.events,{"click .media-widget-preview.populated":"editMedia"}),renderPreview:function(){var e,t,i=this;(i.model.get("attachment_id")||i.model.get("url"))&&(t=i.$el.find(".media-widget-preview"),e=wp.template("wp-media-widget-image-preview"),t.html(e(i.previewTemplateProps.toJSON())),t.addClass("populated"),i.$el.find(".link").is(document.activeElement)||(e=i.$el.find(".media-widget-fields"),t=wp.template("wp-media-widget-image-fields"),e.html(t(i.previewTemplateProps.toJSON()))))},editMedia:function(){var i,e,a=this,t=a.mapModelToMediaFrameProps(a.model.toJSON());"none"===t.link&&(t.linkUrl=""),(i=wp.media({frame:"image",state:"image-details",metadata:t})).$el.addClass("media-widget"),t=function(){var e=i.state().attributes.image.toJSON(),t=e.link;e.link=e.linkUrl,a.selectedAttachment.set(e),a.displaySettings.set("link",t),a.model.set(_.extend(a.mapMediaToModelProps(e),{error:!1}))},i.state("image-details").on("update",t),i.state("replace-image").on("replace",t),e=wp.media.model.Attachment.prototype.sync,wp.media.model.Attachment.prototype.sync=function(){return o.Deferred().rejectWith(this).promise()},i.on("close",function(){i.detach(),wp.media.model.Attachment.prototype.sync=e}),i.open()},getEmbedResetProps:function(){return _.extend(a.MediaWidgetControl.prototype.getEmbedResetProps.call(this),{size:"full",width:0,height:0})},getModelPropsFromMediaFrame:function(e){return _.omit(a.MediaWidgetControl.prototype.getModelPropsFromMediaFrame.call(this,e),"image_title")},mapModelToPreviewTemplateProps:function(){var e=this,t=e.model.get("url"),i=a.MediaWidgetControl.prototype.mapModelToPreviewTemplateProps.call(e);return i.currentFilename=t?t.replace(/\?.*$/,"").replace(/^.+\//,""):"",i.link_url=e.model.get("link_url"),i}});a.controlConstructors.media_image=t,a.modelConstructors.media_image=e}(wp.mediaWidgets,jQuery); \ No newline at end of file
diff --git a/wp-admin/js/widgets/media-video-widget.js b/wp-admin/js/widgets/media-video-widget.js
new file mode 100644
index 0000000..56a8ff1
--- /dev/null
+++ b/wp-admin/js/widgets/media-video-widget.js
@@ -0,0 +1,256 @@
+/**
+ * @output wp-admin/js/widgets/media-video-widget.js
+ */
+
+/* eslint consistent-this: [ "error", "control" ] */
+(function( component ) {
+ 'use strict';
+
+ var VideoWidgetModel, VideoWidgetControl, VideoDetailsMediaFrame;
+
+ /**
+ * Custom video details frame that removes the replace-video state.
+ *
+ * @class wp.mediaWidgets.controlConstructors~VideoDetailsMediaFrame
+ * @augments wp.media.view.MediaFrame.VideoDetails
+ *
+ * @private
+ */
+ VideoDetailsMediaFrame = wp.media.view.MediaFrame.VideoDetails.extend(/** @lends wp.mediaWidgets.controlConstructors~VideoDetailsMediaFrame.prototype */{
+
+ /**
+ * Create the default states.
+ *
+ * @return {void}
+ */
+ createStates: function createStates() {
+ this.states.add([
+ new wp.media.controller.VideoDetails({
+ media: this.media
+ }),
+
+ new wp.media.controller.MediaLibrary({
+ type: 'video',
+ id: 'add-video-source',
+ title: wp.media.view.l10n.videoAddSourceTitle,
+ toolbar: 'add-video-source',
+ media: this.media,
+ menu: false
+ }),
+
+ new wp.media.controller.MediaLibrary({
+ type: 'text',
+ id: 'add-track',
+ title: wp.media.view.l10n.videoAddTrackTitle,
+ toolbar: 'add-track',
+ media: this.media,
+ menu: 'video-details'
+ })
+ ]);
+ }
+ });
+
+ /**
+ * Video widget model.
+ *
+ * See WP_Widget_Video::enqueue_admin_scripts() for amending prototype from PHP exports.
+ *
+ * @class wp.mediaWidgets.modelConstructors.media_video
+ * @augments wp.mediaWidgets.MediaWidgetModel
+ */
+ VideoWidgetModel = component.MediaWidgetModel.extend({});
+
+ /**
+ * Video widget control.
+ *
+ * See WP_Widget_Video::enqueue_admin_scripts() for amending prototype from PHP exports.
+ *
+ * @class wp.mediaWidgets.controlConstructors.media_video
+ * @augments wp.mediaWidgets.MediaWidgetControl
+ */
+ VideoWidgetControl = component.MediaWidgetControl.extend(/** @lends wp.mediaWidgets.controlConstructors.media_video.prototype */{
+
+ /**
+ * Show display settings.
+ *
+ * @type {boolean}
+ */
+ showDisplaySettings: false,
+
+ /**
+ * Cache of oembed responses.
+ *
+ * @type {Object}
+ */
+ oembedResponses: {},
+
+ /**
+ * Map model props to media frame props.
+ *
+ * @param {Object} modelProps - Model props.
+ * @return {Object} Media frame props.
+ */
+ mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) {
+ var control = this, mediaFrameProps;
+ mediaFrameProps = component.MediaWidgetControl.prototype.mapModelToMediaFrameProps.call( control, modelProps );
+ mediaFrameProps.link = 'embed';
+ return mediaFrameProps;
+ },
+
+ /**
+ * Fetches embed data for external videos.
+ *
+ * @return {void}
+ */
+ fetchEmbed: function fetchEmbed() {
+ var control = this, url;
+ url = control.model.get( 'url' );
+
+ // If we already have a local cache of the embed response, return.
+ if ( control.oembedResponses[ url ] ) {
+ return;
+ }
+
+ // If there is an in-flight embed request, abort it.
+ if ( control.fetchEmbedDfd && 'pending' === control.fetchEmbedDfd.state() ) {
+ control.fetchEmbedDfd.abort();
+ }
+
+ control.fetchEmbedDfd = wp.apiRequest({
+ url: wp.media.view.settings.oEmbedProxyUrl,
+ data: {
+ url: control.model.get( 'url' ),
+ maxwidth: control.model.get( 'width' ),
+ maxheight: control.model.get( 'height' ),
+ discover: false
+ },
+ type: 'GET',
+ dataType: 'json',
+ context: control
+ });
+
+ control.fetchEmbedDfd.done( function( response ) {
+ control.oembedResponses[ url ] = response;
+ control.renderPreview();
+ });
+
+ control.fetchEmbedDfd.fail( function() {
+ control.oembedResponses[ url ] = null;
+ });
+ },
+
+ /**
+ * Whether a url is a supported external host.
+ *
+ * @deprecated since 4.9.
+ *
+ * @return {boolean} Whether url is a supported video host.
+ */
+ isHostedVideo: function isHostedVideo() {
+ return true;
+ },
+
+ /**
+ * Render preview.
+ *
+ * @return {void}
+ */
+ renderPreview: function renderPreview() {
+ var control = this, previewContainer, previewTemplate, attachmentId, attachmentUrl, poster, html = '', isOEmbed = false, mime, error, urlParser, matches;
+ attachmentId = control.model.get( 'attachment_id' );
+ attachmentUrl = control.model.get( 'url' );
+ error = control.model.get( 'error' );
+
+ if ( ! attachmentId && ! attachmentUrl ) {
+ return;
+ }
+
+ // Verify the selected attachment mime is supported.
+ mime = control.selectedAttachment.get( 'mime' );
+ if ( mime && attachmentId ) {
+ if ( ! _.contains( _.values( wp.media.view.settings.embedMimes ), mime ) ) {
+ error = 'unsupported_file_type';
+ }
+ } else if ( ! attachmentId ) {
+ urlParser = document.createElement( 'a' );
+ urlParser.href = attachmentUrl;
+ matches = urlParser.pathname.toLowerCase().match( /\.(\w+)$/ );
+ if ( matches ) {
+ if ( ! _.contains( _.keys( wp.media.view.settings.embedMimes ), matches[1] ) ) {
+ error = 'unsupported_file_type';
+ }
+ } else {
+ isOEmbed = true;
+ }
+ }
+
+ if ( isOEmbed ) {
+ control.fetchEmbed();
+ if ( control.oembedResponses[ attachmentUrl ] ) {
+ poster = control.oembedResponses[ attachmentUrl ].thumbnail_url;
+ html = control.oembedResponses[ attachmentUrl ].html.replace( /\swidth="\d+"/, ' width="100%"' ).replace( /\sheight="\d+"/, '' );
+ }
+ }
+
+ previewContainer = control.$el.find( '.media-widget-preview' );
+ previewTemplate = wp.template( 'wp-media-widget-video-preview' );
+
+ previewContainer.html( previewTemplate({
+ model: {
+ attachment_id: attachmentId,
+ html: html,
+ src: attachmentUrl,
+ poster: poster
+ },
+ is_oembed: isOEmbed,
+ error: error
+ }));
+ wp.mediaelement.initialize();
+ },
+
+ /**
+ * Open the media image-edit frame to modify the selected item.
+ *
+ * @return {void}
+ */
+ editMedia: function editMedia() {
+ var control = this, mediaFrame, metadata, updateCallback;
+
+ metadata = control.mapModelToMediaFrameProps( control.model.toJSON() );
+
+ // Set up the media frame.
+ mediaFrame = new VideoDetailsMediaFrame({
+ frame: 'video',
+ state: 'video-details',
+ metadata: metadata
+ });
+ wp.media.frame = mediaFrame;
+ mediaFrame.$el.addClass( 'media-widget' );
+
+ updateCallback = function( mediaFrameProps ) {
+
+ // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview.
+ control.selectedAttachment.set( mediaFrameProps );
+
+ control.model.set( _.extend(
+ _.omit( control.model.defaults(), 'title' ),
+ control.mapMediaToModelProps( mediaFrameProps ),
+ { error: false }
+ ) );
+ };
+
+ mediaFrame.state( 'video-details' ).on( 'update', updateCallback );
+ mediaFrame.state( 'replace-video' ).on( 'replace', updateCallback );
+ mediaFrame.on( 'close', function() {
+ mediaFrame.detach();
+ });
+
+ mediaFrame.open();
+ }
+ });
+
+ // Exports.
+ component.controlConstructors.media_video = VideoWidgetControl;
+ component.modelConstructors.media_video = VideoWidgetModel;
+
+})( wp.mediaWidgets );
diff --git a/wp-admin/js/widgets/media-video-widget.min.js b/wp-admin/js/widgets/media-video-widget.min.js
new file mode 100644
index 0000000..653d52c
--- /dev/null
+++ b/wp-admin/js/widgets/media-video-widget.min.js
@@ -0,0 +1,2 @@
+/*! This file is auto-generated */
+!function(t){"use strict";var i=wp.media.view.MediaFrame.VideoDetails.extend({createStates:function(){this.states.add([new wp.media.controller.VideoDetails({media:this.media}),new wp.media.controller.MediaLibrary({type:"video",id:"add-video-source",title:wp.media.view.l10n.videoAddSourceTitle,toolbar:"add-video-source",media:this.media,menu:!1}),new wp.media.controller.MediaLibrary({type:"text",id:"add-track",title:wp.media.view.l10n.videoAddTrackTitle,toolbar:"add-track",media:this.media,menu:"video-details"})])}}),e=t.MediaWidgetModel.extend({}),d=t.MediaWidgetControl.extend({showDisplaySettings:!1,oembedResponses:{},mapModelToMediaFrameProps:function(e){e=t.MediaWidgetControl.prototype.mapModelToMediaFrameProps.call(this,e);return e.link="embed",e},fetchEmbed:function(){var t=this,d=t.model.get("url");t.oembedResponses[d]||(t.fetchEmbedDfd&&"pending"===t.fetchEmbedDfd.state()&&t.fetchEmbedDfd.abort(),t.fetchEmbedDfd=wp.apiRequest({url:wp.media.view.settings.oEmbedProxyUrl,data:{url:t.model.get("url"),maxwidth:t.model.get("width"),maxheight:t.model.get("height"),discover:!1},type:"GET",dataType:"json",context:t}),t.fetchEmbedDfd.done(function(e){t.oembedResponses[d]=e,t.renderPreview()}),t.fetchEmbedDfd.fail(function(){t.oembedResponses[d]=null}))},isHostedVideo:function(){return!0},renderPreview:function(){var e,t,d=this,i="",o=!1,a=d.model.get("attachment_id"),s=d.model.get("url"),m=d.model.get("error");(a||s)&&((t=d.selectedAttachment.get("mime"))&&a?_.contains(_.values(wp.media.view.settings.embedMimes),t)||(m="unsupported_file_type"):a||((t=document.createElement("a")).href=s,(t=t.pathname.toLowerCase().match(/\.(\w+)$/))?_.contains(_.keys(wp.media.view.settings.embedMimes),t[1])||(m="unsupported_file_type"):o=!0),o&&(d.fetchEmbed(),d.oembedResponses[s])&&(e=d.oembedResponses[s].thumbnail_url,i=d.oembedResponses[s].html.replace(/\swidth="\d+"/,' width="100%"').replace(/\sheight="\d+"/,"")),t=d.$el.find(".media-widget-preview"),d=wp.template("wp-media-widget-video-preview"),t.html(d({model:{attachment_id:a,html:i,src:s,poster:e},is_oembed:o,error:m})),wp.mediaelement.initialize())},editMedia:function(){var t=this,e=t.mapModelToMediaFrameProps(t.model.toJSON()),d=new i({frame:"video",state:"video-details",metadata:e});(wp.media.frame=d).$el.addClass("media-widget"),e=function(e){t.selectedAttachment.set(e),t.model.set(_.extend(_.omit(t.model.defaults(),"title"),t.mapMediaToModelProps(e),{error:!1}))},d.state("video-details").on("update",e),d.state("replace-video").on("replace",e),d.on("close",function(){d.detach()}),d.open()}});t.controlConstructors.media_video=d,t.modelConstructors.media_video=e}(wp.mediaWidgets); \ No newline at end of file
diff --git a/wp-admin/js/widgets/media-widgets.js b/wp-admin/js/widgets/media-widgets.js
new file mode 100644
index 0000000..2ee00a8
--- /dev/null
+++ b/wp-admin/js/widgets/media-widgets.js
@@ -0,0 +1,1336 @@
+/**
+ * @output wp-admin/js/widgets/media-widgets.js
+ */
+
+/* eslint consistent-this: [ "error", "control" ] */
+
+/**
+ * @namespace wp.mediaWidgets
+ * @memberOf wp
+ */
+wp.mediaWidgets = ( function( $ ) {
+ 'use strict';
+
+ var component = {};
+
+ /**
+ * Widget control (view) constructors, mapping widget id_base to subclass of MediaWidgetControl.
+ *
+ * Media widgets register themselves by assigning subclasses of MediaWidgetControl onto this object by widget ID base.
+ *
+ * @memberOf wp.mediaWidgets
+ *
+ * @type {Object.<string, wp.mediaWidgets.MediaWidgetModel>}
+ */
+ component.controlConstructors = {};
+
+ /**
+ * Widget model constructors, mapping widget id_base to subclass of MediaWidgetModel.
+ *
+ * Media widgets register themselves by assigning subclasses of MediaWidgetControl onto this object by widget ID base.
+ *
+ * @memberOf wp.mediaWidgets
+ *
+ * @type {Object.<string, wp.mediaWidgets.MediaWidgetModel>}
+ */
+ component.modelConstructors = {};
+
+ component.PersistentDisplaySettingsLibrary = wp.media.controller.Library.extend(/** @lends wp.mediaWidgets.PersistentDisplaySettingsLibrary.prototype */{
+
+ /**
+ * Library which persists the customized display settings across selections.
+ *
+ * @constructs wp.mediaWidgets.PersistentDisplaySettingsLibrary
+ * @augments wp.media.controller.Library
+ *
+ * @param {Object} options - Options.
+ *
+ * @return {void}
+ */
+ initialize: function initialize( options ) {
+ _.bindAll( this, 'handleDisplaySettingChange' );
+ wp.media.controller.Library.prototype.initialize.call( this, options );
+ },
+
+ /**
+ * Sync changes to the current display settings back into the current customized.
+ *
+ * @param {Backbone.Model} displaySettings - Modified display settings.
+ * @return {void}
+ */
+ handleDisplaySettingChange: function handleDisplaySettingChange( displaySettings ) {
+ this.get( 'selectedDisplaySettings' ).set( displaySettings.attributes );
+ },
+
+ /**
+ * Get the display settings model.
+ *
+ * Model returned is updated with the current customized display settings,
+ * and an event listener is added so that changes made to the settings
+ * will sync back into the model storing the session's customized display
+ * settings.
+ *
+ * @param {Backbone.Model} model - Display settings model.
+ * @return {Backbone.Model} Display settings model.
+ */
+ display: function getDisplaySettingsModel( model ) {
+ var display, selectedDisplaySettings = this.get( 'selectedDisplaySettings' );
+ display = wp.media.controller.Library.prototype.display.call( this, model );
+
+ display.off( 'change', this.handleDisplaySettingChange ); // Prevent duplicated event handlers.
+ display.set( selectedDisplaySettings.attributes );
+ if ( 'custom' === selectedDisplaySettings.get( 'link_type' ) ) {
+ display.linkUrl = selectedDisplaySettings.get( 'link_url' );
+ }
+ display.on( 'change', this.handleDisplaySettingChange );
+ return display;
+ }
+ });
+
+ /**
+ * Extended view for managing the embed UI.
+ *
+ * @class wp.mediaWidgets.MediaEmbedView
+ * @augments wp.media.view.Embed
+ */
+ component.MediaEmbedView = wp.media.view.Embed.extend(/** @lends wp.mediaWidgets.MediaEmbedView.prototype */{
+
+ /**
+ * Initialize.
+ *
+ * @since 4.9.0
+ *
+ * @param {Object} options - Options.
+ * @return {void}
+ */
+ initialize: function( options ) {
+ var view = this, embedController; // eslint-disable-line consistent-this
+ wp.media.view.Embed.prototype.initialize.call( view, options );
+ if ( 'image' !== view.controller.options.mimeType ) {
+ embedController = view.controller.states.get( 'embed' );
+ embedController.off( 'scan', embedController.scanImage, embedController );
+ }
+ },
+
+ /**
+ * Refresh embed view.
+ *
+ * Forked override of {wp.media.view.Embed#refresh()} to suppress irrelevant "link text" field.
+ *
+ * @return {void}
+ */
+ refresh: function refresh() {
+ /**
+ * @class wp.mediaWidgets~Constructor
+ */
+ var Constructor;
+
+ if ( 'image' === this.controller.options.mimeType ) {
+ Constructor = wp.media.view.EmbedImage;
+ } else {
+
+ // This should be eliminated once #40450 lands of when this is merged into core.
+ Constructor = wp.media.view.EmbedLink.extend(/** @lends wp.mediaWidgets~Constructor.prototype */{
+
+ /**
+ * Set the disabled state on the Add to Widget button.
+ *
+ * @param {boolean} disabled - Disabled.
+ * @return {void}
+ */
+ setAddToWidgetButtonDisabled: function setAddToWidgetButtonDisabled( disabled ) {
+ this.views.parent.views.parent.views.get( '.media-frame-toolbar' )[0].$el.find( '.media-button-select' ).prop( 'disabled', disabled );
+ },
+
+ /**
+ * Set or clear an error notice.
+ *
+ * @param {string} notice - Notice.
+ * @return {void}
+ */
+ setErrorNotice: function setErrorNotice( notice ) {
+ var embedLinkView = this, noticeContainer; // eslint-disable-line consistent-this
+
+ noticeContainer = embedLinkView.views.parent.$el.find( '> .notice:first-child' );
+ if ( ! notice ) {
+ if ( noticeContainer.length ) {
+ noticeContainer.slideUp( 'fast' );
+ }
+ } else {
+ if ( ! noticeContainer.length ) {
+ noticeContainer = $( '<div class="media-widget-embed-notice notice notice-error notice-alt"></div>' );
+ noticeContainer.hide();
+ embedLinkView.views.parent.$el.prepend( noticeContainer );
+ }
+ noticeContainer.empty();
+ noticeContainer.append( $( '<p>', {
+ html: notice
+ }));
+ noticeContainer.slideDown( 'fast' );
+ }
+ },
+
+ /**
+ * Update oEmbed.
+ *
+ * @since 4.9.0
+ *
+ * @return {void}
+ */
+ updateoEmbed: function() {
+ var embedLinkView = this, url; // eslint-disable-line consistent-this
+
+ url = embedLinkView.model.get( 'url' );
+
+ // Abort if the URL field was emptied out.
+ if ( ! url ) {
+ embedLinkView.setErrorNotice( '' );
+ embedLinkView.setAddToWidgetButtonDisabled( true );
+ return;
+ }
+
+ if ( ! url.match( /^(http|https):\/\/.+\// ) ) {
+ embedLinkView.controller.$el.find( '#embed-url-field' ).addClass( 'invalid' );
+ embedLinkView.setAddToWidgetButtonDisabled( true );
+ }
+
+ wp.media.view.EmbedLink.prototype.updateoEmbed.call( embedLinkView );
+ },
+
+ /**
+ * Fetch media.
+ *
+ * @return {void}
+ */
+ fetch: function() {
+ var embedLinkView = this, fetchSuccess, matches, fileExt, urlParser, url, re, youTubeEmbedMatch; // eslint-disable-line consistent-this
+ url = embedLinkView.model.get( 'url' );
+
+ if ( embedLinkView.dfd && 'pending' === embedLinkView.dfd.state() ) {
+ embedLinkView.dfd.abort();
+ }
+
+ fetchSuccess = function( response ) {
+ embedLinkView.renderoEmbed({
+ data: {
+ body: response
+ }
+ });
+
+ embedLinkView.controller.$el.find( '#embed-url-field' ).removeClass( 'invalid' );
+ embedLinkView.setErrorNotice( '' );
+ embedLinkView.setAddToWidgetButtonDisabled( false );
+ };
+
+ urlParser = document.createElement( 'a' );
+ urlParser.href = url;
+ matches = urlParser.pathname.toLowerCase().match( /\.(\w+)$/ );
+ if ( matches ) {
+ fileExt = matches[1];
+ if ( ! wp.media.view.settings.embedMimes[ fileExt ] ) {
+ embedLinkView.renderFail();
+ } else if ( 0 !== wp.media.view.settings.embedMimes[ fileExt ].indexOf( embedLinkView.controller.options.mimeType ) ) {
+ embedLinkView.renderFail();
+ } else {
+ fetchSuccess( '<!--success-->' );
+ }
+ return;
+ }
+
+ // Support YouTube embed links.
+ re = /https?:\/\/www\.youtube\.com\/embed\/([^/]+)/;
+ youTubeEmbedMatch = re.exec( url );
+ if ( youTubeEmbedMatch ) {
+ url = 'https://www.youtube.com/watch?v=' + youTubeEmbedMatch[ 1 ];
+ // silently change url to proper oembed-able version.
+ embedLinkView.model.attributes.url = url;
+ }
+
+ embedLinkView.dfd = wp.apiRequest({
+ url: wp.media.view.settings.oEmbedProxyUrl,
+ data: {
+ url: url,
+ maxwidth: embedLinkView.model.get( 'width' ),
+ maxheight: embedLinkView.model.get( 'height' ),
+ discover: false
+ },
+ type: 'GET',
+ dataType: 'json',
+ context: embedLinkView
+ });
+
+ embedLinkView.dfd.done( function( response ) {
+ if ( embedLinkView.controller.options.mimeType !== response.type ) {
+ embedLinkView.renderFail();
+ return;
+ }
+ fetchSuccess( response.html );
+ });
+ embedLinkView.dfd.fail( _.bind( embedLinkView.renderFail, embedLinkView ) );
+ },
+
+ /**
+ * Handle render failure.
+ *
+ * Overrides the {EmbedLink#renderFail()} method to prevent showing the "Link Text" field.
+ * The element is getting display:none in the stylesheet, but the underlying method uses
+ * uses {jQuery.fn.show()} which adds an inline style. This avoids the need for !important.
+ *
+ * @return {void}
+ */
+ renderFail: function renderFail() {
+ var embedLinkView = this; // eslint-disable-line consistent-this
+ embedLinkView.controller.$el.find( '#embed-url-field' ).addClass( 'invalid' );
+ embedLinkView.setErrorNotice( embedLinkView.controller.options.invalidEmbedTypeError || 'ERROR' );
+ embedLinkView.setAddToWidgetButtonDisabled( true );
+ }
+ });
+ }
+
+ this.settings( new Constructor({
+ controller: this.controller,
+ model: this.model.props,
+ priority: 40
+ }));
+ }
+ });
+
+ /**
+ * Custom media frame for selecting uploaded media or providing media by URL.
+ *
+ * @class wp.mediaWidgets.MediaFrameSelect
+ * @augments wp.media.view.MediaFrame.Post
+ */
+ component.MediaFrameSelect = wp.media.view.MediaFrame.Post.extend(/** @lends wp.mediaWidgets.MediaFrameSelect.prototype */{
+
+ /**
+ * Create the default states.
+ *
+ * @return {void}
+ */
+ createStates: function createStates() {
+ var mime = this.options.mimeType, specificMimes = [];
+ _.each( wp.media.view.settings.embedMimes, function( embedMime ) {
+ if ( 0 === embedMime.indexOf( mime ) ) {
+ specificMimes.push( embedMime );
+ }
+ });
+ if ( specificMimes.length > 0 ) {
+ mime = specificMimes;
+ }
+
+ this.states.add([
+
+ // Main states.
+ new component.PersistentDisplaySettingsLibrary({
+ id: 'insert',
+ title: this.options.title,
+ selection: this.options.selection,
+ priority: 20,
+ toolbar: 'main-insert',
+ filterable: 'dates',
+ library: wp.media.query({
+ type: mime
+ }),
+ multiple: false,
+ editable: true,
+
+ selectedDisplaySettings: this.options.selectedDisplaySettings,
+ displaySettings: _.isUndefined( this.options.showDisplaySettings ) ? true : this.options.showDisplaySettings,
+ displayUserSettings: false // We use the display settings from the current/default widget instance props.
+ }),
+
+ new wp.media.controller.EditImage({ model: this.options.editImage }),
+
+ // Embed states.
+ new wp.media.controller.Embed({
+ metadata: this.options.metadata,
+ type: 'image' === this.options.mimeType ? 'image' : 'link',
+ invalidEmbedTypeError: this.options.invalidEmbedTypeError
+ })
+ ]);
+ },
+
+ /**
+ * Main insert toolbar.
+ *
+ * Forked override of {wp.media.view.MediaFrame.Post#mainInsertToolbar()} to override text.
+ *
+ * @param {wp.Backbone.View} view - Toolbar view.
+ * @this {wp.media.controller.Library}
+ * @return {void}
+ */
+ mainInsertToolbar: function mainInsertToolbar( view ) {
+ var controller = this; // eslint-disable-line consistent-this
+ view.set( 'insert', {
+ style: 'primary',
+ priority: 80,
+ text: controller.options.text, // The whole reason for the fork.
+ requires: { selection: true },
+
+ /**
+ * Handle click.
+ *
+ * @ignore
+ *
+ * @fires wp.media.controller.State#insert()
+ * @return {void}
+ */
+ click: function onClick() {
+ var state = controller.state(),
+ selection = state.get( 'selection' );
+
+ controller.close();
+ state.trigger( 'insert', selection ).reset();
+ }
+ });
+ },
+
+ /**
+ * Main embed toolbar.
+ *
+ * Forked override of {wp.media.view.MediaFrame.Post#mainEmbedToolbar()} to override text.
+ *
+ * @param {wp.Backbone.View} toolbar - Toolbar view.
+ * @this {wp.media.controller.Library}
+ * @return {void}
+ */
+ mainEmbedToolbar: function mainEmbedToolbar( toolbar ) {
+ toolbar.view = new wp.media.view.Toolbar.Embed({
+ controller: this,
+ text: this.options.text,
+ event: 'insert'
+ });
+ },
+
+ /**
+ * Embed content.
+ *
+ * Forked override of {wp.media.view.MediaFrame.Post#embedContent()} to suppress irrelevant "link text" field.
+ *
+ * @return {void}
+ */
+ embedContent: function embedContent() {
+ var view = new component.MediaEmbedView({
+ controller: this,
+ model: this.state()
+ }).render();
+
+ this.content.set( view );
+ }
+ });
+
+ component.MediaWidgetControl = Backbone.View.extend(/** @lends wp.mediaWidgets.MediaWidgetControl.prototype */{
+
+ /**
+ * Translation strings.
+ *
+ * The mapping of translation strings is handled by media widget subclasses,
+ * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
+ *
+ * @type {Object}
+ */
+ l10n: {
+ add_to_widget: '{{add_to_widget}}',
+ add_media: '{{add_media}}'
+ },
+
+ /**
+ * Widget ID base.
+ *
+ * This may be defined by the subclass. It may be exported from PHP to JS
+ * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts(). If not,
+ * it will attempt to be discovered by looking to see if this control
+ * instance extends each member of component.controlConstructors, and if
+ * it does extend one, will use the key as the id_base.
+ *
+ * @type {string}
+ */
+ id_base: '',
+
+ /**
+ * Mime type.
+ *
+ * This must be defined by the subclass. It may be exported from PHP to JS
+ * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
+ *
+ * @type {string}
+ */
+ mime_type: '',
+
+ /**
+ * View events.
+ *
+ * @type {Object}
+ */
+ events: {
+ 'click .notice-missing-attachment a': 'handleMediaLibraryLinkClick',
+ 'click .select-media': 'selectMedia',
+ 'click .placeholder': 'selectMedia',
+ 'click .edit-media': 'editMedia'
+ },
+
+ /**
+ * Show display settings.
+ *
+ * @type {boolean}
+ */
+ showDisplaySettings: true,
+
+ /**
+ * Media Widget Control.
+ *
+ * @constructs wp.mediaWidgets.MediaWidgetControl
+ * @augments Backbone.View
+ * @abstract
+ *
+ * @param {Object} options - Options.
+ * @param {Backbone.Model} options.model - Model.
+ * @param {jQuery} options.el - Control field container element.
+ * @param {jQuery} options.syncContainer - Container element where fields are synced for the server.
+ *
+ * @return {void}
+ */
+ initialize: function initialize( options ) {
+ var control = this;
+
+ Backbone.View.prototype.initialize.call( control, options );
+
+ if ( ! ( control.model instanceof component.MediaWidgetModel ) ) {
+ throw new Error( 'Missing options.model' );
+ }
+ if ( ! options.el ) {
+ throw new Error( 'Missing options.el' );
+ }
+ if ( ! options.syncContainer ) {
+ throw new Error( 'Missing options.syncContainer' );
+ }
+
+ control.syncContainer = options.syncContainer;
+
+ control.$el.addClass( 'media-widget-control' );
+
+ // Allow methods to be passed in with control context preserved.
+ _.bindAll( control, 'syncModelToInputs', 'render', 'updateSelectedAttachment', 'renderPreview' );
+
+ if ( ! control.id_base ) {
+ _.find( component.controlConstructors, function( Constructor, idBase ) {
+ if ( control instanceof Constructor ) {
+ control.id_base = idBase;
+ return true;
+ }
+ return false;
+ });
+ if ( ! control.id_base ) {
+ throw new Error( 'Missing id_base.' );
+ }
+ }
+
+ // Track attributes needed to renderPreview in it's own model.
+ control.previewTemplateProps = new Backbone.Model( control.mapModelToPreviewTemplateProps() );
+
+ // Re-render the preview when the attachment changes.
+ control.selectedAttachment = new wp.media.model.Attachment();
+ control.renderPreview = _.debounce( control.renderPreview );
+ control.listenTo( control.previewTemplateProps, 'change', control.renderPreview );
+
+ // Make sure a copy of the selected attachment is always fetched.
+ control.model.on( 'change:attachment_id', control.updateSelectedAttachment );
+ control.model.on( 'change:url', control.updateSelectedAttachment );
+ control.updateSelectedAttachment();
+
+ /*
+ * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state.
+ * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model
+ * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>.
+ */
+ control.listenTo( control.model, 'change', control.syncModelToInputs );
+ control.listenTo( control.model, 'change', control.syncModelToPreviewProps );
+ control.listenTo( control.model, 'change', control.render );
+
+ // Update the title.
+ control.$el.on( 'input change', '.title', function updateTitle() {
+ control.model.set({
+ title: $( this ).val().trim()
+ });
+ });
+
+ // Update link_url attribute.
+ control.$el.on( 'input change', '.link', function updateLinkUrl() {
+ var linkUrl = $( this ).val().trim(), linkType = 'custom';
+ if ( control.selectedAttachment.get( 'linkUrl' ) === linkUrl || control.selectedAttachment.get( 'link' ) === linkUrl ) {
+ linkType = 'post';
+ } else if ( control.selectedAttachment.get( 'url' ) === linkUrl ) {
+ linkType = 'file';
+ }
+ control.model.set( {
+ link_url: linkUrl,
+ link_type: linkType
+ });
+
+ // Update display settings for the next time the user opens to select from the media library.
+ control.displaySettings.set( {
+ link: linkType,
+ linkUrl: linkUrl
+ });
+ });
+
+ /*
+ * Copy current display settings from the widget model to serve as basis
+ * of customized display settings for the current media frame session.
+ * Changes to display settings will be synced into this model, and
+ * when a new selection is made, the settings from this will be synced
+ * into that AttachmentDisplay's model to persist the setting changes.
+ */
+ control.displaySettings = new Backbone.Model( _.pick(
+ control.mapModelToMediaFrameProps(
+ _.extend( control.model.defaults(), control.model.toJSON() )
+ ),
+ _.keys( wp.media.view.settings.defaultProps )
+ ) );
+ },
+
+ /**
+ * Update the selected attachment if necessary.
+ *
+ * @return {void}
+ */
+ updateSelectedAttachment: function updateSelectedAttachment() {
+ var control = this, attachment;
+
+ if ( 0 === control.model.get( 'attachment_id' ) ) {
+ control.selectedAttachment.clear();
+ control.model.set( 'error', false );
+ } else if ( control.model.get( 'attachment_id' ) !== control.selectedAttachment.get( 'id' ) ) {
+ attachment = new wp.media.model.Attachment({
+ id: control.model.get( 'attachment_id' )
+ });
+ attachment.fetch()
+ .done( function done() {
+ control.model.set( 'error', false );
+ control.selectedAttachment.set( attachment.toJSON() );
+ })
+ .fail( function fail() {
+ control.model.set( 'error', 'missing_attachment' );
+ });
+ }
+ },
+
+ /**
+ * Sync the model attributes to the hidden inputs, and update previewTemplateProps.
+ *
+ * @return {void}
+ */
+ syncModelToPreviewProps: function syncModelToPreviewProps() {
+ var control = this;
+ control.previewTemplateProps.set( control.mapModelToPreviewTemplateProps() );
+ },
+
+ /**
+ * Sync the model attributes to the hidden inputs, and update previewTemplateProps.
+ *
+ * @return {void}
+ */
+ syncModelToInputs: function syncModelToInputs() {
+ var control = this;
+ control.syncContainer.find( '.media-widget-instance-property' ).each( function() {
+ var input = $( this ), value, propertyName;
+ propertyName = input.data( 'property' );
+ value = control.model.get( propertyName );
+ if ( _.isUndefined( value ) ) {
+ return;
+ }
+
+ if ( 'array' === control.model.schema[ propertyName ].type && _.isArray( value ) ) {
+ value = value.join( ',' );
+ } else if ( 'boolean' === control.model.schema[ propertyName ].type ) {
+ value = value ? '1' : ''; // Because in PHP, strval( true ) === '1' && strval( false ) === ''.
+ } else {
+ value = String( value );
+ }
+
+ if ( input.val() !== value ) {
+ input.val( value );
+ input.trigger( 'change' );
+ }
+ });
+ },
+
+ /**
+ * Get template.
+ *
+ * @return {Function} Template.
+ */
+ template: function template() {
+ var control = this;
+ if ( ! $( '#tmpl-widget-media-' + control.id_base + '-control' ).length ) {
+ throw new Error( 'Missing widget control template for ' + control.id_base );
+ }
+ return wp.template( 'widget-media-' + control.id_base + '-control' );
+ },
+
+ /**
+ * Render template.
+ *
+ * @return {void}
+ */
+ render: function render() {
+ var control = this, titleInput;
+
+ if ( ! control.templateRendered ) {
+ control.$el.html( control.template()( control.model.toJSON() ) );
+ control.renderPreview(); // Hereafter it will re-render when control.selectedAttachment changes.
+ control.templateRendered = true;
+ }
+
+ titleInput = control.$el.find( '.title' );
+ if ( ! titleInput.is( document.activeElement ) ) {
+ titleInput.val( control.model.get( 'title' ) );
+ }
+
+ control.$el.toggleClass( 'selected', control.isSelected() );
+ },
+
+ /**
+ * Render media preview.
+ *
+ * @abstract
+ * @return {void}
+ */
+ renderPreview: function renderPreview() {
+ throw new Error( 'renderPreview must be implemented' );
+ },
+
+ /**
+ * Whether a media item is selected.
+ *
+ * @return {boolean} Whether selected and no error.
+ */
+ isSelected: function isSelected() {
+ var control = this;
+
+ if ( control.model.get( 'error' ) ) {
+ return false;
+ }
+
+ return Boolean( control.model.get( 'attachment_id' ) || control.model.get( 'url' ) );
+ },
+
+ /**
+ * Handle click on link to Media Library to open modal, such as the link that appears when in the missing attachment error notice.
+ *
+ * @param {jQuery.Event} event - Event.
+ * @return {void}
+ */
+ handleMediaLibraryLinkClick: function handleMediaLibraryLinkClick( event ) {
+ var control = this;
+ event.preventDefault();
+ control.selectMedia();
+ },
+
+ /**
+ * Open the media select frame to chose an item.
+ *
+ * @return {void}
+ */
+ selectMedia: function selectMedia() {
+ var control = this, selection, mediaFrame, defaultSync, mediaFrameProps, selectionModels = [];
+
+ if ( control.isSelected() && 0 !== control.model.get( 'attachment_id' ) ) {
+ selectionModels.push( control.selectedAttachment );
+ }
+
+ selection = new wp.media.model.Selection( selectionModels, { multiple: false } );
+
+ mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() );
+ if ( mediaFrameProps.size ) {
+ control.displaySettings.set( 'size', mediaFrameProps.size );
+ }
+
+ mediaFrame = new component.MediaFrameSelect({
+ title: control.l10n.add_media,
+ frame: 'post',
+ text: control.l10n.add_to_widget,
+ selection: selection,
+ mimeType: control.mime_type,
+ selectedDisplaySettings: control.displaySettings,
+ showDisplaySettings: control.showDisplaySettings,
+ metadata: mediaFrameProps,
+ state: control.isSelected() && 0 === control.model.get( 'attachment_id' ) ? 'embed' : 'insert',
+ invalidEmbedTypeError: control.l10n.unsupported_file_type
+ });
+ wp.media.frame = mediaFrame; // See wp.media().
+
+ // Handle selection of a media item.
+ mediaFrame.on( 'insert', function onInsert() {
+ var attachment = {}, state = mediaFrame.state();
+
+ // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview.
+ if ( 'embed' === state.get( 'id' ) ) {
+ _.extend( attachment, { id: 0 }, state.props.toJSON() );
+ } else {
+ _.extend( attachment, state.get( 'selection' ).first().toJSON() );
+ }
+
+ control.selectedAttachment.set( attachment );
+ control.model.set( 'error', false );
+
+ // Update widget instance.
+ control.model.set( control.getModelPropsFromMediaFrame( mediaFrame ) );
+ });
+
+ // Disable syncing of attachment changes back to server (except for deletions). See <https://core.trac.wordpress.org/ticket/40403>.
+ defaultSync = wp.media.model.Attachment.prototype.sync;
+ wp.media.model.Attachment.prototype.sync = function( method ) {
+ if ( 'delete' === method ) {
+ return defaultSync.apply( this, arguments );
+ } else {
+ return $.Deferred().rejectWith( this ).promise();
+ }
+ };
+ mediaFrame.on( 'close', function onClose() {
+ wp.media.model.Attachment.prototype.sync = defaultSync;
+ });
+
+ mediaFrame.$el.addClass( 'media-widget' );
+ mediaFrame.open();
+
+ // Clear the selected attachment when it is deleted in the media select frame.
+ if ( selection ) {
+ selection.on( 'destroy', function onDestroy( attachment ) {
+ if ( control.model.get( 'attachment_id' ) === attachment.get( 'id' ) ) {
+ control.model.set({
+ attachment_id: 0,
+ url: ''
+ });
+ }
+ });
+ }
+
+ /*
+ * Make sure focus is set inside of modal so that hitting Esc will close
+ * the modal and not inadvertently cause the widget to collapse in the customizer.
+ */
+ mediaFrame.$el.find( '.media-frame-menu .media-menu-item.active' ).focus();
+ },
+
+ /**
+ * Get the instance props from the media selection frame.
+ *
+ * @param {wp.media.view.MediaFrame.Select} mediaFrame - Select frame.
+ * @return {Object} Props.
+ */
+ getModelPropsFromMediaFrame: function getModelPropsFromMediaFrame( mediaFrame ) {
+ var control = this, state, mediaFrameProps, modelProps;
+
+ state = mediaFrame.state();
+ if ( 'insert' === state.get( 'id' ) ) {
+ mediaFrameProps = state.get( 'selection' ).first().toJSON();
+ mediaFrameProps.postUrl = mediaFrameProps.link;
+
+ if ( control.showDisplaySettings ) {
+ _.extend(
+ mediaFrameProps,
+ mediaFrame.content.get( '.attachments-browser' ).sidebar.get( 'display' ).model.toJSON()
+ );
+ }
+ if ( mediaFrameProps.sizes && mediaFrameProps.size && mediaFrameProps.sizes[ mediaFrameProps.size ] ) {
+ mediaFrameProps.url = mediaFrameProps.sizes[ mediaFrameProps.size ].url;
+ }
+ } else if ( 'embed' === state.get( 'id' ) ) {
+ mediaFrameProps = _.extend(
+ state.props.toJSON(),
+ { attachment_id: 0 }, // Because some media frames use `attachment_id` not `id`.
+ control.model.getEmbedResetProps()
+ );
+ } else {
+ throw new Error( 'Unexpected state: ' + state.get( 'id' ) );
+ }
+
+ if ( mediaFrameProps.id ) {
+ mediaFrameProps.attachment_id = mediaFrameProps.id;
+ }
+
+ modelProps = control.mapMediaToModelProps( mediaFrameProps );
+
+ // Clear the extension prop so sources will be reset for video and audio media.
+ _.each( wp.media.view.settings.embedExts, function( ext ) {
+ if ( ext in control.model.schema && modelProps.url !== modelProps[ ext ] ) {
+ modelProps[ ext ] = '';
+ }
+ });
+
+ return modelProps;
+ },
+
+ /**
+ * Map media frame props to model props.
+ *
+ * @param {Object} mediaFrameProps - Media frame props.
+ * @return {Object} Model props.
+ */
+ mapMediaToModelProps: function mapMediaToModelProps( mediaFrameProps ) {
+ var control = this, mediaFramePropToModelPropMap = {}, modelProps = {}, extension;
+ _.each( control.model.schema, function( fieldSchema, modelProp ) {
+
+ // Ignore widget title attribute.
+ if ( 'title' === modelProp ) {
+ return;
+ }
+ mediaFramePropToModelPropMap[ fieldSchema.media_prop || modelProp ] = modelProp;
+ });
+
+ _.each( mediaFrameProps, function( value, mediaProp ) {
+ var propName = mediaFramePropToModelPropMap[ mediaProp ] || mediaProp;
+ if ( control.model.schema[ propName ] ) {
+ modelProps[ propName ] = value;
+ }
+ });
+
+ if ( 'custom' === mediaFrameProps.size ) {
+ modelProps.width = mediaFrameProps.customWidth;
+ modelProps.height = mediaFrameProps.customHeight;
+ }
+
+ if ( 'post' === mediaFrameProps.link ) {
+ modelProps.link_url = mediaFrameProps.postUrl || mediaFrameProps.linkUrl;
+ } else if ( 'file' === mediaFrameProps.link ) {
+ modelProps.link_url = mediaFrameProps.url;
+ }
+
+ // Because some media frames use `id` instead of `attachment_id`.
+ if ( ! mediaFrameProps.attachment_id && mediaFrameProps.id ) {
+ modelProps.attachment_id = mediaFrameProps.id;
+ }
+
+ if ( mediaFrameProps.url ) {
+ extension = mediaFrameProps.url.replace( /#.*$/, '' ).replace( /\?.*$/, '' ).split( '.' ).pop().toLowerCase();
+ if ( extension in control.model.schema ) {
+ modelProps[ extension ] = mediaFrameProps.url;
+ }
+ }
+
+ // Always omit the titles derived from mediaFrameProps.
+ return _.omit( modelProps, 'title' );
+ },
+
+ /**
+ * Map model props to media frame props.
+ *
+ * @param {Object} modelProps - Model props.
+ * @return {Object} Media frame props.
+ */
+ mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) {
+ var control = this, mediaFrameProps = {};
+
+ _.each( modelProps, function( value, modelProp ) {
+ var fieldSchema = control.model.schema[ modelProp ] || {};
+ mediaFrameProps[ fieldSchema.media_prop || modelProp ] = value;
+ });
+
+ // Some media frames use attachment_id.
+ mediaFrameProps.attachment_id = mediaFrameProps.id;
+
+ if ( 'custom' === mediaFrameProps.size ) {
+ mediaFrameProps.customWidth = control.model.get( 'width' );
+ mediaFrameProps.customHeight = control.model.get( 'height' );
+ }
+
+ return mediaFrameProps;
+ },
+
+ /**
+ * Map model props to previewTemplateProps.
+ *
+ * @return {Object} Preview Template Props.
+ */
+ mapModelToPreviewTemplateProps: function mapModelToPreviewTemplateProps() {
+ var control = this, previewTemplateProps = {};
+ _.each( control.model.schema, function( value, prop ) {
+ if ( ! value.hasOwnProperty( 'should_preview_update' ) || value.should_preview_update ) {
+ previewTemplateProps[ prop ] = control.model.get( prop );
+ }
+ });
+
+ // Templates need to be aware of the error.
+ previewTemplateProps.error = control.model.get( 'error' );
+ return previewTemplateProps;
+ },
+
+ /**
+ * Open the media frame to modify the selected item.
+ *
+ * @abstract
+ * @return {void}
+ */
+ editMedia: function editMedia() {
+ throw new Error( 'editMedia not implemented' );
+ }
+ });
+
+ /**
+ * Media widget model.
+ *
+ * @class wp.mediaWidgets.MediaWidgetModel
+ * @augments Backbone.Model
+ */
+ component.MediaWidgetModel = Backbone.Model.extend(/** @lends wp.mediaWidgets.MediaWidgetModel.prototype */{
+
+ /**
+ * Id attribute.
+ *
+ * @type {string}
+ */
+ idAttribute: 'widget_id',
+
+ /**
+ * Instance schema.
+ *
+ * This adheres to JSON Schema and subclasses should have their schema
+ * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
+ *
+ * @type {Object.<string, Object>}
+ */
+ schema: {
+ title: {
+ type: 'string',
+ 'default': ''
+ },
+ attachment_id: {
+ type: 'integer',
+ 'default': 0
+ },
+ url: {
+ type: 'string',
+ 'default': ''
+ }
+ },
+
+ /**
+ * Get default attribute values.
+ *
+ * @return {Object} Mapping of property names to their default values.
+ */
+ defaults: function() {
+ var defaults = {};
+ _.each( this.schema, function( fieldSchema, field ) {
+ defaults[ field ] = fieldSchema['default'];
+ });
+ return defaults;
+ },
+
+ /**
+ * Set attribute value(s).
+ *
+ * This is a wrapped version of Backbone.Model#set() which allows us to
+ * cast the attribute values from the hidden inputs' string values into
+ * the appropriate data types (integers or booleans).
+ *
+ * @param {string|Object} key - Attribute name or attribute pairs.
+ * @param {mixed|Object} [val] - Attribute value or options object.
+ * @param {Object} [options] - Options when attribute name and value are passed separately.
+ * @return {wp.mediaWidgets.MediaWidgetModel} This model.
+ */
+ set: function set( key, val, options ) {
+ var model = this, attrs, opts, castedAttrs; // eslint-disable-line consistent-this
+ if ( null === key ) {
+ return model;
+ }
+ if ( 'object' === typeof key ) {
+ attrs = key;
+ opts = val;
+ } else {
+ attrs = {};
+ attrs[ key ] = val;
+ opts = options;
+ }
+
+ castedAttrs = {};
+ _.each( attrs, function( value, name ) {
+ var type;
+ if ( ! model.schema[ name ] ) {
+ castedAttrs[ name ] = value;
+ return;
+ }
+ type = model.schema[ name ].type;
+ if ( 'array' === type ) {
+ castedAttrs[ name ] = value;
+ if ( ! _.isArray( castedAttrs[ name ] ) ) {
+ castedAttrs[ name ] = castedAttrs[ name ].split( /,/ ); // Good enough for parsing an ID list.
+ }
+ if ( model.schema[ name ].items && 'integer' === model.schema[ name ].items.type ) {
+ castedAttrs[ name ] = _.filter(
+ _.map( castedAttrs[ name ], function( id ) {
+ return parseInt( id, 10 );
+ },
+ function( id ) {
+ return 'number' === typeof id;
+ }
+ ) );
+ }
+ } else if ( 'integer' === type ) {
+ castedAttrs[ name ] = parseInt( value, 10 );
+ } else if ( 'boolean' === type ) {
+ castedAttrs[ name ] = ! ( ! value || '0' === value || 'false' === value );
+ } else {
+ castedAttrs[ name ] = value;
+ }
+ });
+
+ return Backbone.Model.prototype.set.call( this, castedAttrs, opts );
+ },
+
+ /**
+ * Get props which are merged on top of the model when an embed is chosen (as opposed to an attachment).
+ *
+ * @return {Object} Reset/override props.
+ */
+ getEmbedResetProps: function getEmbedResetProps() {
+ return {
+ id: 0
+ };
+ }
+ });
+
+ /**
+ * Collection of all widget model instances.
+ *
+ * @memberOf wp.mediaWidgets
+ *
+ * @type {Backbone.Collection}
+ */
+ component.modelCollection = new ( Backbone.Collection.extend( {
+ model: component.MediaWidgetModel
+ }) )();
+
+ /**
+ * Mapping of widget ID to instances of MediaWidgetControl subclasses.
+ *
+ * @memberOf wp.mediaWidgets
+ *
+ * @type {Object.<string, wp.mediaWidgets.MediaWidgetControl>}
+ */
+ component.widgetControls = {};
+
+ /**
+ * Handle widget being added or initialized for the first time at the widget-added event.
+ *
+ * @memberOf wp.mediaWidgets
+ *
+ * @param {jQuery.Event} event - Event.
+ * @param {jQuery} widgetContainer - Widget container element.
+ *
+ * @return {void}
+ */
+ component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) {
+ var fieldContainer, syncContainer, widgetForm, idBase, ControlConstructor, ModelConstructor, modelAttributes, widgetControl, widgetModel, widgetId, animatedCheckDelay = 50, renderWhenAnimationDone;
+ widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen.
+ idBase = widgetForm.find( '> .id_base' ).val();
+ widgetId = widgetForm.find( '> .widget-id' ).val();
+
+ // Prevent initializing already-added widgets.
+ if ( component.widgetControls[ widgetId ] ) {
+ return;
+ }
+
+ ControlConstructor = component.controlConstructors[ idBase ];
+ if ( ! ControlConstructor ) {
+ return;
+ }
+
+ ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel;
+
+ /*
+ * Create a container element for the widget control (Backbone.View).
+ * This is inserted into the DOM immediately before the .widget-content
+ * element because the contents of this element are essentially "managed"
+ * by PHP, where each widget update cause the entire element to be emptied
+ * and replaced with the rendered output of WP_Widget::form() which is
+ * sent back in Ajax request made to save/update the widget instance.
+ * To prevent a "flash of replaced DOM elements and re-initialized JS
+ * components", the JS template is rendered outside of the normal form
+ * container.
+ */
+ fieldContainer = $( '<div></div>' );
+ syncContainer = widgetContainer.find( '.widget-content:first' );
+ syncContainer.before( fieldContainer );
+
+ /*
+ * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state.
+ * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model
+ * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>.
+ */
+ modelAttributes = {};
+ syncContainer.find( '.media-widget-instance-property' ).each( function() {
+ var input = $( this );
+ modelAttributes[ input.data( 'property' ) ] = input.val();
+ });
+ modelAttributes.widget_id = widgetId;
+
+ widgetModel = new ModelConstructor( modelAttributes );
+
+ widgetControl = new ControlConstructor({
+ el: fieldContainer,
+ syncContainer: syncContainer,
+ model: widgetModel
+ });
+
+ /*
+ * Render the widget once the widget parent's container finishes animating,
+ * as the widget-added event fires with a slideDown of the container.
+ * This ensures that the container's dimensions are fixed so that ME.js
+ * can initialize with the proper dimensions.
+ */
+ renderWhenAnimationDone = function() {
+ if ( ! widgetContainer.hasClass( 'open' ) ) {
+ setTimeout( renderWhenAnimationDone, animatedCheckDelay );
+ } else {
+ widgetControl.render();
+ }
+ };
+ renderWhenAnimationDone();
+
+ /*
+ * Note that the model and control currently won't ever get garbage-collected
+ * when a widget gets removed/deleted because there is no widget-removed event.
+ */
+ component.modelCollection.add( [ widgetModel ] );
+ component.widgetControls[ widgetModel.get( 'widget_id' ) ] = widgetControl;
+ };
+
+ /**
+ * Setup widget in accessibility mode.
+ *
+ * @memberOf wp.mediaWidgets
+ *
+ * @return {void}
+ */
+ component.setupAccessibleMode = function setupAccessibleMode() {
+ var widgetForm, widgetId, idBase, widgetControl, ControlConstructor, ModelConstructor, modelAttributes, fieldContainer, syncContainer;
+ widgetForm = $( '.editwidget > form' );
+ if ( 0 === widgetForm.length ) {
+ return;
+ }
+
+ idBase = widgetForm.find( '.id_base' ).val();
+
+ ControlConstructor = component.controlConstructors[ idBase ];
+ if ( ! ControlConstructor ) {
+ return;
+ }
+
+ widgetId = widgetForm.find( '> .widget-control-actions > .widget-id' ).val();
+
+ ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel;
+ fieldContainer = $( '<div></div>' );
+ syncContainer = widgetForm.find( '> .widget-inside' );
+ syncContainer.before( fieldContainer );
+
+ modelAttributes = {};
+ syncContainer.find( '.media-widget-instance-property' ).each( function() {
+ var input = $( this );
+ modelAttributes[ input.data( 'property' ) ] = input.val();
+ });
+ modelAttributes.widget_id = widgetId;
+
+ widgetControl = new ControlConstructor({
+ el: fieldContainer,
+ syncContainer: syncContainer,
+ model: new ModelConstructor( modelAttributes )
+ });
+
+ component.modelCollection.add( [ widgetControl.model ] );
+ component.widgetControls[ widgetControl.model.get( 'widget_id' ) ] = widgetControl;
+
+ widgetControl.render();
+ };
+
+ /**
+ * Sync widget instance data sanitized from server back onto widget model.
+ *
+ * This gets called via the 'widget-updated' event when saving a widget from
+ * the widgets admin screen and also via the 'widget-synced' event when making
+ * a change to a widget in the customizer.
+ *
+ * @memberOf wp.mediaWidgets
+ *
+ * @param {jQuery.Event} event - Event.
+ * @param {jQuery} widgetContainer - Widget container element.
+ *
+ * @return {void}
+ */
+ component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) {
+ var widgetForm, widgetContent, widgetId, widgetControl, attributes = {};
+ widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
+ widgetId = widgetForm.find( '> .widget-id' ).val();
+
+ widgetControl = component.widgetControls[ widgetId ];
+ if ( ! widgetControl ) {
+ return;
+ }
+
+ // Make sure the server-sanitized values get synced back into the model.
+ widgetContent = widgetForm.find( '> .widget-content' );
+ widgetContent.find( '.media-widget-instance-property' ).each( function() {
+ var property = $( this ).data( 'property' );
+ attributes[ property ] = $( this ).val();
+ });
+
+ // Suspend syncing model back to inputs when syncing from inputs to model, preventing infinite loop.
+ widgetControl.stopListening( widgetControl.model, 'change', widgetControl.syncModelToInputs );
+ widgetControl.model.set( attributes );
+ widgetControl.listenTo( widgetControl.model, 'change', widgetControl.syncModelToInputs );
+ };
+
+ /**
+ * Initialize functionality.
+ *
+ * This function exists to prevent the JS file from having to boot itself.
+ * When WordPress enqueues this script, it should have an inline script
+ * attached which calls wp.mediaWidgets.init().
+ *
+ * @memberOf wp.mediaWidgets
+ *
+ * @return {void}
+ */
+ component.init = function init() {
+ var $document = $( document );
+ $document.on( 'widget-added', component.handleWidgetAdded );
+ $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated );
+
+ /*
+ * Manually trigger widget-added events for media widgets on the admin
+ * screen once they are expanded. The widget-added event is not triggered
+ * for each pre-existing widget on the widgets admin screen like it is
+ * on the customizer. Likewise, the customizer only triggers widget-added
+ * when the widget is expanded to just-in-time construct the widget form
+ * when it is actually going to be displayed. So the following implements
+ * the same for the widgets admin screen, to invoke the widget-added
+ * handler when a pre-existing media widget is expanded.
+ */
+ $( function initializeExistingWidgetContainers() {
+ var widgetContainers;
+ if ( 'widgets' !== window.pagenow ) {
+ return;
+ }
+ widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' );
+ widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() {
+ var widgetContainer = $( this );
+ component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer );
+ });
+
+ // Accessibility mode.
+ if ( document.readyState === 'complete' ) {
+ // Page is fully loaded.
+ component.setupAccessibleMode();
+ } else {
+ // Page is still loading.
+ $( window ).on( 'load', function() {
+ component.setupAccessibleMode();
+ });
+ }
+ });
+ };
+
+ return component;
+})( jQuery );
diff --git a/wp-admin/js/widgets/media-widgets.min.js b/wp-admin/js/widgets/media-widgets.min.js
new file mode 100644
index 0000000..1e3b45d
--- /dev/null
+++ b/wp-admin/js/widgets/media-widgets.min.js
@@ -0,0 +1,2 @@
+/*! This file is auto-generated */
+wp.mediaWidgets=function(c){"use strict";var m={controlConstructors:{},modelConstructors:{}};return m.PersistentDisplaySettingsLibrary=wp.media.controller.Library.extend({initialize:function(e){_.bindAll(this,"handleDisplaySettingChange"),wp.media.controller.Library.prototype.initialize.call(this,e)},handleDisplaySettingChange:function(e){this.get("selectedDisplaySettings").set(e.attributes)},display:function(e){var t=this.get("selectedDisplaySettings"),e=wp.media.controller.Library.prototype.display.call(this,e);return e.off("change",this.handleDisplaySettingChange),e.set(t.attributes),"custom"===t.get("link_type")&&(e.linkUrl=t.get("link_url")),e.on("change",this.handleDisplaySettingChange),e}}),m.MediaEmbedView=wp.media.view.Embed.extend({initialize:function(e){var t=this;wp.media.view.Embed.prototype.initialize.call(t,e),"image"!==t.controller.options.mimeType&&(e=t.controller.states.get("embed")).off("scan",e.scanImage,e)},refresh:function(){var e="image"===this.controller.options.mimeType?wp.media.view.EmbedImage:wp.media.view.EmbedLink.extend({setAddToWidgetButtonDisabled:function(e){this.views.parent.views.parent.views.get(".media-frame-toolbar")[0].$el.find(".media-button-select").prop("disabled",e)},setErrorNotice:function(e){var t=this.views.parent.$el.find("> .notice:first-child");e?(t.length||((t=c('<div class="media-widget-embed-notice notice notice-error notice-alt"></div>')).hide(),this.views.parent.$el.prepend(t)),t.empty(),t.append(c("<p>",{html:e})),t.slideDown("fast")):t.length&&t.slideUp("fast")},updateoEmbed:function(){var e=this,t=e.model.get("url");t?(t.match(/^(http|https):\/\/.+\//)||(e.controller.$el.find("#embed-url-field").addClass("invalid"),e.setAddToWidgetButtonDisabled(!0)),wp.media.view.EmbedLink.prototype.updateoEmbed.call(e)):(e.setErrorNotice(""),e.setAddToWidgetButtonDisabled(!0))},fetch:function(){var t,e,i=this,n=i.model.get("url");i.dfd&&"pending"===i.dfd.state()&&i.dfd.abort(),t=function(e){i.renderoEmbed({data:{body:e}}),i.controller.$el.find("#embed-url-field").removeClass("invalid"),i.setErrorNotice(""),i.setAddToWidgetButtonDisabled(!1)},(e=document.createElement("a")).href=n,(e=e.pathname.toLowerCase().match(/\.(\w+)$/))?(e=e[1],!wp.media.view.settings.embedMimes[e]||0!==wp.media.view.settings.embedMimes[e].indexOf(i.controller.options.mimeType)?i.renderFail():t("\x3c!--success--\x3e")):((e=/https?:\/\/www\.youtube\.com\/embed\/([^/]+)/.exec(n))&&(n="https://www.youtube.com/watch?v="+e[1],i.model.attributes.url=n),i.dfd=wp.apiRequest({url:wp.media.view.settings.oEmbedProxyUrl,data:{url:n,maxwidth:i.model.get("width"),maxheight:i.model.get("height"),discover:!1},type:"GET",dataType:"json",context:i}),i.dfd.done(function(e){i.controller.options.mimeType!==e.type?i.renderFail():t(e.html)}),i.dfd.fail(_.bind(i.renderFail,i)))},renderFail:function(){var e=this;e.controller.$el.find("#embed-url-field").addClass("invalid"),e.setErrorNotice(e.controller.options.invalidEmbedTypeError||"ERROR"),e.setAddToWidgetButtonDisabled(!0)}});this.settings(new e({controller:this.controller,model:this.model.props,priority:40}))}}),m.MediaFrameSelect=wp.media.view.MediaFrame.Post.extend({createStates:function(){var t=this.options.mimeType,i=[];_.each(wp.media.view.settings.embedMimes,function(e){0===e.indexOf(t)&&i.push(e)}),0<i.length&&(t=i),this.states.add([new m.PersistentDisplaySettingsLibrary({id:"insert",title:this.options.title,selection:this.options.selection,priority:20,toolbar:"main-insert",filterable:"dates",library:wp.media.query({type:t}),multiple:!1,editable:!0,selectedDisplaySettings:this.options.selectedDisplaySettings,displaySettings:!!_.isUndefined(this.options.showDisplaySettings)||this.options.showDisplaySettings,displayUserSettings:!1}),new wp.media.controller.EditImage({model:this.options.editImage}),new wp.media.controller.Embed({metadata:this.options.metadata,type:"image"===this.options.mimeType?"image":"link",invalidEmbedTypeError:this.options.invalidEmbedTypeError})])},mainInsertToolbar:function(e){var i=this;e.set("insert",{style:"primary",priority:80,text:i.options.text,requires:{selection:!0},click:function(){var e=i.state(),t=e.get("selection");i.close(),e.trigger("insert",t).reset()}})},mainEmbedToolbar:function(e){e.view=new wp.media.view.Toolbar.Embed({controller:this,text:this.options.text,event:"insert"})},embedContent:function(){var e=new m.MediaEmbedView({controller:this,model:this.state()}).render();this.content.set(e)}}),m.MediaWidgetControl=Backbone.View.extend({l10n:{add_to_widget:"{{add_to_widget}}",add_media:"{{add_media}}"},id_base:"",mime_type:"",events:{"click .notice-missing-attachment a":"handleMediaLibraryLinkClick","click .select-media":"selectMedia","click .placeholder":"selectMedia","click .edit-media":"editMedia"},showDisplaySettings:!0,initialize:function(e){var i=this;if(Backbone.View.prototype.initialize.call(i,e),!(i.model instanceof m.MediaWidgetModel))throw new Error("Missing options.model");if(!e.el)throw new Error("Missing options.el");if(!e.syncContainer)throw new Error("Missing options.syncContainer");if(i.syncContainer=e.syncContainer,i.$el.addClass("media-widget-control"),_.bindAll(i,"syncModelToInputs","render","updateSelectedAttachment","renderPreview"),!i.id_base&&(_.find(m.controlConstructors,function(e,t){return i instanceof e&&(i.id_base=t,!0)}),!i.id_base))throw new Error("Missing id_base.");i.previewTemplateProps=new Backbone.Model(i.mapModelToPreviewTemplateProps()),i.selectedAttachment=new wp.media.model.Attachment,i.renderPreview=_.debounce(i.renderPreview),i.listenTo(i.previewTemplateProps,"change",i.renderPreview),i.model.on("change:attachment_id",i.updateSelectedAttachment),i.model.on("change:url",i.updateSelectedAttachment),i.updateSelectedAttachment(),i.listenTo(i.model,"change",i.syncModelToInputs),i.listenTo(i.model,"change",i.syncModelToPreviewProps),i.listenTo(i.model,"change",i.render),i.$el.on("input change",".title",function(){i.model.set({title:c(this).val().trim()})}),i.$el.on("input change",".link",function(){var e=c(this).val().trim(),t="custom";i.selectedAttachment.get("linkUrl")===e||i.selectedAttachment.get("link")===e?t="post":i.selectedAttachment.get("url")===e&&(t="file"),i.model.set({link_url:e,link_type:t}),i.displaySettings.set({link:t,linkUrl:e})}),i.displaySettings=new Backbone.Model(_.pick(i.mapModelToMediaFrameProps(_.extend(i.model.defaults(),i.model.toJSON())),_.keys(wp.media.view.settings.defaultProps)))},updateSelectedAttachment:function(){var e,t=this;0===t.model.get("attachment_id")?(t.selectedAttachment.clear(),t.model.set("error",!1)):t.model.get("attachment_id")!==t.selectedAttachment.get("id")&&(e=new wp.media.model.Attachment({id:t.model.get("attachment_id")})).fetch().done(function(){t.model.set("error",!1),t.selectedAttachment.set(e.toJSON())}).fail(function(){t.model.set("error","missing_attachment")})},syncModelToPreviewProps:function(){this.previewTemplateProps.set(this.mapModelToPreviewTemplateProps())},syncModelToInputs:function(){var n=this;n.syncContainer.find(".media-widget-instance-property").each(function(){var e=c(this),t=e.data("property"),i=n.model.get(t);_.isUndefined(i)||(i="array"===n.model.schema[t].type&&_.isArray(i)?i.join(","):"boolean"===n.model.schema[t].type?i?"1":"":String(i),e.val()!==i&&(e.val(i),e.trigger("change")))})},template:function(){if(c("#tmpl-widget-media-"+this.id_base+"-control").length)return wp.template("widget-media-"+this.id_base+"-control");throw new Error("Missing widget control template for "+this.id_base)},render:function(){var e,t=this;t.templateRendered||(t.$el.html(t.template()(t.model.toJSON())),t.renderPreview(),t.templateRendered=!0),(e=t.$el.find(".title")).is(document.activeElement)||e.val(t.model.get("title")),t.$el.toggleClass("selected",t.isSelected())},renderPreview:function(){throw new Error("renderPreview must be implemented")},isSelected:function(){return!this.model.get("error")&&Boolean(this.model.get("attachment_id")||this.model.get("url"))},handleMediaLibraryLinkClick:function(e){e.preventDefault(),this.selectMedia()},selectMedia:function(){var i,t,e,n=this,d=[];n.isSelected()&&0!==n.model.get("attachment_id")&&d.push(n.selectedAttachment),d=new wp.media.model.Selection(d,{multiple:!1}),(e=n.mapModelToMediaFrameProps(n.model.toJSON())).size&&n.displaySettings.set("size",e.size),i=new m.MediaFrameSelect({title:n.l10n.add_media,frame:"post",text:n.l10n.add_to_widget,selection:d,mimeType:n.mime_type,selectedDisplaySettings:n.displaySettings,showDisplaySettings:n.showDisplaySettings,metadata:e,state:n.isSelected()&&0===n.model.get("attachment_id")?"embed":"insert",invalidEmbedTypeError:n.l10n.unsupported_file_type}),(wp.media.frame=i).on("insert",function(){var e={},t=i.state();"embed"===t.get("id")?_.extend(e,{id:0},t.props.toJSON()):_.extend(e,t.get("selection").first().toJSON()),n.selectedAttachment.set(e),n.model.set("error",!1),n.model.set(n.getModelPropsFromMediaFrame(i))}),t=wp.media.model.Attachment.prototype.sync,wp.media.model.Attachment.prototype.sync=function(e){return"delete"===e?t.apply(this,arguments):c.Deferred().rejectWith(this).promise()},i.on("close",function(){wp.media.model.Attachment.prototype.sync=t}),i.$el.addClass("media-widget"),i.open(),d&&d.on("destroy",function(e){n.model.get("attachment_id")===e.get("id")&&n.model.set({attachment_id:0,url:""})}),i.$el.find(".media-frame-menu .media-menu-item.active").focus()},getModelPropsFromMediaFrame:function(e){var t,i,n=this,d=e.state();if("insert"===d.get("id"))(t=d.get("selection").first().toJSON()).postUrl=t.link,n.showDisplaySettings&&_.extend(t,e.content.get(".attachments-browser").sidebar.get("display").model.toJSON()),t.sizes&&t.size&&t.sizes[t.size]&&(t.url=t.sizes[t.size].url);else{if("embed"!==d.get("id"))throw new Error("Unexpected state: "+d.get("id"));t=_.extend(d.props.toJSON(),{attachment_id:0},n.model.getEmbedResetProps())}return t.id&&(t.attachment_id=t.id),i=n.mapMediaToModelProps(t),_.each(wp.media.view.settings.embedExts,function(e){e in n.model.schema&&i.url!==i[e]&&(i[e]="")}),i},mapMediaToModelProps:function(e){var t,i=this,n={},d={};return _.each(i.model.schema,function(e,t){"title"!==t&&(n[e.media_prop||t]=t)}),_.each(e,function(e,t){t=n[t]||t;i.model.schema[t]&&(d[t]=e)}),"custom"===e.size&&(d.width=e.customWidth,d.height=e.customHeight),"post"===e.link?d.link_url=e.postUrl||e.linkUrl:"file"===e.link&&(d.link_url=e.url),!e.attachment_id&&e.id&&(d.attachment_id=e.id),e.url&&(t=e.url.replace(/#.*$/,"").replace(/\?.*$/,"").split(".").pop().toLowerCase())in i.model.schema&&(d[t]=e.url),_.omit(d,"title")},mapModelToMediaFrameProps:function(e){var n=this,d={};return _.each(e,function(e,t){var i=n.model.schema[t]||{};d[i.media_prop||t]=e}),d.attachment_id=d.id,"custom"===d.size&&(d.customWidth=n.model.get("width"),d.customHeight=n.model.get("height")),d},mapModelToPreviewTemplateProps:function(){var i=this,n={};return _.each(i.model.schema,function(e,t){e.hasOwnProperty("should_preview_update")&&!e.should_preview_update||(n[t]=i.model.get(t))}),n.error=i.model.get("error"),n},editMedia:function(){throw new Error("editMedia not implemented")}}),m.MediaWidgetModel=Backbone.Model.extend({idAttribute:"widget_id",schema:{title:{type:"string",default:""},attachment_id:{type:"integer",default:0},url:{type:"string",default:""}},defaults:function(){var i={};return _.each(this.schema,function(e,t){i[t]=e.default}),i},set:function(e,t,i){var n,d,o=this;return null===e?o:(e="object"==typeof e?(n=e,t):((n={})[e]=t,i),d={},_.each(n,function(e,t){var i;o.schema[t]?"array"===(i=o.schema[t].type)?(d[t]=e,_.isArray(d[t])||(d[t]=d[t].split(/,/)),o.schema[t].items&&"integer"===o.schema[t].items.type&&(d[t]=_.filter(_.map(d[t],function(e){return parseInt(e,10)},function(e){return"number"==typeof e})))):d[t]="integer"===i?parseInt(e,10):"boolean"===i?!(!e||"0"===e||"false"===e):e:d[t]=e}),Backbone.Model.prototype.set.call(this,d,e))},getEmbedResetProps:function(){return{id:0}}}),m.modelCollection=new(Backbone.Collection.extend({model:m.MediaWidgetModel})),m.widgetControls={},m.handleWidgetAdded=function(e,t){var i,n,d,o,a,s,r=t.find("> .widget-inside > .form, > .widget-inside > form"),l=r.find("> .id_base").val(),r=r.find("> .widget-id").val();m.widgetControls[r]||(d=m.controlConstructors[l])&&(l=m.modelConstructors[l]||m.MediaWidgetModel,i=c("<div></div>"),(n=t.find(".widget-content:first")).before(i),o={},n.find(".media-widget-instance-property").each(function(){var e=c(this);o[e.data("property")]=e.val()}),o.widget_id=r,r=new l(o),a=new d({el:i,syncContainer:n,model:r}),(s=function(){t.hasClass("open")?a.render():setTimeout(s,50)})(),m.modelCollection.add([r]),m.widgetControls[r.get("widget_id")]=a)},m.setupAccessibleMode=function(){var e,t,i,n,d,o=c(".editwidget > form");0!==o.length&&(i=o.find(".id_base").val(),t=m.controlConstructors[i])&&(e=o.find("> .widget-control-actions > .widget-id").val(),i=m.modelConstructors[i]||m.MediaWidgetModel,d=c("<div></div>"),(o=o.find("> .widget-inside")).before(d),n={},o.find(".media-widget-instance-property").each(function(){var e=c(this);n[e.data("property")]=e.val()}),n.widget_id=e,e=new t({el:d,syncContainer:o,model:new i(n)}),m.modelCollection.add([e.model]),(m.widgetControls[e.model.get("widget_id")]=e).render())},m.handleWidgetUpdated=function(e,t){var i={},t=t.find("> .widget-inside > .form, > .widget-inside > form"),n=t.find("> .widget-id").val(),n=m.widgetControls[n];n&&(t.find("> .widget-content").find(".media-widget-instance-property").each(function(){var e=c(this).data("property");i[e]=c(this).val()}),n.stopListening(n.model,"change",n.syncModelToInputs),n.model.set(i),n.listenTo(n.model,"change",n.syncModelToInputs))},m.init=function(){var e=c(document);e.on("widget-added",m.handleWidgetAdded),e.on("widget-synced widget-updated",m.handleWidgetUpdated),c(function(){"widgets"===window.pagenow&&(c(".widgets-holder-wrap:not(#available-widgets)").find("div.widget").one("click.toggle-widget-expanded",function(){var e=c(this);m.handleWidgetAdded(new jQuery.Event("widget-added"),e)}),"complete"===document.readyState?m.setupAccessibleMode():c(window).on("load",function(){m.setupAccessibleMode()}))})},m}(jQuery); \ No newline at end of file
diff --git a/wp-admin/js/widgets/text-widgets.js b/wp-admin/js/widgets/text-widgets.js
new file mode 100644
index 0000000..48d7247
--- /dev/null
+++ b/wp-admin/js/widgets/text-widgets.js
@@ -0,0 +1,550 @@
+/**
+ * @output wp-admin/js/widgets/text-widgets.js
+ */
+
+/* global tinymce, switchEditors */
+/* eslint consistent-this: [ "error", "control" ] */
+
+/**
+ * @namespace wp.textWidgets
+ */
+wp.textWidgets = ( function( $ ) {
+ 'use strict';
+
+ var component = {
+ dismissedPointers: [],
+ idBases: [ 'text' ]
+ };
+
+ component.TextWidgetControl = Backbone.View.extend(/** @lends wp.textWidgets.TextWidgetControl.prototype */{
+
+ /**
+ * View events.
+ *
+ * @type {Object}
+ */
+ events: {},
+
+ /**
+ * Text widget control.
+ *
+ * @constructs wp.textWidgets.TextWidgetControl
+ * @augments Backbone.View
+ * @abstract
+ *
+ * @param {Object} options - Options.
+ * @param {jQuery} options.el - Control field container element.
+ * @param {jQuery} options.syncContainer - Container element where fields are synced for the server.
+ *
+ * @return {void}
+ */
+ initialize: function initialize( options ) {
+ var control = this;
+
+ if ( ! options.el ) {
+ throw new Error( 'Missing options.el' );
+ }
+ if ( ! options.syncContainer ) {
+ throw new Error( 'Missing options.syncContainer' );
+ }
+
+ Backbone.View.prototype.initialize.call( control, options );
+ control.syncContainer = options.syncContainer;
+
+ control.$el.addClass( 'text-widget-fields' );
+ control.$el.html( wp.template( 'widget-text-control-fields' ) );
+
+ control.customHtmlWidgetPointer = control.$el.find( '.wp-pointer.custom-html-widget-pointer' );
+ if ( control.customHtmlWidgetPointer.length ) {
+ control.customHtmlWidgetPointer.find( '.close' ).on( 'click', function( event ) {
+ event.preventDefault();
+ control.customHtmlWidgetPointer.hide();
+ $( '#' + control.fields.text.attr( 'id' ) + '-html' ).trigger( 'focus' );
+ control.dismissPointers( [ 'text_widget_custom_html' ] );
+ });
+ control.customHtmlWidgetPointer.find( '.add-widget' ).on( 'click', function( event ) {
+ event.preventDefault();
+ control.customHtmlWidgetPointer.hide();
+ control.openAvailableWidgetsPanel();
+ });
+ }
+
+ control.pasteHtmlPointer = control.$el.find( '.wp-pointer.paste-html-pointer' );
+ if ( control.pasteHtmlPointer.length ) {
+ control.pasteHtmlPointer.find( '.close' ).on( 'click', function( event ) {
+ event.preventDefault();
+ control.pasteHtmlPointer.hide();
+ control.editor.focus();
+ control.dismissPointers( [ 'text_widget_custom_html', 'text_widget_paste_html' ] );
+ });
+ }
+
+ control.fields = {
+ title: control.$el.find( '.title' ),
+ text: control.$el.find( '.text' )
+ };
+
+ // Sync input fields to hidden sync fields which actually get sent to the server.
+ _.each( control.fields, function( fieldInput, fieldName ) {
+ fieldInput.on( 'input change', function updateSyncField() {
+ var syncInput = control.syncContainer.find( '.sync-input.' + fieldName );
+ if ( syncInput.val() !== fieldInput.val() ) {
+ syncInput.val( fieldInput.val() );
+ syncInput.trigger( 'change' );
+ }
+ });
+
+ // Note that syncInput cannot be re-used because it will be destroyed with each widget-updated event.
+ fieldInput.val( control.syncContainer.find( '.sync-input.' + fieldName ).val() );
+ });
+ },
+
+ /**
+ * Dismiss pointers for Custom HTML widget.
+ *
+ * @since 4.8.1
+ *
+ * @param {Array} pointers Pointer IDs to dismiss.
+ * @return {void}
+ */
+ dismissPointers: function dismissPointers( pointers ) {
+ _.each( pointers, function( pointer ) {
+ wp.ajax.post( 'dismiss-wp-pointer', {
+ pointer: pointer
+ });
+ component.dismissedPointers.push( pointer );
+ });
+ },
+
+ /**
+ * Open available widgets panel.
+ *
+ * @since 4.8.1
+ * @return {void}
+ */
+ openAvailableWidgetsPanel: function openAvailableWidgetsPanel() {
+ var sidebarControl;
+ wp.customize.section.each( function( section ) {
+ if ( section.extended( wp.customize.Widgets.SidebarSection ) && section.expanded() ) {
+ sidebarControl = wp.customize.control( 'sidebars_widgets[' + section.params.sidebarId + ']' );
+ }
+ });
+ if ( ! sidebarControl ) {
+ return;
+ }
+ setTimeout( function() { // Timeout to prevent click event from causing panel to immediately collapse.
+ wp.customize.Widgets.availableWidgetsPanel.open( sidebarControl );
+ wp.customize.Widgets.availableWidgetsPanel.$search.val( 'HTML' ).trigger( 'keyup' );
+ });
+ },
+
+ /**
+ * Update input fields from the sync fields.
+ *
+ * This function is called at the widget-updated and widget-synced events.
+ * A field will only be updated if it is not currently focused, to avoid
+ * overwriting content that the user is entering.
+ *
+ * @return {void}
+ */
+ updateFields: function updateFields() {
+ var control = this, syncInput;
+
+ if ( ! control.fields.title.is( document.activeElement ) ) {
+ syncInput = control.syncContainer.find( '.sync-input.title' );
+ control.fields.title.val( syncInput.val() );
+ }
+
+ syncInput = control.syncContainer.find( '.sync-input.text' );
+ if ( control.fields.text.is( ':visible' ) ) {
+ if ( ! control.fields.text.is( document.activeElement ) ) {
+ control.fields.text.val( syncInput.val() );
+ }
+ } else if ( control.editor && ! control.editorFocused && syncInput.val() !== control.fields.text.val() ) {
+ control.editor.setContent( wp.oldEditor.autop( syncInput.val() ) );
+ }
+ },
+
+ /**
+ * Initialize editor.
+ *
+ * @return {void}
+ */
+ initializeEditor: function initializeEditor() {
+ var control = this, changeDebounceDelay = 1000, id, textarea, triggerChangeIfDirty, restoreTextMode = false, needsTextareaChangeTrigger = false, previousValue;
+ textarea = control.fields.text;
+ id = textarea.attr( 'id' );
+ previousValue = textarea.val();
+
+ /**
+ * Trigger change if dirty.
+ *
+ * @return {void}
+ */
+ triggerChangeIfDirty = function() {
+ var updateWidgetBuffer = 300; // See wp.customize.Widgets.WidgetControl._setupUpdateUI() which uses 250ms for updateWidgetDebounced.
+ if ( control.editor.isDirty() ) {
+
+ /*
+ * Account for race condition in customizer where user clicks Save & Publish while
+ * focus was just previously given to the editor. Since updates to the editor
+ * are debounced at 1 second and since widget input changes are only synced to
+ * settings after 250ms, the customizer needs to be put into the processing
+ * state during the time between the change event is triggered and updateWidget
+ * logic starts. Note that the debounced update-widget request should be able
+ * to be removed with the removal of the update-widget request entirely once
+ * widgets are able to mutate their own instance props directly in JS without
+ * having to make server round-trips to call the respective WP_Widget::update()
+ * callbacks. See <https://core.trac.wordpress.org/ticket/33507>.
+ */
+ if ( wp.customize && wp.customize.state ) {
+ wp.customize.state( 'processing' ).set( wp.customize.state( 'processing' ).get() + 1 );
+ _.delay( function() {
+ wp.customize.state( 'processing' ).set( wp.customize.state( 'processing' ).get() - 1 );
+ }, updateWidgetBuffer );
+ }
+
+ if ( ! control.editor.isHidden() ) {
+ control.editor.save();
+ }
+ }
+
+ // Trigger change on textarea when it has changed so the widget can enter a dirty state.
+ if ( needsTextareaChangeTrigger && previousValue !== textarea.val() ) {
+ textarea.trigger( 'change' );
+ needsTextareaChangeTrigger = false;
+ previousValue = textarea.val();
+ }
+ };
+
+ // Just-in-time force-update the hidden input fields.
+ control.syncContainer.closest( '.widget' ).find( '[name=savewidget]:first' ).on( 'click', function onClickSaveButton() {
+ triggerChangeIfDirty();
+ });
+
+ /**
+ * Build (or re-build) the visual editor.
+ *
+ * @return {void}
+ */
+ function buildEditor() {
+ var editor, onInit, showPointerElement;
+
+ // Abort building if the textarea is gone, likely due to the widget having been deleted entirely.
+ if ( ! document.getElementById( id ) ) {
+ return;
+ }
+
+ // The user has disabled TinyMCE.
+ if ( typeof window.tinymce === 'undefined' ) {
+ wp.oldEditor.initialize( id, {
+ quicktags: true,
+ mediaButtons: true
+ });
+
+ return;
+ }
+
+ // Destroy any existing editor so that it can be re-initialized after a widget-updated event.
+ if ( tinymce.get( id ) ) {
+ restoreTextMode = tinymce.get( id ).isHidden();
+ wp.oldEditor.remove( id );
+ }
+
+ // Add or enable the `wpview` plugin.
+ $( document ).one( 'wp-before-tinymce-init.text-widget-init', function( event, init ) {
+ // If somebody has removed all plugins, they must have a good reason.
+ // Keep it that way.
+ if ( ! init.plugins ) {
+ return;
+ } else if ( ! /\bwpview\b/.test( init.plugins ) ) {
+ init.plugins += ',wpview';
+ }
+ } );
+
+ wp.oldEditor.initialize( id, {
+ tinymce: {
+ wpautop: true
+ },
+ quicktags: true,
+ mediaButtons: true
+ });
+
+ /**
+ * Show a pointer, focus on dismiss, and speak the contents for a11y.
+ *
+ * @param {jQuery} pointerElement Pointer element.
+ * @return {void}
+ */
+ showPointerElement = function( pointerElement ) {
+ pointerElement.show();
+ pointerElement.find( '.close' ).trigger( 'focus' );
+ wp.a11y.speak( pointerElement.find( 'h3, p' ).map( function() {
+ return $( this ).text();
+ } ).get().join( '\n\n' ) );
+ };
+
+ editor = window.tinymce.get( id );
+ if ( ! editor ) {
+ throw new Error( 'Failed to initialize editor' );
+ }
+ onInit = function() {
+
+ // When a widget is moved in the DOM the dynamically-created TinyMCE iframe will be destroyed and has to be re-built.
+ $( editor.getWin() ).on( 'pagehide', function() {
+ _.defer( buildEditor );
+ });
+
+ // If a prior mce instance was replaced, and it was in text mode, toggle to text mode.
+ if ( restoreTextMode ) {
+ switchEditors.go( id, 'html' );
+ }
+
+ // Show the pointer.
+ $( '#' + id + '-html' ).on( 'click', function() {
+ control.pasteHtmlPointer.hide(); // Hide the HTML pasting pointer.
+
+ if ( -1 !== component.dismissedPointers.indexOf( 'text_widget_custom_html' ) ) {
+ return;
+ }
+ showPointerElement( control.customHtmlWidgetPointer );
+ });
+
+ // Hide the pointer when switching tabs.
+ $( '#' + id + '-tmce' ).on( 'click', function() {
+ control.customHtmlWidgetPointer.hide();
+ });
+
+ // Show pointer when pasting HTML.
+ editor.on( 'pastepreprocess', function( event ) {
+ var content = event.content;
+ if ( -1 !== component.dismissedPointers.indexOf( 'text_widget_paste_html' ) || ! content || ! /&lt;\w+.*?&gt;/.test( content ) ) {
+ return;
+ }
+
+ // Show the pointer after a slight delay so the user sees what they pasted.
+ _.delay( function() {
+ showPointerElement( control.pasteHtmlPointer );
+ }, 250 );
+ });
+ };
+
+ if ( editor.initialized ) {
+ onInit();
+ } else {
+ editor.on( 'init', onInit );
+ }
+
+ control.editorFocused = false;
+
+ editor.on( 'focus', function onEditorFocus() {
+ control.editorFocused = true;
+ });
+ editor.on( 'paste', function onEditorPaste() {
+ editor.setDirty( true ); // Because pasting doesn't currently set the dirty state.
+ triggerChangeIfDirty();
+ });
+ editor.on( 'NodeChange', function onNodeChange() {
+ needsTextareaChangeTrigger = true;
+ });
+ editor.on( 'NodeChange', _.debounce( triggerChangeIfDirty, changeDebounceDelay ) );
+ editor.on( 'blur hide', function onEditorBlur() {
+ control.editorFocused = false;
+ triggerChangeIfDirty();
+ });
+
+ control.editor = editor;
+ }
+
+ buildEditor();
+ }
+ });
+
+ /**
+ * Mapping of widget ID to instances of TextWidgetControl subclasses.
+ *
+ * @memberOf wp.textWidgets
+ *
+ * @type {Object.<string, wp.textWidgets.TextWidgetControl>}
+ */
+ component.widgetControls = {};
+
+ /**
+ * Handle widget being added or initialized for the first time at the widget-added event.
+ *
+ * @memberOf wp.textWidgets
+ *
+ * @param {jQuery.Event} event - Event.
+ * @param {jQuery} widgetContainer - Widget container element.
+ *
+ * @return {void}
+ */
+ component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) {
+ var widgetForm, idBase, widgetControl, widgetId, animatedCheckDelay = 50, renderWhenAnimationDone, fieldContainer, syncContainer;
+ widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen.
+
+ idBase = widgetForm.find( '> .id_base' ).val();
+ if ( -1 === component.idBases.indexOf( idBase ) ) {
+ return;
+ }
+
+ // Prevent initializing already-added widgets.
+ widgetId = widgetForm.find( '.widget-id' ).val();
+ if ( component.widgetControls[ widgetId ] ) {
+ return;
+ }
+
+ // Bypass using TinyMCE when widget is in legacy mode.
+ if ( ! widgetForm.find( '.visual' ).val() ) {
+ return;
+ }
+
+ /*
+ * Create a container element for the widget control fields.
+ * This is inserted into the DOM immediately before the .widget-content
+ * element because the contents of this element are essentially "managed"
+ * by PHP, where each widget update cause the entire element to be emptied
+ * and replaced with the rendered output of WP_Widget::form() which is
+ * sent back in Ajax request made to save/update the widget instance.
+ * To prevent a "flash of replaced DOM elements and re-initialized JS
+ * components", the JS template is rendered outside of the normal form
+ * container.
+ */
+ fieldContainer = $( '<div></div>' );
+ syncContainer = widgetContainer.find( '.widget-content:first' );
+ syncContainer.before( fieldContainer );
+
+ widgetControl = new component.TextWidgetControl({
+ el: fieldContainer,
+ syncContainer: syncContainer
+ });
+
+ component.widgetControls[ widgetId ] = widgetControl;
+
+ /*
+ * Render the widget once the widget parent's container finishes animating,
+ * as the widget-added event fires with a slideDown of the container.
+ * This ensures that the textarea is visible and an iframe can be embedded
+ * with TinyMCE being able to set contenteditable on it.
+ */
+ renderWhenAnimationDone = function() {
+ if ( ! widgetContainer.hasClass( 'open' ) ) {
+ setTimeout( renderWhenAnimationDone, animatedCheckDelay );
+ } else {
+ widgetControl.initializeEditor();
+ }
+ };
+ renderWhenAnimationDone();
+ };
+
+ /**
+ * Setup widget in accessibility mode.
+ *
+ * @memberOf wp.textWidgets
+ *
+ * @return {void}
+ */
+ component.setupAccessibleMode = function setupAccessibleMode() {
+ var widgetForm, idBase, widgetControl, fieldContainer, syncContainer;
+ widgetForm = $( '.editwidget > form' );
+ if ( 0 === widgetForm.length ) {
+ return;
+ }
+
+ idBase = widgetForm.find( '.id_base' ).val();
+ if ( -1 === component.idBases.indexOf( idBase ) ) {
+ return;
+ }
+
+ // Bypass using TinyMCE when widget is in legacy mode.
+ if ( ! widgetForm.find( '.visual' ).val() ) {
+ return;
+ }
+
+ fieldContainer = $( '<div></div>' );
+ syncContainer = widgetForm.find( '> .widget-inside' );
+ syncContainer.before( fieldContainer );
+
+ widgetControl = new component.TextWidgetControl({
+ el: fieldContainer,
+ syncContainer: syncContainer
+ });
+
+ widgetControl.initializeEditor();
+ };
+
+ /**
+ * Sync widget instance data sanitized from server back onto widget model.
+ *
+ * This gets called via the 'widget-updated' event when saving a widget from
+ * the widgets admin screen and also via the 'widget-synced' event when making
+ * a change to a widget in the customizer.
+ *
+ * @memberOf wp.textWidgets
+ *
+ * @param {jQuery.Event} event - Event.
+ * @param {jQuery} widgetContainer - Widget container element.
+ * @return {void}
+ */
+ component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) {
+ var widgetForm, widgetId, widgetControl, idBase;
+ widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
+
+ idBase = widgetForm.find( '> .id_base' ).val();
+ if ( -1 === component.idBases.indexOf( idBase ) ) {
+ return;
+ }
+
+ widgetId = widgetForm.find( '> .widget-id' ).val();
+ widgetControl = component.widgetControls[ widgetId ];
+ if ( ! widgetControl ) {
+ return;
+ }
+
+ widgetControl.updateFields();
+ };
+
+ /**
+ * Initialize functionality.
+ *
+ * This function exists to prevent the JS file from having to boot itself.
+ * When WordPress enqueues this script, it should have an inline script
+ * attached which calls wp.textWidgets.init().
+ *
+ * @memberOf wp.textWidgets
+ *
+ * @return {void}
+ */
+ component.init = function init() {
+ var $document = $( document );
+ $document.on( 'widget-added', component.handleWidgetAdded );
+ $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated );
+
+ /*
+ * Manually trigger widget-added events for media widgets on the admin
+ * screen once they are expanded. The widget-added event is not triggered
+ * for each pre-existing widget on the widgets admin screen like it is
+ * on the customizer. Likewise, the customizer only triggers widget-added
+ * when the widget is expanded to just-in-time construct the widget form
+ * when it is actually going to be displayed. So the following implements
+ * the same for the widgets admin screen, to invoke the widget-added
+ * handler when a pre-existing media widget is expanded.
+ */
+ $( function initializeExistingWidgetContainers() {
+ var widgetContainers;
+ if ( 'widgets' !== window.pagenow ) {
+ return;
+ }
+ widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' );
+ widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() {
+ var widgetContainer = $( this );
+ component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer );
+ });
+
+ // Accessibility mode.
+ component.setupAccessibleMode();
+ });
+ };
+
+ return component;
+})( jQuery );
diff --git a/wp-admin/js/widgets/text-widgets.min.js b/wp-admin/js/widgets/text-widgets.min.js
new file mode 100644
index 0000000..6877ac8
--- /dev/null
+++ b/wp-admin/js/widgets/text-widgets.min.js
@@ -0,0 +1,2 @@
+/*! This file is auto-generated */
+wp.textWidgets=function(r){"use strict";var u={dismissedPointers:[],idBases:["text"]};return u.TextWidgetControl=Backbone.View.extend({events:{},initialize:function(e){var n=this;if(!e.el)throw new Error("Missing options.el");if(!e.syncContainer)throw new Error("Missing options.syncContainer");Backbone.View.prototype.initialize.call(n,e),n.syncContainer=e.syncContainer,n.$el.addClass("text-widget-fields"),n.$el.html(wp.template("widget-text-control-fields")),n.customHtmlWidgetPointer=n.$el.find(".wp-pointer.custom-html-widget-pointer"),n.customHtmlWidgetPointer.length&&(n.customHtmlWidgetPointer.find(".close").on("click",function(e){e.preventDefault(),n.customHtmlWidgetPointer.hide(),r("#"+n.fields.text.attr("id")+"-html").trigger("focus"),n.dismissPointers(["text_widget_custom_html"])}),n.customHtmlWidgetPointer.find(".add-widget").on("click",function(e){e.preventDefault(),n.customHtmlWidgetPointer.hide(),n.openAvailableWidgetsPanel()})),n.pasteHtmlPointer=n.$el.find(".wp-pointer.paste-html-pointer"),n.pasteHtmlPointer.length&&n.pasteHtmlPointer.find(".close").on("click",function(e){e.preventDefault(),n.pasteHtmlPointer.hide(),n.editor.focus(),n.dismissPointers(["text_widget_custom_html","text_widget_paste_html"])}),n.fields={title:n.$el.find(".title"),text:n.$el.find(".text")},_.each(n.fields,function(t,i){t.on("input change",function(){var e=n.syncContainer.find(".sync-input."+i);e.val()!==t.val()&&(e.val(t.val()),e.trigger("change"))}),t.val(n.syncContainer.find(".sync-input."+i).val())})},dismissPointers:function(e){_.each(e,function(e){wp.ajax.post("dismiss-wp-pointer",{pointer:e}),u.dismissedPointers.push(e)})},openAvailableWidgetsPanel:function(){var t;wp.customize.section.each(function(e){e.extended(wp.customize.Widgets.SidebarSection)&&e.expanded()&&(t=wp.customize.control("sidebars_widgets["+e.params.sidebarId+"]"))}),t&&setTimeout(function(){wp.customize.Widgets.availableWidgetsPanel.open(t),wp.customize.Widgets.availableWidgetsPanel.$search.val("HTML").trigger("keyup")})},updateFields:function(){var e,t=this;t.fields.title.is(document.activeElement)||(e=t.syncContainer.find(".sync-input.title"),t.fields.title.val(e.val())),e=t.syncContainer.find(".sync-input.text"),t.fields.text.is(":visible")?t.fields.text.is(document.activeElement)||t.fields.text.val(e.val()):t.editor&&!t.editorFocused&&e.val()!==t.fields.text.val()&&t.editor.setContent(wp.oldEditor.autop(e.val()))},initializeEditor:function(){var d,e,o,t,s=this,a=1e3,l=!1,c=!1;e=s.fields.text,d=e.attr("id"),t=e.val(),o=function(){s.editor.isDirty()&&(wp.customize&&wp.customize.state&&(wp.customize.state("processing").set(wp.customize.state("processing").get()+1),_.delay(function(){wp.customize.state("processing").set(wp.customize.state("processing").get()-1)},300)),s.editor.isHidden()||s.editor.save()),c&&t!==e.val()&&(e.trigger("change"),c=!1,t=e.val())},s.syncContainer.closest(".widget").find("[name=savewidget]:first").on("click",function(){o()}),function e(){var t,i,n;if(document.getElementById(d))if(void 0===window.tinymce)wp.oldEditor.initialize(d,{quicktags:!0,mediaButtons:!0});else{if(tinymce.get(d)&&(l=tinymce.get(d).isHidden(),wp.oldEditor.remove(d)),r(document).one("wp-before-tinymce-init.text-widget-init",function(e,t){t.plugins&&!/\bwpview\b/.test(t.plugins)&&(t.plugins+=",wpview")}),wp.oldEditor.initialize(d,{tinymce:{wpautop:!0},quicktags:!0,mediaButtons:!0}),n=function(e){e.show(),e.find(".close").trigger("focus"),wp.a11y.speak(e.find("h3, p").map(function(){return r(this).text()}).get().join("\n\n"))},!(t=window.tinymce.get(d)))throw new Error("Failed to initialize editor");i=function(){r(t.getWin()).on("pagehide",function(){_.defer(e)}),l&&switchEditors.go(d,"html"),r("#"+d+"-html").on("click",function(){s.pasteHtmlPointer.hide(),-1===u.dismissedPointers.indexOf("text_widget_custom_html")&&n(s.customHtmlWidgetPointer)}),r("#"+d+"-tmce").on("click",function(){s.customHtmlWidgetPointer.hide()}),t.on("pastepreprocess",function(e){e=e.content,-1===u.dismissedPointers.indexOf("text_widget_paste_html")&&e&&/&lt;\w+.*?&gt;/.test(e)&&_.delay(function(){n(s.pasteHtmlPointer)},250)})},t.initialized?i():t.on("init",i),s.editorFocused=!1,t.on("focus",function(){s.editorFocused=!0}),t.on("paste",function(){t.setDirty(!0),o()}),t.on("NodeChange",function(){c=!0}),t.on("NodeChange",_.debounce(o,a)),t.on("blur hide",function(){s.editorFocused=!1,o()}),s.editor=t}}()}}),u.widgetControls={},u.handleWidgetAdded=function(e,t){var i,n,d,o=t.find("> .widget-inside > .form, > .widget-inside > form"),s=o.find("> .id_base").val();-1===u.idBases.indexOf(s)||(s=o.find(".widget-id").val(),u.widgetControls[s])||o.find(".visual").val()&&(o=r("<div></div>"),(d=t.find(".widget-content:first")).before(o),i=new u.TextWidgetControl({el:o,syncContainer:d}),u.widgetControls[s]=i,(n=function(){t.hasClass("open")?i.initializeEditor():setTimeout(n,50)})())},u.setupAccessibleMode=function(){var e,t=r(".editwidget > form");0!==t.length&&(e=t.find(".id_base").val(),-1!==u.idBases.indexOf(e))&&t.find(".visual").val()&&(e=r("<div></div>"),(t=t.find("> .widget-inside")).before(e),new u.TextWidgetControl({el:e,syncContainer:t}).initializeEditor())},u.handleWidgetUpdated=function(e,t){var t=t.find("> .widget-inside > .form, > .widget-inside > form"),i=t.find("> .id_base").val();-1!==u.idBases.indexOf(i)&&(i=t.find("> .widget-id").val(),t=u.widgetControls[i])&&t.updateFields()},u.init=function(){var e=r(document);e.on("widget-added",u.handleWidgetAdded),e.on("widget-synced widget-updated",u.handleWidgetUpdated),r(function(){"widgets"===window.pagenow&&(r(".widgets-holder-wrap:not(#available-widgets)").find("div.widget").one("click.toggle-widget-expanded",function(){var e=r(this);u.handleWidgetAdded(new jQuery.Event("widget-added"),e)}),u.setupAccessibleMode())})},u}(jQuery); \ No newline at end of file