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.

Built from https://develop.svn.wordpress.org/trunk@40941


git-svn-id: http://core.svn.wordpress.org/trunk@40791 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Weston Ruter 2017-06-25 18:48:41 +00:00
parent a507248146
commit b7ca48fff0
5 changed files with 141 additions and 41 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();
});
});
};

File diff suppressed because one or more lines are too long

View File

@ -25,8 +25,8 @@ wp.textWidgets = ( function( $ ) {
* Initialize.
*
* @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 ) {
@ -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

@ -1 +1 @@
wp.textWidgets=function(a){"use strict";var b={};return b.TextWidgetControl=Backbone.View.extend({events:{},initialize:function(b){var c=this;if(!b.el)throw new Error("Missing options.el");Backbone.View.prototype.initialize.call(c,b),c.fieldContainer=a('<div class="text-widget-fields"></div>'),c.fieldContainer.html(wp.template("widget-text-control-fields")),c.widgetContentContainer=c.$el.find(".widget-content:first"),c.widgetContentContainer.before(c.fieldContainer),c.fields={title:c.fieldContainer.find(".title"),text:c.fieldContainer.find(".text")},_.each(c.fields,function(b,d){b.on("input change",function(){var b=c.widgetContentContainer.find("input[type=hidden]."+d);b.val()!==a(this).val()&&(b.val(a(this).val()),b.trigger("change"))}),b.val(c.widgetContentContainer.find("input[type=hidden]."+d).val())})},updateFields:function(){var a,b=this;b.fields.title.is(document.activeElement)||(a=b.widgetContentContainer.find("input[type=hidden].title"),b.fields.title.val(a.val())),a=b.widgetContentContainer.find("input[type=hidden].text"),b.fields.text.is(":visible")?b.fields.text.is(document.activeElement)||b.fields.text.val(a.val()):b.editor&&!b.editorFocused&&a.val()!==b.fields.text.val()&&b.editor.setContent(wp.editor.autop(a.val()))},initializeEditor:function(){function b(){var h,i,j;if(document.getElementById(c)){if(tinymce.get(c)&&(g=tinymce.get(c).isHidden(),wp.editor.remove(c)),wp.editor.initialize(c,{tinymce:{wpautop:!0},quicktags:!0}),h=window.tinymce.get(c),!h)throw new Error("Failed to initialize editor");j=function(){a(h.getWin()).on("unload",function(){_.defer(b)}),g&&switchEditors.go(c,"toggle")},h.initialized?j():h.on("init",j),e.editorFocused=!1,i=function(){var a=300;h.isDirty()&&(wp.customize&&(wp.customize.state("processing").set(wp.customize.state("processing").get()+1),_.delay(function(){wp.customize.state("processing").set(wp.customize.state("processing").get()-1)},a)),h.save(),d.trigger("change"))},h.on("focus",function(){e.editorFocused=!0}),h.on("NodeChange",_.debounce(i,f)),h.on("blur",function(){e.editorFocused=!1,i()}),e.editor=h}}var c,d,e=this,f=1e3,g=!1;d=e.fields.text,c=d.attr("id"),b()}}),b.widgetControls={},b.handleWidgetAdded=function(a,c){var d,e,f,g,h,i,j=50;d=c.find("> .widget-inside > .form, > .widget-inside > form"),e=d.find("> .id_base").val(),"text"===e&&(g=d.find("> .widget-id").val(),b.widgetControls[g]||(f=new b.TextWidgetControl({el:c}),b.widgetControls[g]=f,h=c.parent(),(i=function(){h.is(":animated")?setTimeout(i,j):f.initializeEditor()})()))},b.handleWidgetUpdated=function(a,c){var d,e,f,g;d=c.find("> .widget-inside > .form, > .widget-inside > form"),g=d.find("> .id_base").val(),"text"===g&&(e=d.find("> .widget-id").val(),f=b.widgetControls[e],f&&f.updateFields())},b.init=function(){var c=a(document);c.on("widget-added",b.handleWidgetAdded),c.on("widget-synced widget-updated",b.handleWidgetUpdated),a(function(){var c;"widgets"===window.pagenow&&(c=a(".widgets-holder-wrap:not(#available-widgets)").find("div.widget"),c.one("click.toggle-widget-expanded",function(){var c=a(this);b.handleWidgetAdded(new jQuery.Event("widget-added"),c)}))})},b}(jQuery);
wp.textWidgets=function(a){"use strict";var b={};return b.TextWidgetControl=Backbone.View.extend({events:{},initialize:function(b){var c=this;if(!b.el)throw new Error("Missing options.el");if(!b.syncContainer)throw new Error("Missing options.syncContainer");Backbone.View.prototype.initialize.call(c,b),c.syncContainer=b.syncContainer,c.$el.addClass("text-widget-fields"),c.$el.html(wp.template("widget-text-control-fields")),c.fields={title:c.$el.find(".title"),text:c.$el.find(".text")},_.each(c.fields,function(b,d){b.on("input change",function(){var b=c.syncContainer.find("input[type=hidden]."+d);b.val()!==a(this).val()&&(b.val(a(this).val()),b.trigger("change"))}),b.val(c.syncContainer.find("input[type=hidden]."+d).val())})},updateFields:function(){var a,b=this;b.fields.title.is(document.activeElement)||(a=b.syncContainer.find("input[type=hidden].title"),b.fields.title.val(a.val())),a=b.syncContainer.find("input[type=hidden].text"),b.fields.text.is(":visible")?b.fields.text.is(document.activeElement)||b.fields.text.val(a.val()):b.editor&&!b.editorFocused&&a.val()!==b.fields.text.val()&&b.editor.setContent(wp.editor.autop(a.val()))},initializeEditor:function(){function b(){var h,i,j;if(document.getElementById(c)){if(tinymce.get(c)&&(g=tinymce.get(c).isHidden(),wp.editor.remove(c)),wp.editor.initialize(c,{tinymce:{wpautop:!0},quicktags:!0}),h=window.tinymce.get(c),!h)throw new Error("Failed to initialize editor");j=function(){a(h.getWin()).on("unload",function(){_.defer(b)}),g&&switchEditors.go(c,"toggle")},h.initialized?j():h.on("init",j),e.editorFocused=!1,i=function(){var a=300;h.isDirty()&&(wp.customize&&(wp.customize.state("processing").set(wp.customize.state("processing").get()+1),_.delay(function(){wp.customize.state("processing").set(wp.customize.state("processing").get()-1)},a)),h.save(),d.trigger("change"))},h.on("focus",function(){e.editorFocused=!0}),h.on("NodeChange",_.debounce(i,f)),h.on("blur",function(){e.editorFocused=!1,i()}),e.editor=h}}var c,d,e=this,f=1e3,g=!1;d=e.fields.text,c=d.attr("id"),b()}}),b.widgetControls={},b.handleWidgetAdded=function(c,d){var e,f,g,h,i,j,k,l,m=50;e=d.find("> .widget-inside > .form, > .widget-inside > form"),f=e.find("> .id_base").val(),"text"===f&&(h=e.find(".widget-id").val(),b.widgetControls[h]||(k=a("<div></div>"),l=d.find(".widget-content:first"),l.before(k),g=new b.TextWidgetControl({el:k,syncContainer:l}),b.widgetControls[h]=g,i=d.parent(),(j=function(){i.is(":animated")?setTimeout(j,m):g.initializeEditor()})()))},b.setupAccessibleMode=function(){var c,d,e,f,g;c=a(".editwidget > form"),0!==c.length&&(d=c.find("> .widget-control-actions > .id_base").val(),"text"===d&&(f=a("<div></div>"),g=c.find("> .widget-inside"),g.before(f),e=new b.TextWidgetControl({el:f,syncContainer:g}),e.initializeEditor()))},b.handleWidgetUpdated=function(a,c){var d,e,f,g;d=c.find("> .widget-inside > .form, > .widget-inside > form"),g=d.find("> .id_base").val(),"text"===g&&(e=d.find("> .widget-id").val(),f=b.widgetControls[e],f&&f.updateFields())},b.init=function(){var c=a(document);c.on("widget-added",b.handleWidgetAdded),c.on("widget-synced widget-updated",b.handleWidgetUpdated),a(function(){var c;"widgets"===window.pagenow&&(c=a(".widgets-holder-wrap:not(#available-widgets)").find("div.widget"),c.one("click.toggle-widget-expanded",function(){var c=a(this);b.handleWidgetAdded(new jQuery.Event("widget-added"),c)}),a(window).on("load",function(){b.setupAccessibleMode()}))})},b}(jQuery);

View File

@ -4,7 +4,7 @@
*
* @global string $wp_version
*/
$wp_version = '4.9-alpha-40940';
$wp_version = '4.9-alpha-40941';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.