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