| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 /** 2 * @output wp-admin/js/widgets/custom-html-widgets.js 3 */ 4 5 /* global wp */ 6 /* eslint consistent-this: [ "error", "control" ] */ 7 /* eslint no-magic-numbers: ["error", { "ignore": [0,1,-1] }] */ 8 9 /** 10 * @namespace wp.customHtmlWidget 11 * @memberOf wp 12 * 13 * @param {JQueryStatic} $ The jQuery object. 14 */ 15 wp.customHtmlWidgets = ( function( $ ) { 16 'use strict'; 17 18 var component = { 19 idBases: [ 'custom_html' ], 20 codeEditorSettings: {}, 21 l10n: { 22 errorNotice: { 23 singular: '', 24 plural: '' 25 } 26 } 27 }; 28 29 component.CustomHtmlWidgetControl = Backbone.View.extend(/** @lends wp.customHtmlWidgets.CustomHtmlWidgetControl.prototype */{ 30 31 /** 32 * View events. 33 * 34 * @type {Object} 35 */ 36 events: {}, 37 38 /** 39 * Text widget control. 40 * 41 * @constructs wp.customHtmlWidgets.CustomHtmlWidgetControl 42 * @augments Backbone.View 43 * @abstract 44 * 45 * @param {Object} options Options. 46 * @param {jQuery} options.el Control field container element. 47 * @param {jQuery} options.syncContainer Container element where fields are synced for the server. 48 * 49 * @return {void} 50 */ 51 initialize: function initialize( options ) { 52 var control = this; 53 54 if ( ! options.el ) { 55 throw new Error( 'Missing options.el' ); 56 } 57 if ( ! options.syncContainer ) { 58 throw new Error( 'Missing options.syncContainer' ); 59 } 60 61 Backbone.View.prototype.initialize.call( control, options ); 62 control.syncContainer = options.syncContainer; 63 control.widgetIdBase = control.syncContainer.parent().find( '.id_base' ).val(); 64 control.widgetNumber = control.syncContainer.parent().find( '.widget_number' ).val(); 65 control.customizeSettingId = 'widget_' + control.widgetIdBase + '[' + String( control.widgetNumber ) + ']'; 66 67 control.$el.addClass( 'custom-html-widget-fields' ); 68 control.$el.html( wp.template( 'widget-custom-html-control-fields' )( { codeEditorDisabled: component.codeEditorSettings.disabled } ) ); 69 70 control.errorNoticeContainer = control.$el.find( '.code-editor-error-container' ); 71 control.currentErrorAnnotations = []; 72 control.saveButton = control.syncContainer.add( control.syncContainer.parent().find( '.widget-control-actions' ) ).find( '.widget-control-save, #savewidget' ); 73 control.saveButton.addClass( 'custom-html-widget-save-button' ); // To facilitate style targeting. 74 75 control.fields = { 76 title: control.$el.find( '.title' ), 77 content: control.$el.find( '.content' ) 78 }; 79 80 // Sync input fields to hidden sync fields which actually get sent to the server. 81 _.each( control.fields, function( fieldInput, fieldName ) { 82 fieldInput.on( 'input change', function updateSyncField() { 83 var syncInput = control.syncContainer.find( '.sync-input.' + fieldName ); 84 if ( syncInput.val() !== fieldInput.val() ) { 85 syncInput.val( fieldInput.val() ); 86 syncInput.trigger( 'change' ); 87 } 88 }); 89 90 // Note that syncInput cannot be re-used because it will be destroyed with each widget-updated event. 91 fieldInput.val( control.syncContainer.find( '.sync-input.' + fieldName ).val() ); 92 }); 93 }, 94 95 /** 96 * Update input fields from the sync fields. 97 * 98 * This function is called at the widget-updated and widget-synced events. 99 * A field will only be updated if it is not currently focused, to avoid 100 * overwriting content that the user is entering. 101 * 102 * @return {void} 103 */ 104 updateFields: function updateFields() { 105 var control = this, syncInput; 106 107 if ( ! control.fields.title.is( document.activeElement ) ) { 108 syncInput = control.syncContainer.find( '.sync-input.title' ); 109 control.fields.title.val( syncInput.val() ); 110 } 111 112 /* 113 * Prevent updating content when the editor is focused or if there are current error annotations, 114 * to prevent the editor's contents from getting sanitized as soon as a user removes focus from 115 * the editor. This is particularly important for users who cannot unfiltered_html. 116 */ 117 control.contentUpdateBypassed = control.fields.content.is( document.activeElement ) || control.editor && control.editor.codemirror.state.focused || 0 !== control.currentErrorAnnotations.length; 118 if ( ! control.contentUpdateBypassed ) { 119 syncInput = control.syncContainer.find( '.sync-input.content' ); 120 control.fields.content.val( syncInput.val() ); 121 } 122 }, 123 124 /** 125 * Show linting error notice. 126 * 127 * @param {Array} errorAnnotations Error annotations. 128 * @return {void} 129 */ 130 updateErrorNotice: function( errorAnnotations ) { 131 var control = this, errorNotice, message = '', customizeSetting; 132 133 if ( 1 === errorAnnotations.length ) { 134 message = component.l10n.errorNotice.singular.replace( '%d', '1' ); 135 } else if ( errorAnnotations.length > 1 ) { 136 message = component.l10n.errorNotice.plural.replace( '%d', String( errorAnnotations.length ) ); 137 } 138 139 if ( control.fields.content[0].setCustomValidity ) { 140 control.fields.content[0].setCustomValidity( message ); 141 } 142 143 if ( wp.customize && wp.customize.has( control.customizeSettingId ) ) { 144 customizeSetting = wp.customize( control.customizeSettingId ); 145 customizeSetting.notifications.remove( 'htmlhint_error' ); 146 if ( 0 !== errorAnnotations.length ) { 147 customizeSetting.notifications.add( 'htmlhint_error', new wp.customize.Notification( 'htmlhint_error', { 148 message: message, 149 type: 'error' 150 } ) ); 151 } 152 } else if ( 0 !== errorAnnotations.length ) { 153 errorNotice = $( '<div class="inline notice notice-error notice-alt" role="alert"></div>' ); 154 errorNotice.append( $( '<p></p>', { 155 text: message 156 } ) ); 157 control.errorNoticeContainer.empty(); 158 control.errorNoticeContainer.append( errorNotice ); 159 control.errorNoticeContainer.slideDown( 'fast' ); 160 wp.a11y.speak( message ); 161 } else { 162 control.errorNoticeContainer.slideUp( 'fast' ); 163 } 164 }, 165 166 /** 167 * Initialize editor. 168 * 169 * @return {void} 170 */ 171 initializeEditor: function initializeEditor() { 172 var control = this, settings; 173 174 if ( component.codeEditorSettings.disabled ) { 175 return; 176 } 177 178 settings = _.extend( {}, component.codeEditorSettings, { 179 180 /** 181 * Handle tabbing to the field before the editor. 182 * 183 * @ignore 184 * 185 * @return {void} 186 */ 187 onTabPrevious: function onTabPrevious() { 188 control.fields.title.focus(); 189 }, 190 191 /** 192 * Handle tabbing to the field after the editor. 193 * 194 * @ignore 195 * 196 * @return {void} 197 */ 198 onTabNext: function onTabNext() { 199 var tabbables = control.syncContainer.add( control.syncContainer.parent().find( '.widget-position, .widget-control-actions' ) ).find( ':tabbable' ); 200 tabbables.first().focus(); 201 }, 202 203 /** 204 * Disable save button and store linting errors for use in updateFields. 205 * 206 * @ignore 207 * 208 * @param {Array} errorAnnotations Error notifications. 209 * @return {void} 210 */ 211 onChangeLintingErrors: function onChangeLintingErrors( errorAnnotations ) { 212 control.currentErrorAnnotations = errorAnnotations; 213 }, 214 215 /** 216 * Update error notice. 217 * 218 * @ignore 219 * 220 * @param {Array} errorAnnotations Error annotations. 221 * @return {void} 222 */ 223 onUpdateErrorNotice: function onUpdateErrorNotice( errorAnnotations ) { 224 control.saveButton.toggleClass( 'validation-blocked disabled', errorAnnotations.length > 0 ); 225 control.updateErrorNotice( errorAnnotations ); 226 } 227 }); 228 229 control.editor = wp.codeEditor.initialize( control.fields.content, settings ); 230 231 // Improve the editor accessibility. 232 $( control.editor.codemirror.display.lineDiv ) 233 .attr({ 234 role: 'textbox', 235 'aria-multiline': 'true', 236 'aria-labelledby': control.fields.content[0].id + '-label', 237 'aria-describedby': 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4' 238 }); 239 240 // Focus the editor when clicking on its label. 241 $( '#' + control.fields.content[0].id + '-label' ).on( 'click', function() { 242 control.editor.codemirror.focus(); 243 }); 244 245 control.fields.content.on( 'change', function() { 246 if ( this.value !== control.editor.codemirror.getValue() ) { 247 control.editor.codemirror.setValue( this.value ); 248 } 249 }); 250 control.editor.codemirror.on( 'change', function() { 251 var value = control.editor.codemirror.getValue(); 252 if ( value !== control.fields.content.val() ) { 253 control.fields.content.val( value ).trigger( 'change' ); 254 } 255 }); 256 257 // 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. 258 control.editor.codemirror.on( 'blur', function() { 259 if ( control.contentUpdateBypassed ) { 260 control.syncContainer.find( '.sync-input.content' ).trigger( 'change' ); 261 } 262 }); 263 264 // Prevent hitting Esc from collapsing the widget control. 265 if ( wp.customize ) { 266 control.editor.codemirror.on( 'keydown', function onKeydown( codemirror, event ) { 267 var escKeyCode = 27; 268 if ( escKeyCode === event.keyCode ) { 269 event.stopPropagation(); 270 } 271 }); 272 } 273 } 274 }); 275 276 /** 277 * Mapping of widget ID to instances of CustomHtmlWidgetControl subclasses. 278 * 279 * @alias wp.customHtmlWidgets.widgetControls 280 * 281 * @type {Object.<string, wp.textWidgets.CustomHtmlWidgetControl>} 282 */ 283 component.widgetControls = {}; 284 285 /** 286 * Handle widget being added or initialized for the first time at the widget-added event. 287 * 288 * @alias wp.customHtmlWidgets.handleWidgetAdded 289 * 290 * @param {jQuery.Event} event Event. 291 * @param {jQuery} widgetContainer Widget container element. 292 * 293 * @return {void} 294 */ 295 component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) { 296 var widgetForm, idBase, widgetControl, widgetId, animatedCheckDelay = 50, renderWhenAnimationDone, fieldContainer, syncContainer; 297 widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen. 298 299 idBase = widgetForm.find( '> .id_base' ).val(); 300 if ( -1 === component.idBases.indexOf( idBase ) ) { 301 return; 302 } 303 304 // Prevent initializing already-added widgets. 305 widgetId = widgetForm.find( '.widget-id' ).val(); 306 if ( component.widgetControls[ widgetId ] ) { 307 return; 308 } 309 310 /* 311 * Create a container element for the widget control fields. 312 * This is inserted into the DOM immediately before the .widget-content 313 * element because the contents of this element are essentially "managed" 314 * by PHP, where each widget update cause the entire element to be emptied 315 * and replaced with the rendered output of WP_Widget::form() which is 316 * sent back in Ajax request made to save/update the widget instance. 317 * To prevent a "flash of replaced DOM elements and re-initialized JS 318 * components", the JS template is rendered outside of the normal form 319 * container. 320 */ 321 fieldContainer = $( '<div></div>' ); 322 syncContainer = widgetContainer.find( '.widget-content:first' ); 323 syncContainer.before( fieldContainer ); 324 325 widgetControl = new component.CustomHtmlWidgetControl({ 326 el: fieldContainer, 327 syncContainer: syncContainer 328 }); 329 330 component.widgetControls[ widgetId ] = widgetControl; 331 332 /* 333 * Render the widget once the widget parent's container finishes animating, 334 * as the widget-added event fires with a slideDown of the container. 335 * This ensures that the textarea is visible and the editor can be initialized. 336 */ 337 renderWhenAnimationDone = function() { 338 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://core.trac.wordpress.org/changeset/41260 339 setTimeout( renderWhenAnimationDone, animatedCheckDelay ); 340 } else { 341 widgetControl.initializeEditor(); 342 } 343 }; 344 renderWhenAnimationDone(); 345 }; 346 347 /** 348 * Setup widget in accessibility mode. 349 * 350 * @alias wp.customHtmlWidgets.setupAccessibleMode 351 * 352 * @return {void} 353 */ 354 component.setupAccessibleMode = function setupAccessibleMode() { 355 var widgetForm, idBase, widgetControl, fieldContainer, syncContainer; 356 widgetForm = $( '.editwidget > form' ); 357 if ( 0 === widgetForm.length ) { 358 return; 359 } 360 361 idBase = widgetForm.find( '.id_base' ).val(); 362 if ( -1 === component.idBases.indexOf( idBase ) ) { 363 return; 364 } 365 366 fieldContainer = $( '<div></div>' ); 367 syncContainer = widgetForm.find( '> .widget-inside' ); 368 syncContainer.before( fieldContainer ); 369 370 widgetControl = new component.CustomHtmlWidgetControl({ 371 el: fieldContainer, 372 syncContainer: syncContainer 373 }); 374 375 widgetControl.initializeEditor(); 376 }; 377 378 /** 379 * Sync widget instance data sanitized from server back onto widget model. 380 * 381 * This gets called via the 'widget-updated' event when saving a widget from 382 * the widgets admin screen and also via the 'widget-synced' event when making 383 * a change to a widget in the customizer. 384 * 385 * @alias wp.customHtmlWidgets.handleWidgetUpdated 386 * 387 * @param {jQuery.Event} event Event. 388 * @param {jQuery} widgetContainer Widget container element. 389 * @return {void} 390 */ 391 component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) { 392 var widgetForm, widgetId, widgetControl, idBase; 393 widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); 394 395 idBase = widgetForm.find( '> .id_base' ).val(); 396 if ( -1 === component.idBases.indexOf( idBase ) ) { 397 return; 398 } 399 400 widgetId = widgetForm.find( '> .widget-id' ).val(); 401 widgetControl = component.widgetControls[ widgetId ]; 402 if ( ! widgetControl ) { 403 return; 404 } 405 406 widgetControl.updateFields(); 407 }; 408 409 /** 410 * Initialize functionality. 411 * 412 * This function exists to prevent the JS file from having to boot itself. 413 * When WordPress enqueues this script, it should have an inline script 414 * attached which calls wp.textWidgets.init(). 415 * 416 * @alias wp.customHtmlWidgets.init 417 * 418 * @param {Object} settings Options for code editor, exported from PHP. 419 * 420 * @return {void} 421 */ 422 component.init = function init( settings ) { 423 var $document = $( document ); 424 _.extend( component.codeEditorSettings, settings ); 425 426 $document.on( 'widget-added', component.handleWidgetAdded ); 427 $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated ); 428 429 /* 430 * Manually trigger widget-added events for media widgets on the admin 431 * screen once they are expanded. The widget-added event is not triggered 432 * for each pre-existing widget on the widgets admin screen like it is 433 * on the customizer. Likewise, the customizer only triggers widget-added 434 * when the widget is expanded to just-in-time construct the widget form 435 * when it is actually going to be displayed. So the following implements 436 * the same for the widgets admin screen, to invoke the widget-added 437 * handler when a pre-existing media widget is expanded. 438 */ 439 $( function initializeExistingWidgetContainers() { 440 var widgetContainers; 441 if ( 'widgets' !== window.pagenow ) { 442 return; 443 } 444 widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' ); 445 widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() { 446 var widgetContainer = $( this ); 447 component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer ); 448 }); 449 450 // Accessibility mode. 451 if ( document.readyState === 'complete' ) { 452 // Page is fully loaded. 453 component.setupAccessibleMode(); 454 } else { 455 // Page is still loading. 456 $( window ).on( 'load', function() { 457 component.setupAccessibleMode(); 458 }); 459 } 460 }); 461 }; 462 463 return component; 464 })( jQuery );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Sep 12 08:20:32 2026 | Cross-referenced by PHPXref |