run:R W Run
15.41 KB
2026-03-11 16:18:51
R W Run
5.45 KB
2026-03-11 16:18:51
R W Run
4.18 KB
2026-03-11 16:18:51
R W Run
1.41 KB
2026-03-11 16:18:51
R W Run
10.11 KB
2026-03-11 16:18:51
R W Run
3.68 KB
2026-03-11 16:18:51
R W Run
5.34 KB
2026-03-11 16:18:51
R W Run
1.98 KB
2026-03-11 16:18:51
R W Run
6.86 KB
2026-03-11 16:18:51
R W Run
2.64 KB
2026-03-11 16:18:51
R W Run
41.86 KB
2026-03-11 16:18:51
R W Run
13.91 KB
2026-03-11 16:18:51
R W Run
17.63 KB
2026-03-11 16:18:51
R W Run
5.72 KB
2026-03-11 16:18:51
R W Run
error_log
📄custom-html-widgets.js
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 */
13wp.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" role="alert"></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 );
463