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 {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 = $( '<div class="media-widget-control"></div>' );
widgetContent.before( controlContainer );
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.
@ -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>.
*/
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 = $( '<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.
*
@ -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();
});
});
};

View File

@ -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 = $( '<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.$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 = $( '<div></div>' );
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 = $( '<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.
*
@ -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();
});
});
};

View File

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