diff --git a/src/wp-admin/js/widgets/media-widgets.js b/src/wp-admin/js/widgets/media-widgets.js index 1c4d6fecaf..598d94d8b1 100644 --- a/src/wp-admin/js/widgets/media-widgets.js +++ b/src/wp-admin/js/widgets/media-widgets.js @@ -435,7 +435,8 @@ wp.mediaWidgets = ( function( $ ) { * * @param {Object} options - Options. * @param {Backbone.Model} options.model - Model. - * @param {jQuery} options.el - Control container element. + * @param {jQuery} options.el - Control field container element. + * @param {jQuery} options.syncContainer - Container element where fields are synced for the server. * @returns {void} */ initialize: function initialize( options ) { @@ -443,12 +444,19 @@ wp.mediaWidgets = ( function( $ ) { Backbone.View.prototype.initialize.call( control, options ); - if ( ! control.el ) { - throw new Error( 'Missing options.el' ); - } 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' ); @@ -553,7 +561,7 @@ wp.mediaWidgets = ( function( $ ) { */ syncModelToInputs: function syncModelToInputs() { var control = this; - control.$el.next( '.widget-content' ).find( '.media-widget-instance-property' ).each( function() { + control.syncContainer.find( '.media-widget-instance-property' ).each( function() { var input = $( this ), value; value = control.model.get( input.data( 'property' ) ); if ( _.isUndefined( value ) ) { @@ -1009,9 +1017,8 @@ wp.mediaWidgets = ( function( $ ) { * @returns {void} */ component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) { - var widgetContent, controlContainer, widgetForm, idBase, ControlConstructor, ModelConstructor, modelAttributes, widgetControl, widgetModel, widgetId, widgetInside, animatedCheckDelay = 50, renderWhenAnimationDone; + var fieldContainer, syncContainer, widgetForm, idBase, ControlConstructor, ModelConstructor, modelAttributes, widgetControl, widgetModel, widgetId, widgetInside, 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. - widgetContent = widgetForm.find( '> .widget-content' ); idBase = widgetForm.find( '> .id_base' ).val(); widgetId = widgetForm.find( '> .widget-id' ).val(); @@ -1038,8 +1045,9 @@ wp.mediaWidgets = ( function( $ ) { * components", the JS template is rendered outside of the normal form * container. */ - controlContainer = $( '
' ); - widgetContent.before( controlContainer ); + fieldContainer = $( '
' ); + 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. @@ -1047,7 +1055,7 @@ wp.mediaWidgets = ( function( $ ) { * from the start, without having to sync with hidden fields. See . */ modelAttributes = {}; - widgetContent.find( '.media-widget-instance-property' ).each( function() { + syncContainer.find( '.media-widget-instance-property' ).each( function() { var input = $( this ); modelAttributes[ input.data( 'property' ) ] = input.val(); }); @@ -1056,7 +1064,8 @@ wp.mediaWidgets = ( function( $ ) { widgetModel = new ModelConstructor( modelAttributes ); widgetControl = new ControlConstructor({ - el: controlContainer, + el: fieldContainer, + syncContainer: syncContainer, model: widgetModel }); @@ -1084,6 +1093,51 @@ wp.mediaWidgets = ( function( $ ) { component.widgetControls[ widgetModel.get( 'widget_id' ) ] = widgetControl; }; + /** + * Setup widget in accessibility mode. + * + * @returns {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( '> .widget-control-actions > .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 = $( '
' ); + 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. * @@ -1152,6 +1206,11 @@ wp.mediaWidgets = ( function( $ ) { var widgetContainer = $( this ); component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer ); }); + + // Accessibility mode. + $( window ).on( 'load', function() { + component.setupAccessibleMode(); + }); }); }; diff --git a/src/wp-admin/js/widgets/text-widgets.js b/src/wp-admin/js/widgets/text-widgets.js index 2108b65548..6f057044cf 100644 --- a/src/wp-admin/js/widgets/text-widgets.js +++ b/src/wp-admin/js/widgets/text-widgets.js @@ -24,9 +24,9 @@ wp.textWidgets = ( function( $ ) { /** * Initialize. * - * @param {Object} options - Options. - * @param {Backbone.Model} options.model - Model. - * @param {jQuery} options.el - Control container element. + * @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. * @returns {void} */ initialize: function initialize( options ) { @@ -35,34 +35,25 @@ wp.textWidgets = ( function( $ ) { 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; - /* - * 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. - */ - control.fieldContainer = $( '
' ); - control.fieldContainer.html( wp.template( 'widget-text-control-fields' ) ); - control.widgetContentContainer = control.$el.find( '.widget-content:first' ); - control.widgetContentContainer.before( control.fieldContainer ); + control.$el.addClass( 'text-widget-fields' ); + control.$el.html( wp.template( 'widget-text-control-fields' ) ); control.fields = { - title: control.fieldContainer.find( '.title' ), - text: control.fieldContainer.find( '.text' ) + 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.widgetContentContainer.find( 'input[type=hidden].' + fieldName ); + var syncInput = control.syncContainer.find( 'input[type=hidden].' + fieldName ); if ( syncInput.val() !== $( this ).val() ) { syncInput.val( $( this ).val() ); syncInput.trigger( 'change' ); @@ -70,7 +61,7 @@ wp.textWidgets = ( function( $ ) { }); // Note that syncInput cannot be re-used because it will be destroyed with each widget-updated event. - fieldInput.val( control.widgetContentContainer.find( 'input[type=hidden].' + fieldName ).val() ); + fieldInput.val( control.syncContainer.find( 'input[type=hidden].' + fieldName ).val() ); }); }, @@ -87,11 +78,11 @@ wp.textWidgets = ( function( $ ) { var control = this, syncInput; if ( ! control.fields.title.is( document.activeElement ) ) { - syncInput = control.widgetContentContainer.find( 'input[type=hidden].title' ); + syncInput = control.syncContainer.find( 'input[type=hidden].title' ); control.fields.title.val( syncInput.val() ); } - syncInput = control.widgetContentContainer.find( 'input[type=hidden].text' ); + syncInput = control.syncContainer.find( 'input[type=hidden].text' ); if ( control.fields.text.is( ':visible' ) ) { if ( ! control.fields.text.is( document.activeElement ) ) { control.fields.text.val( syncInput.val() ); @@ -219,7 +210,7 @@ wp.textWidgets = ( function( $ ) { * @returns {void} */ component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) { - var widgetForm, idBase, widgetControl, widgetId, animatedCheckDelay = 50, widgetInside, renderWhenAnimationDone; + var widgetForm, idBase, widgetControl, widgetId, animatedCheckDelay = 50, widgetInside, 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(); @@ -228,13 +219,29 @@ wp.textWidgets = ( function( $ ) { } // Prevent initializing already-added widgets. - widgetId = widgetForm.find( '> .widget-id' ).val(); + 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 = $( '
' ); + syncContainer = widgetContainer.find( '.widget-content:first' ); + syncContainer.before( fieldContainer ); + widgetControl = new component.TextWidgetControl({ - el: widgetContainer + el: fieldContainer, + syncContainer: syncContainer }); component.widgetControls[ widgetId ] = widgetControl; @@ -256,6 +263,35 @@ wp.textWidgets = ( function( $ ) { renderWhenAnimationDone(); }; + /** + * Setup widget in accessibility mode. + * + * @returns {void} + */ + component.setupAccessibleMode = function setupAccessibleMode() { + var widgetForm, idBase, widgetControl, fieldContainer, syncContainer; + widgetForm = $( '.editwidget > form' ); + if ( 0 === widgetForm.length ) { + return; + } + + idBase = widgetForm.find( '> .widget-control-actions > .id_base' ).val(); + if ( 'text' !== idBase ) { + return; + } + + fieldContainer = $( '
' ); + 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. * @@ -319,6 +355,11 @@ wp.textWidgets = ( function( $ ) { var widgetContainer = $( this ); component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer ); }); + + // Accessibility mode. + $( window ).on( 'load', function() { + component.setupAccessibleMode(); + }); }); }; diff --git a/tests/qunit/wp-admin/js/widgets/test-media-image-widget.js b/tests/qunit/wp-admin/js/widgets/test-media-image-widget.js index 4c2c8a4378..212726e641 100644 --- a/tests/qunit/wp-admin/js/widgets/test-media-image-widget.js +++ b/tests/qunit/wp-admin/js/widgets/test-media-image-widget.js @@ -17,6 +17,8 @@ imageWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_image(); imageWidgetControlInstance = new ImageWidgetControl({ + el: jQuery( '
' ), + syncContainer: jQuery( '
' ), model: imageWidgetModelInstance }); @@ -84,6 +86,8 @@ imageWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_image(); imageWidgetControlInstance = new wp.mediaWidgets.controlConstructors.media_image({ + el: jQuery( '
' ), + syncContainer: jQuery( '
' ), model: imageWidgetModelInstance }); equal( imageWidgetControlInstance.$el.find( 'img' ).length, 0, 'No images should be rendered' ); diff --git a/tests/qunit/wp-admin/js/widgets/test-media-video-widget.js b/tests/qunit/wp-admin/js/widgets/test-media-video-widget.js index bef61c5317..0abd1c0289 100644 --- a/tests/qunit/wp-admin/js/widgets/test-media-video-widget.js +++ b/tests/qunit/wp-admin/js/widgets/test-media-video-widget.js @@ -17,6 +17,8 @@ videoWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_video(); videoWidgetControlInstance = new VideoWidgetControl({ + el: jQuery( '
' ), + syncContainer: jQuery( '
' ), model: videoWidgetModelInstance }); @@ -46,6 +48,8 @@ videoWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_video(); videoWidgetControlInstance = new wp.mediaWidgets.controlConstructors.media_video({ + el: jQuery( '
' ), + syncContainer: jQuery( '
' ), model: videoWidgetModelInstance }); equal( videoWidgetControlInstance.$el.find( 'a' ).length, 0, 'No video links should be rendered' );