Widgets: Add accessibility mode support for TinyMCE-enhanced Text and Media widgets (Video, Audio, Images).

Amends [40640], [40631].
Props westonruter, afercia.
See #35243, #32417.
Fixes #40986.


git-svn-id: https://develop.svn.wordpress.org/trunk@40941 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Weston Ruter 2017-06-25 18:47:13 +00:00
parent d7fc80ca43
commit f25d9d7909
4 changed files with 146 additions and 38 deletions

View File

@ -435,7 +435,8 @@ wp.mediaWidgets = ( function( $ ) {
* *
* @param {Object} options - Options. * @param {Object} options - Options.
* @param {Backbone.Model} options.model - Model. * @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} * @returns {void}
*/ */
initialize: function initialize( options ) { initialize: function initialize( options ) {
@ -443,12 +444,19 @@ wp.mediaWidgets = ( function( $ ) {
Backbone.View.prototype.initialize.call( control, options ); Backbone.View.prototype.initialize.call( control, options );
if ( ! control.el ) {
throw new Error( 'Missing options.el' );
}
if ( ! ( control.model instanceof component.MediaWidgetModel ) ) { if ( ! ( control.model instanceof component.MediaWidgetModel ) ) {
throw new Error( 'Missing options.model' ); 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. // Allow methods to be passed in with control context preserved.
_.bindAll( control, 'syncModelToInputs', 'render', 'updateSelectedAttachment', 'renderPreview' ); _.bindAll( control, 'syncModelToInputs', 'render', 'updateSelectedAttachment', 'renderPreview' );
@ -553,7 +561,7 @@ wp.mediaWidgets = ( function( $ ) {
*/ */
syncModelToInputs: function syncModelToInputs() { syncModelToInputs: function syncModelToInputs() {
var control = this; 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; var input = $( this ), value;
value = control.model.get( input.data( 'property' ) ); value = control.model.get( input.data( 'property' ) );
if ( _.isUndefined( value ) ) { if ( _.isUndefined( value ) ) {
@ -1009,9 +1017,8 @@ wp.mediaWidgets = ( function( $ ) {
* @returns {void} * @returns {void}
*/ */
component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) { 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. 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(); idBase = widgetForm.find( '> .id_base' ).val();
widgetId = widgetForm.find( '> .widget-id' ).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 * components", the JS template is rendered outside of the normal form
* container. * container.
*/ */
controlContainer = $( '<div class="media-widget-control"></div>' ); fieldContainer = $( '<div></div>' );
widgetContent.before( controlContainer ); 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. * 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 <https://core.trac.wordpress.org/ticket/33507>. * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>.
*/ */
modelAttributes = {}; modelAttributes = {};
widgetContent.find( '.media-widget-instance-property' ).each( function() { syncContainer.find( '.media-widget-instance-property' ).each( function() {
var input = $( this ); var input = $( this );
modelAttributes[ input.data( 'property' ) ] = input.val(); modelAttributes[ input.data( 'property' ) ] = input.val();
}); });
@ -1056,7 +1064,8 @@ wp.mediaWidgets = ( function( $ ) {
widgetModel = new ModelConstructor( modelAttributes ); widgetModel = new ModelConstructor( modelAttributes );
widgetControl = new ControlConstructor({ widgetControl = new ControlConstructor({
el: controlContainer, el: fieldContainer,
syncContainer: syncContainer,
model: widgetModel model: widgetModel
}); });
@ -1084,6 +1093,51 @@ wp.mediaWidgets = ( function( $ ) {
component.widgetControls[ widgetModel.get( 'widget_id' ) ] = widgetControl; 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 = $( '<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. * Sync widget instance data sanitized from server back onto widget model.
* *
@ -1152,6 +1206,11 @@ wp.mediaWidgets = ( function( $ ) {
var widgetContainer = $( this ); var widgetContainer = $( this );
component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer ); component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer );
}); });
// Accessibility mode.
$( window ).on( 'load', function() {
component.setupAccessibleMode();
});
}); });
}; };

View File

@ -25,8 +25,8 @@ wp.textWidgets = ( function( $ ) {
* Initialize. * Initialize.
* *
* @param {Object} options - Options. * @param {Object} options - Options.
* @param {Backbone.Model} options.model - Model. * @param {jQuery} options.el - Control field container element.
* @param {jQuery} options.el - Control container element. * @param {jQuery} options.syncContainer - Container element where fields are synced for the server.
* @returns {void} * @returns {void}
*/ */
initialize: function initialize( options ) { initialize: function initialize( options ) {
@ -35,34 +35,25 @@ wp.textWidgets = ( function( $ ) {
if ( ! options.el ) { if ( ! options.el ) {
throw new Error( 'Missing options.el' ); throw new Error( 'Missing options.el' );
} }
if ( ! options.syncContainer ) {
throw new Error( 'Missing options.syncContainer' );
}
Backbone.View.prototype.initialize.call( control, options ); Backbone.View.prototype.initialize.call( control, options );
control.syncContainer = options.syncContainer;
/* control.$el.addClass( 'text-widget-fields' );
* Create a container element for the widget control fields. control.$el.html( wp.template( 'widget-text-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 = $( '<div class="text-widget-fields"></div>' );
control.fieldContainer.html( wp.template( 'widget-text-control-fields' ) );
control.widgetContentContainer = control.$el.find( '.widget-content:first' );
control.widgetContentContainer.before( control.fieldContainer );
control.fields = { control.fields = {
title: control.fieldContainer.find( '.title' ), title: control.$el.find( '.title' ),
text: control.fieldContainer.find( '.text' ) text: control.$el.find( '.text' )
}; };
// Sync input fields to hidden sync fields which actually get sent to the server. // Sync input fields to hidden sync fields which actually get sent to the server.
_.each( control.fields, function( fieldInput, fieldName ) { _.each( control.fields, function( fieldInput, fieldName ) {
fieldInput.on( 'input change', function updateSyncField() { 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() ) { if ( syncInput.val() !== $( this ).val() ) {
syncInput.val( $( this ).val() ); syncInput.val( $( this ).val() );
syncInput.trigger( 'change' ); 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. // 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; var control = this, syncInput;
if ( ! control.fields.title.is( document.activeElement ) ) { 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() ); 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( ':visible' ) ) {
if ( ! control.fields.text.is( document.activeElement ) ) { if ( ! control.fields.text.is( document.activeElement ) ) {
control.fields.text.val( syncInput.val() ); control.fields.text.val( syncInput.val() );
@ -219,7 +210,7 @@ wp.textWidgets = ( function( $ ) {
* @returns {void} * @returns {void}
*/ */
component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) { 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. 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(); idBase = widgetForm.find( '> .id_base' ).val();
@ -228,13 +219,29 @@ wp.textWidgets = ( function( $ ) {
} }
// Prevent initializing already-added widgets. // Prevent initializing already-added widgets.
widgetId = widgetForm.find( '> .widget-id' ).val(); widgetId = widgetForm.find( '.widget-id' ).val();
if ( component.widgetControls[ widgetId ] ) { if ( component.widgetControls[ widgetId ] ) {
return; 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.TextWidgetControl({ widgetControl = new component.TextWidgetControl({
el: widgetContainer el: fieldContainer,
syncContainer: syncContainer
}); });
component.widgetControls[ widgetId ] = widgetControl; component.widgetControls[ widgetId ] = widgetControl;
@ -256,6 +263,35 @@ wp.textWidgets = ( function( $ ) {
renderWhenAnimationDone(); 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 = $( '<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. * Sync widget instance data sanitized from server back onto widget model.
* *
@ -319,6 +355,11 @@ wp.textWidgets = ( function( $ ) {
var widgetContainer = $( this ); var widgetContainer = $( this );
component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer ); component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer );
}); });
// Accessibility mode.
$( window ).on( 'load', function() {
component.setupAccessibleMode();
});
}); });
}; };

View File

@ -17,6 +17,8 @@
imageWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_image(); imageWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_image();
imageWidgetControlInstance = new ImageWidgetControl({ imageWidgetControlInstance = new ImageWidgetControl({
el: jQuery( '<div></div>' ),
syncContainer: jQuery( '<div></div>' ),
model: imageWidgetModelInstance model: imageWidgetModelInstance
}); });
@ -84,6 +86,8 @@
imageWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_image(); imageWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_image();
imageWidgetControlInstance = new wp.mediaWidgets.controlConstructors.media_image({ imageWidgetControlInstance = new wp.mediaWidgets.controlConstructors.media_image({
el: jQuery( '<div></div>' ),
syncContainer: jQuery( '<div></div>' ),
model: imageWidgetModelInstance model: imageWidgetModelInstance
}); });
equal( imageWidgetControlInstance.$el.find( 'img' ).length, 0, 'No images should be rendered' ); equal( imageWidgetControlInstance.$el.find( 'img' ).length, 0, 'No images should be rendered' );

View File

@ -17,6 +17,8 @@
videoWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_video(); videoWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_video();
videoWidgetControlInstance = new VideoWidgetControl({ videoWidgetControlInstance = new VideoWidgetControl({
el: jQuery( '<div></div>' ),
syncContainer: jQuery( '<div></div>' ),
model: videoWidgetModelInstance model: videoWidgetModelInstance
}); });
@ -46,6 +48,8 @@
videoWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_video(); videoWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_video();
videoWidgetControlInstance = new wp.mediaWidgets.controlConstructors.media_video({ videoWidgetControlInstance = new wp.mediaWidgets.controlConstructors.media_video({
el: jQuery( '<div></div>' ),
syncContainer: jQuery( '<div></div>' ),
model: videoWidgetModelInstance model: videoWidgetModelInstance
}); });
equal( videoWidgetControlInstance.$el.find( 'a' ).length, 0, 'No video links should be rendered' ); equal( videoWidgetControlInstance.$el.find( 'a' ).length, 0, 'No video links should be rendered' );