run:R W Run
DIR
2026-03-11 16:18:51
R W Run
2.86 KB
2026-03-11 16:18:51
R W Run
758 By
2026-03-11 16:18:51
R W Run
6.24 KB
2026-03-11 16:18:51
R W Run
2.95 KB
2026-03-11 16:18:51
R W Run
5.66 KB
2026-03-11 16:18:51
R W Run
2.04 KB
2026-03-11 16:18:51
R W Run
11.32 KB
2026-03-11 16:18:51
R W Run
3.01 KB
2026-03-11 16:18:51
R W Run
9.54 KB
2026-03-11 16:18:51
R W Run
3.4 KB
2026-03-11 16:18:51
R W Run
2.85 KB
2026-03-11 16:18:51
R W Run
1.28 KB
2026-03-11 16:18:51
R W Run
61.15 KB
2026-03-11 16:18:51
R W Run
23.12 KB
2026-03-11 16:18:51
R W Run
3.35 KB
2026-03-11 16:18:51
R W Run
1.18 KB
2026-03-11 16:18:51
R W Run
1.98 KB
2026-03-11 16:18:51
R W Run
288.41 KB
2026-03-11 16:18:51
R W Run
109.69 KB
2026-03-11 16:18:51
R W Run
111.46 KB
2026-03-11 16:18:51
R W Run
47.14 KB
2026-03-11 16:18:51
R W Run
70.05 KB
2026-03-11 16:18:51
R W Run
27.41 KB
2026-03-11 16:18:51
R W Run
27.02 KB
2026-03-11 16:18:51
R W Run
8.65 KB
2026-03-11 16:18:51
R W Run
37.12 KB
2026-03-11 16:18:51
R W Run
15.13 KB
2026-03-11 16:18:51
R W Run
41.61 KB
2026-03-11 16:18:51
R W Run
13.14 KB
2026-03-11 16:18:51
R W Run
44 KB
2026-03-11 16:18:51
R W Run
12.78 KB
2026-03-11 16:18:51
R W Run
7.67 KB
2026-03-11 16:18:51
R W Run
5.41 KB
2026-03-11 16:18:51
R W Run
3.65 KB
2026-03-11 16:18:51
R W Run
39.98 KB
2026-03-11 16:18:51
R W Run
15.15 KB
2026-03-11 16:18:51
R W Run
20.17 KB
2026-03-11 16:18:51
R W Run
9.41 KB
2026-03-11 16:18:51
R W Run
7.61 KB
2026-03-11 16:18:51
R W Run
2.93 KB
2026-03-11 16:18:51
R W Run
23.09 KB
2026-03-11 16:18:51
R W Run
890 By
2026-03-11 16:18:51
R W Run
423 By
2026-03-11 16:18:51
R W Run
3.89 KB
2026-03-11 16:18:51
R W Run
1.7 KB
2026-03-11 16:18:51
R W Run
1.27 KB
2026-03-11 16:18:51
R W Run
611 By
2026-03-11 16:18:51
R W Run
3.38 KB
2026-03-11 16:18:51
R W Run
1.13 KB
2026-03-11 16:18:51
R W Run
6.61 KB
2026-03-11 16:18:51
R W Run
2.38 KB
2026-03-11 16:18:51
R W Run
61.15 KB
2026-03-11 16:18:51
R W Run
30.06 KB
2026-03-11 16:18:51
R W Run
4.14 KB
2026-03-11 16:18:51
R W Run
1.1 KB
2026-03-11 16:18:51
R W Run
1.31 KB
2026-03-11 16:18:51
R W Run
847 By
2026-03-11 16:18:51
R W Run
6.92 KB
2026-03-11 16:18:51
R W Run
2.35 KB
2026-03-11 16:18:51
R W Run
38.68 KB
2026-03-11 16:18:51
R W Run
18.4 KB
2026-03-11 16:18:51
R W Run
18.49 KB
2026-03-11 16:18:51
R W Run
6.6 KB
2026-03-11 16:18:51
R W Run
10.67 KB
2026-03-11 16:18:51
R W Run
5.03 KB
2026-03-11 16:18:51
R W Run
33.92 KB
2026-03-11 16:18:51
R W Run
17.97 KB
2026-03-11 16:18:51
R W Run
876 By
2026-03-11 16:18:51
R W Run
620 By
2026-03-11 16:18:51
R W Run
13.15 KB
2026-03-11 16:18:51
R W Run
6.13 KB
2026-03-11 16:18:51
R W Run
6.1 KB
2026-03-11 16:18:51
R W Run
2.2 KB
2026-03-11 16:18:51
R W Run
3.2 KB
2026-03-11 16:18:51
R W Run
1.53 KB
2026-03-11 16:18:51
R W Run
10.88 KB
2026-03-11 16:18:51
R W Run
3 KB
2026-03-11 16:18:51
R W Run
5.64 KB
2026-03-11 16:18:51
R W Run
2.22 KB
2026-03-11 16:18:51
R W Run
5.96 KB
2026-03-11 16:18:51
R W Run
2.41 KB
2026-03-11 16:18:51
R W Run
24.77 KB
2026-03-11 16:18:51
R W Run
11.43 KB
2026-03-11 16:18:51
R W Run
54.94 KB
2026-03-11 16:18:51
R W Run
26.51 KB
2026-03-11 16:18:51
R W Run
109.37 KB
2026-03-11 16:18:51
R W Run
47.31 KB
2026-03-11 16:18:51
R W Run
17.91 KB
2026-03-11 16:18:51
R W Run
7.81 KB
2026-03-11 16:18:51
R W Run
2.25 KB
2026-03-11 16:18:51
R W Run
676 By
2026-03-11 16:18:51
R W Run
22.56 KB
2026-03-11 16:18:51
R W Run
12.31 KB
2026-03-11 16:18:51
R W Run
7.52 KB
2026-03-11 16:18:51
R W Run
1.49 KB
2026-03-11 16:18:51
R W Run
740 By
2026-03-11 16:18:51
R W Run
458 By
2026-03-11 16:18:51
R W Run
error_log
📄customize-widgets.js
1/**
2 * @output wp-admin/js/customize-widgets.js
3 */
4
5/* global _wpCustomizeWidgetsSettings */
6(function( wp, $ ){
7
8 if ( ! wp || ! wp.customize ) { return; }
9
10 // Set up our namespace...
11 var api = wp.customize,
12 l10n;
13
14 /**
15 * @namespace wp.customize.Widgets
16 */
17 api.Widgets = api.Widgets || {};
18 api.Widgets.savedWidgetIds = {};
19
20 // Link settings.
21 api.Widgets.data = _wpCustomizeWidgetsSettings || {};
22 l10n = api.Widgets.data.l10n;
23
24 /**
25 * wp.customize.Widgets.WidgetModel
26 *
27 * A single widget model.
28 *
29 * @class wp.customize.Widgets.WidgetModel
30 * @augments Backbone.Model
31 */
32 api.Widgets.WidgetModel = Backbone.Model.extend(/** @lends wp.customize.Widgets.WidgetModel.prototype */{
33 id: null,
34 temp_id: null,
35 classname: null,
36 control_tpl: null,
37 description: null,
38 is_disabled: null,
39 is_multi: null,
40 multi_number: null,
41 name: null,
42 id_base: null,
43 transport: null,
44 params: [],
45 width: null,
46 height: null,
47 search_matched: true
48 });
49
50 /**
51 * wp.customize.Widgets.WidgetCollection
52 *
53 * Collection for widget models.
54 *
55 * @class wp.customize.Widgets.WidgetCollection
56 * @augments Backbone.Collection
57 */
58 api.Widgets.WidgetCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.WidgetCollection.prototype */{
59 model: api.Widgets.WidgetModel,
60
61 // Controls searching on the current widget collection
62 // and triggers an update event.
63 doSearch: function( value ) {
64
65 // Don't do anything if we've already done this search.
66 // Useful because the search handler fires multiple times per keystroke.
67 if ( this.terms === value ) {
68 return;
69 }
70
71 // Updates terms with the value passed.
72 this.terms = value;
73
74 // If we have terms, run a search...
75 if ( this.terms.length > 0 ) {
76 this.search( this.terms );
77 }
78
79 // If search is blank, set all the widgets as they matched the search to reset the views.
80 if ( this.terms === '' ) {
81 this.each( function ( widget ) {
82 widget.set( 'search_matched', true );
83 } );
84 }
85 },
86
87 // Performs a search within the collection.
88 // @uses RegExp
89 search: function( term ) {
90 var match, haystack;
91
92 // Escape the term string for RegExp meta characters.
93 term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
94
95 // Consider spaces as word delimiters and match the whole string
96 // so matching terms can be combined.
97 term = term.replace( / /g, ')(?=.*' );
98 match = new RegExp( '^(?=.*' + term + ').+', 'i' );
99
100 this.each( function ( data ) {
101 haystack = [ data.get( 'name' ), data.get( 'description' ) ].join( ' ' );
102 data.set( 'search_matched', match.test( haystack ) );
103 } );
104 }
105 });
106 api.Widgets.availableWidgets = new api.Widgets.WidgetCollection( api.Widgets.data.availableWidgets );
107
108 /**
109 * wp.customize.Widgets.SidebarModel
110 *
111 * A single sidebar model.
112 *
113 * @class wp.customize.Widgets.SidebarModel
114 * @augments Backbone.Model
115 */
116 api.Widgets.SidebarModel = Backbone.Model.extend(/** @lends wp.customize.Widgets.SidebarModel.prototype */{
117 after_title: null,
118 after_widget: null,
119 before_title: null,
120 before_widget: null,
121 'class': null,
122 description: null,
123 id: null,
124 name: null,
125 is_rendered: false
126 });
127
128 /**
129 * wp.customize.Widgets.SidebarCollection
130 *
131 * Collection for sidebar models.
132 *
133 * @class wp.customize.Widgets.SidebarCollection
134 * @augments Backbone.Collection
135 */
136 api.Widgets.SidebarCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.SidebarCollection.prototype */{
137 model: api.Widgets.SidebarModel
138 });
139 api.Widgets.registeredSidebars = new api.Widgets.SidebarCollection( api.Widgets.data.registeredSidebars );
140
141 api.Widgets.AvailableWidgetsPanelView = wp.Backbone.View.extend(/** @lends wp.customize.Widgets.AvailableWidgetsPanelView.prototype */{
142
143 el: '#available-widgets',
144
145 events: {
146 'input #widgets-search': 'search',
147 'focus .widget-tpl' : 'focus',
148 'click .widget-tpl' : '_submit',
149 'keypress .widget-tpl' : '_submit',
150 'keydown' : 'keyboardAccessible'
151 },
152
153 // Cache current selected widget.
154 selected: null,
155
156 // Cache sidebar control which has opened panel.
157 currentSidebarControl: null,
158 $search: null,
159 $clearResults: null,
160 searchMatchesCount: null,
161
162 /**
163 * View class for the available widgets panel.
164 *
165 * @constructs wp.customize.Widgets.AvailableWidgetsPanelView
166 * @augments wp.Backbone.View
167 */
168 initialize: function() {
169 var self = this;
170
171 this.$search = $( '#widgets-search' );
172
173 this.$clearResults = this.$el.find( '.clear-results' );
174
175 _.bindAll( this, 'close' );
176
177 this.listenTo( this.collection, 'change', this.updateList );
178
179 this.updateList();
180
181 // Set the initial search count to the number of available widgets.
182 this.searchMatchesCount = this.collection.length;
183
184 /*
185 * If the available widgets panel is open and the customize controls
186 * are interacted with (i.e. available widgets panel is blurred) then
187 * close the available widgets panel. Also close on back button click.
188 */
189 $( '#customize-controls, #available-widgets .customize-section-title' ).on( 'click keydown', function( e ) {
190 var isAddNewBtn = $( e.target ).is( '.add-new-widget, .add-new-widget *' );
191 if ( $( 'body' ).hasClass( 'adding-widget' ) && ! isAddNewBtn ) {
192 self.close();
193 }
194 } );
195
196 // Clear the search results and trigger an `input` event to fire a new search.
197 this.$clearResults.on( 'click', function() {
198 self.$search.val( '' ).trigger( 'focus' ).trigger( 'input' );
199 } );
200
201 // Close the panel if the URL in the preview changes.
202 api.previewer.bind( 'url', this.close );
203 },
204
205 /**
206 * Performs a search and handles selected widget.
207 */
208 search: _.debounce( function( event ) {
209 var firstVisible;
210
211 this.collection.doSearch( event.target.value );
212 // Update the search matches count.
213 this.updateSearchMatchesCount();
214 // Announce how many search results.
215 this.announceSearchMatches();
216
217 // Remove a widget from being selected if it is no longer visible.
218 if ( this.selected && ! this.selected.is( ':visible' ) ) {
219 this.selected.removeClass( 'selected' );
220 this.selected = null;
221 }
222
223 // If a widget was selected but the filter value has been cleared out, clear selection.
224 if ( this.selected && ! event.target.value ) {
225 this.selected.removeClass( 'selected' );
226 this.selected = null;
227 }
228
229 // If a filter has been entered and a widget hasn't been selected, select the first one shown.
230 if ( ! this.selected && event.target.value ) {
231 firstVisible = this.$el.find( '> .widget-tpl:visible:first' );
232 if ( firstVisible.length ) {
233 this.select( firstVisible );
234 }
235 }
236
237 // Toggle the clear search results button.
238 if ( '' !== event.target.value ) {
239 this.$clearResults.addClass( 'is-visible' );
240 } else if ( '' === event.target.value ) {
241 this.$clearResults.removeClass( 'is-visible' );
242 }
243
244 // Set a CSS class on the search container when there are no search results.
245 if ( ! this.searchMatchesCount ) {
246 this.$el.addClass( 'no-widgets-found' );
247 } else {
248 this.$el.removeClass( 'no-widgets-found' );
249 }
250 }, 500 ),
251
252 /**
253 * Updates the count of the available widgets that have the `search_matched` attribute.
254 */
255 updateSearchMatchesCount: function() {
256 this.searchMatchesCount = this.collection.where({ search_matched: true }).length;
257 },
258
259 /**
260 * Sends a message to the aria-live region to announce how many search results.
261 */
262 announceSearchMatches: function() {
263 var message = l10n.widgetsFound.replace( '%d', this.searchMatchesCount ) ;
264
265 if ( ! this.searchMatchesCount ) {
266 message = l10n.noWidgetsFound;
267 }
268
269 wp.a11y.speak( message );
270 },
271
272 /**
273 * Changes visibility of available widgets.
274 */
275 updateList: function() {
276 this.collection.each( function( widget ) {
277 var widgetTpl = $( '#widget-tpl-' + widget.id );
278 widgetTpl.toggle( widget.get( 'search_matched' ) && ! widget.get( 'is_disabled' ) );
279 if ( widget.get( 'is_disabled' ) && widgetTpl.is( this.selected ) ) {
280 this.selected = null;
281 }
282 } );
283 },
284
285 /**
286 * Highlights a widget.
287 */
288 select: function( widgetTpl ) {
289 this.selected = $( widgetTpl );
290 this.selected.siblings( '.widget-tpl' ).removeClass( 'selected' );
291 this.selected.addClass( 'selected' );
292 },
293
294 /**
295 * Highlights a widget on focus.
296 */
297 focus: function( event ) {
298 this.select( $( event.currentTarget ) );
299 },
300
301 /**
302 * Handles submit for keypress and click on widget.
303 */
304 _submit: function( event ) {
305 // Only proceed with keypress if it is Enter or Spacebar.
306 if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
307 return;
308 }
309
310 this.submit( $( event.currentTarget ) );
311 },
312
313 /**
314 * Adds a selected widget to the sidebar.
315 */
316 submit: function( widgetTpl ) {
317 var widgetId, widget, widgetFormControl;
318
319 if ( ! widgetTpl ) {
320 widgetTpl = this.selected;
321 }
322
323 if ( ! widgetTpl || ! this.currentSidebarControl ) {
324 return;
325 }
326
327 this.select( widgetTpl );
328
329 widgetId = $( this.selected ).data( 'widget-id' );
330 widget = this.collection.findWhere( { id: widgetId } );
331 if ( ! widget ) {
332 return;
333 }
334
335 widgetFormControl = this.currentSidebarControl.addWidget( widget.get( 'id_base' ) );
336 if ( widgetFormControl ) {
337 widgetFormControl.focus();
338 }
339
340 this.close();
341 },
342
343 /**
344 * Opens the panel.
345 */
346 open: function( sidebarControl ) {
347 this.currentSidebarControl = sidebarControl;
348
349 // Wide widget controls appear over the preview, and so they need to be collapsed when the panel opens.
350 _( this.currentSidebarControl.getWidgetFormControls() ).each( function( control ) {
351 if ( control.params.is_wide ) {
352 control.collapseForm();
353 }
354 } );
355
356 if ( api.section.has( 'publish_settings' ) ) {
357 api.section( 'publish_settings' ).collapse();
358 }
359
360 $( 'body' ).addClass( 'adding-widget' );
361
362 this.$el.find( '.selected' ).removeClass( 'selected' );
363
364 // Reset search.
365 this.collection.doSearch( '' );
366
367 if ( ! api.settings.browser.mobile ) {
368 this.$search.trigger( 'focus' );
369 }
370 },
371
372 /**
373 * Closes the panel.
374 */
375 close: function( options ) {
376 options = options || {};
377
378 if ( options.returnFocus && this.currentSidebarControl ) {
379 this.currentSidebarControl.container.find( '.add-new-widget' ).focus();
380 }
381
382 this.currentSidebarControl = null;
383 this.selected = null;
384
385 $( 'body' ).removeClass( 'adding-widget' );
386
387 this.$search.val( '' ).trigger( 'input' );
388 },
389
390 /**
391 * Adds keyboard accessibility to the panel.
392 */
393 keyboardAccessible: function( event ) {
394 var isEnter = ( event.which === 13 ),
395 isEsc = ( event.which === 27 ),
396 isDown = ( event.which === 40 ),
397 isUp = ( event.which === 38 ),
398 isTab = ( event.which === 9 ),
399 isShift = ( event.shiftKey ),
400 selected = null,
401 firstVisible = this.$el.find( '> .widget-tpl:visible:first' ),
402 lastVisible = this.$el.find( '> .widget-tpl:visible:last' ),
403 isSearchFocused = $( event.target ).is( this.$search ),
404 isLastWidgetFocused = $( event.target ).is( '.widget-tpl:visible:last' );
405
406 if ( isDown || isUp ) {
407 if ( isDown ) {
408 if ( isSearchFocused ) {
409 selected = firstVisible;
410 } else if ( this.selected && this.selected.nextAll( '.widget-tpl:visible' ).length !== 0 ) {
411 selected = this.selected.nextAll( '.widget-tpl:visible:first' );
412 }
413 } else if ( isUp ) {
414 if ( isSearchFocused ) {
415 selected = lastVisible;
416 } else if ( this.selected && this.selected.prevAll( '.widget-tpl:visible' ).length !== 0 ) {
417 selected = this.selected.prevAll( '.widget-tpl:visible:first' );
418 }
419 }
420
421 this.select( selected );
422
423 if ( selected ) {
424 selected.trigger( 'focus' );
425 } else {
426 this.$search.trigger( 'focus' );
427 }
428
429 return;
430 }
431
432 // If enter pressed but nothing entered, don't do anything.
433 if ( isEnter && ! this.$search.val() ) {
434 return;
435 }
436
437 if ( isEnter ) {
438 this.submit();
439 } else if ( isEsc ) {
440 this.close( { returnFocus: true } );
441 }
442
443 if ( this.currentSidebarControl && isTab && ( isShift && isSearchFocused || ! isShift && isLastWidgetFocused ) ) {
444 this.currentSidebarControl.container.find( '.add-new-widget' ).focus();
445 event.preventDefault();
446 }
447 }
448 });
449
450 /**
451 * Handlers for the widget-synced event, organized by widget ID base.
452 * Other widgets may provide their own update handlers by adding
453 * listeners for the widget-synced event.
454 *
455 * @alias wp.customize.Widgets.formSyncHandlers
456 */
457 api.Widgets.formSyncHandlers = {
458
459 /**
460 * @param {jQuery.Event} e
461 * @param {jQuery} widget
462 * @param {string} newForm
463 */
464 rss: function( e, widget, newForm ) {
465 var oldWidgetError = widget.find( '.widget-error:first' ),
466 newWidgetError = $( '<div>' + newForm + '</div>' ).find( '.widget-error:first' );
467
468 if ( oldWidgetError.length && newWidgetError.length ) {
469 oldWidgetError.replaceWith( newWidgetError );
470 } else if ( oldWidgetError.length ) {
471 oldWidgetError.remove();
472 } else if ( newWidgetError.length ) {
473 widget.find( '.widget-content:first' ).prepend( newWidgetError );
474 }
475 }
476 };
477
478 api.Widgets.WidgetControl = api.Control.extend(/** @lends wp.customize.Widgets.WidgetControl.prototype */{
479 defaultExpandedArguments: {
480 duration: 'fast',
481 completeCallback: $.noop
482 },
483
484 /**
485 * wp.customize.Widgets.WidgetControl
486 *
487 * Customizer control for widgets.
488 * Note that 'widget_form' must match the WP_Widget_Form_Customize_Control::$type
489 *
490 * @since 4.1.0
491 *
492 * @constructs wp.customize.Widgets.WidgetControl
493 * @augments wp.customize.Control
494 */
495 initialize: function( id, options ) {
496 var control = this;
497
498 control.widgetControlEmbedded = false;
499 control.widgetContentEmbedded = false;
500 control.expanded = new api.Value( false );
501 control.expandedArgumentsQueue = [];
502 control.expanded.bind( function( expanded ) {
503 var args = control.expandedArgumentsQueue.shift();
504 args = $.extend( {}, control.defaultExpandedArguments, args );
505 control.onChangeExpanded( expanded, args );
506 });
507 control.altNotice = true;
508
509 api.Control.prototype.initialize.call( control, id, options );
510 },
511
512 /**
513 * Set up the control.
514 *
515 * @since 3.9.0
516 */
517 ready: function() {
518 var control = this;
519
520 /*
521 * Embed a placeholder once the section is expanded. The full widget
522 * form content will be embedded once the control itself is expanded,
523 * and at this point the widget-added event will be triggered.
524 */
525 if ( ! control.section() ) {
526 control.embedWidgetControl();
527 } else {
528 api.section( control.section(), function( section ) {
529 var onExpanded = function( isExpanded ) {
530 if ( isExpanded ) {
531 control.embedWidgetControl();
532 section.expanded.unbind( onExpanded );
533 }
534 };
535 if ( section.expanded() ) {
536 onExpanded( true );
537 } else {
538 section.expanded.bind( onExpanded );
539 }
540 } );
541 }
542 },
543
544 /**
545 * Embed the .widget element inside the li container.
546 *
547 * @since 4.4.0
548 */
549 embedWidgetControl: function() {
550 var control = this, widgetControl;
551
552 if ( control.widgetControlEmbedded ) {
553 return;
554 }
555 control.widgetControlEmbedded = true;
556
557 widgetControl = $( control.params.widget_control );
558 control.container.append( widgetControl );
559
560 control._setupModel();
561 control._setupWideWidget();
562 control._setupControlToggle();
563
564 control._setupWidgetTitle();
565 control._setupReorderUI();
566 control._setupHighlightEffects();
567 control._setupUpdateUI();
568 control._setupRemoveUI();
569 },
570
571 /**
572 * Embed the actual widget form inside of .widget-content and finally trigger the widget-added event.
573 *
574 * @since 4.4.0
575 */
576 embedWidgetContent: function() {
577 var control = this, widgetContent;
578
579 control.embedWidgetControl();
580 if ( control.widgetContentEmbedded ) {
581 return;
582 }
583 control.widgetContentEmbedded = true;
584
585 // Update the notification container element now that the widget content has been embedded.
586 control.notifications.container = control.getNotificationsContainerElement();
587 control.notifications.render();
588
589 widgetContent = $( control.params.widget_content );
590 control.container.find( '.widget-content:first' ).append( widgetContent );
591
592 /*
593 * Trigger widget-added event so that plugins can attach any event
594 * listeners and dynamic UI elements.
595 */
596 $( document ).trigger( 'widget-added', [ control.container.find( '.widget:first' ) ] );
597
598 },
599
600 /**
601 * Handle changes to the setting
602 */
603 _setupModel: function() {
604 var self = this, rememberSavedWidgetId;
605
606 // Remember saved widgets so we know which to trash (move to inactive widgets sidebar).
607 rememberSavedWidgetId = function() {
608 api.Widgets.savedWidgetIds[self.params.widget_id] = true;
609 };
610 api.bind( 'ready', rememberSavedWidgetId );
611 api.bind( 'saved', rememberSavedWidgetId );
612
613 this._updateCount = 0;
614 this.isWidgetUpdating = false;
615 this.liveUpdateMode = true;
616
617 // Update widget whenever model changes.
618 this.setting.bind( function( to, from ) {
619 if ( ! _( from ).isEqual( to ) && ! self.isWidgetUpdating ) {
620 self.updateWidget( { instance: to } );
621 }
622 } );
623 },
624
625 /**
626 * Add special behaviors for wide widget controls
627 */
628 _setupWideWidget: function() {
629 var self = this, $widgetInside, $widgetForm, $customizeSidebar,
630 $themeControlsContainer, positionWidget;
631
632 if ( ! this.params.is_wide || $( window ).width() <= 640 /* max-width breakpoint in customize-controls.css */ ) {
633 return;
634 }
635
636 $widgetInside = this.container.find( '.widget-inside' );
637 $widgetForm = $widgetInside.find( '> .form' );
638 $customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
639 this.container.addClass( 'wide-widget-control' );
640
641 this.container.find( '.form:first' ).css( {
642 'max-width': this.params.width,
643 'min-height': this.params.height
644 } );
645
646 /**
647 * Keep the widget-inside positioned so the top of fixed-positioned
648 * element is at the same top position as the widget-top. When the
649 * widget-top is scrolled out of view, keep the widget-top in view;
650 * likewise, don't allow the widget to drop off the bottom of the window.
651 * If a widget is too tall to fit in the window, don't let the height
652 * exceed the window height so that the contents of the widget control
653 * will become scrollable (overflow:auto).
654 */
655 positionWidget = function() {
656 var offsetTop = self.container.offset().top,
657 windowHeight = $( window ).height(),
658 formHeight = $widgetForm.outerHeight(),
659 top;
660 $widgetInside.css( 'max-height', windowHeight );
661 top = Math.max(
662 0, // Prevent top from going off screen.
663 Math.min(
664 Math.max( offsetTop, 0 ), // Distance widget in panel is from top of screen.
665 windowHeight - formHeight // Flush up against bottom of screen.
666 )
667 );
668 $widgetInside.css( 'top', top );
669 };
670
671 $themeControlsContainer = $( '#customize-theme-controls' );
672 this.container.on( 'expand', function() {
673 positionWidget();
674 $customizeSidebar.on( 'scroll', positionWidget );
675 $( window ).on( 'resize', positionWidget );
676 $themeControlsContainer.on( 'expanded collapsed', positionWidget );
677 } );
678 this.container.on( 'collapsed', function() {
679 $customizeSidebar.off( 'scroll', positionWidget );
680 $( window ).off( 'resize', positionWidget );
681 $themeControlsContainer.off( 'expanded collapsed', positionWidget );
682 } );
683
684 // Reposition whenever a sidebar's widgets are changed.
685 api.each( function( setting ) {
686 if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) {
687 setting.bind( function() {
688 if ( self.container.hasClass( 'expanded' ) ) {
689 positionWidget();
690 }
691 } );
692 }
693 } );
694 },
695
696 /**
697 * Show/hide the control when clicking on the form title, when clicking
698 * the close button
699 */
700 _setupControlToggle: function() {
701 var self = this, $closeBtn;
702
703 this.container.find( '.widget-top' ).on( 'click', function( e ) {
704 e.preventDefault();
705 var sidebarWidgetsControl = self.getSidebarWidgetsControl();
706 if ( sidebarWidgetsControl.isReordering ) {
707 return;
708 }
709 self.expanded( ! self.expanded() );
710 } );
711
712 $closeBtn = this.container.find( '.widget-control-close' );
713 $closeBtn.on( 'click', function() {
714 self.collapse();
715 self.container.find( '.widget-top .widget-action:first' ).focus(); // Keyboard accessibility.
716 } );
717 },
718
719 /**
720 * Update the title of the form if a title field is entered
721 */
722 _setupWidgetTitle: function() {
723 var self = this, updateTitle;
724
725 updateTitle = function() {
726 var title = self.setting().title,
727 inWidgetTitle = self.container.find( '.in-widget-title' );
728
729 if ( title ) {
730 inWidgetTitle.text( ': ' + title );
731 } else {
732 inWidgetTitle.text( '' );
733 }
734 };
735 this.setting.bind( updateTitle );
736 updateTitle();
737 },
738
739 /**
740 * Set up the widget-reorder-nav
741 */
742 _setupReorderUI: function() {
743 var self = this, selectSidebarItem, $moveWidgetArea,
744 $reorderNav, updateAvailableSidebars, template;
745
746 /**
747 * select the provided sidebar list item in the move widget area
748 *
749 * @param {jQuery} li
750 */
751 selectSidebarItem = function( li ) {
752 li.siblings( '.selected' ).removeClass( 'selected' );
753 li.addClass( 'selected' );
754 var isSelfSidebar = ( li.data( 'id' ) === self.params.sidebar_id );
755 self.container.find( '.move-widget-btn' ).prop( 'disabled', isSelfSidebar );
756 };
757
758 /**
759 * Add the widget reordering elements to the widget control
760 */
761 this.container.find( '.widget-title-action' ).after( $( api.Widgets.data.tpl.widgetReorderNav ) );
762
763
764 template = _.template( api.Widgets.data.tpl.moveWidgetArea );
765 $moveWidgetArea = $( template( {
766 sidebars: _( api.Widgets.registeredSidebars.toArray() ).pluck( 'attributes' )
767 } )
768 );
769 this.container.find( '.widget-top' ).after( $moveWidgetArea );
770
771 /**
772 * Update available sidebars when their rendered state changes
773 */
774 updateAvailableSidebars = function() {
775 var $sidebarItems = $moveWidgetArea.find( 'li' ), selfSidebarItem,
776 renderedSidebarCount = 0;
777
778 selfSidebarItem = $sidebarItems.filter( function(){
779 return $( this ).data( 'id' ) === self.params.sidebar_id;
780 } );
781
782 $sidebarItems.each( function() {
783 var li = $( this ),
784 sidebarId, sidebar, sidebarIsRendered;
785
786 sidebarId = li.data( 'id' );
787 sidebar = api.Widgets.registeredSidebars.get( sidebarId );
788 sidebarIsRendered = sidebar.get( 'is_rendered' );
789
790 li.toggle( sidebarIsRendered );
791
792 if ( sidebarIsRendered ) {
793 renderedSidebarCount += 1;
794 }
795
796 if ( li.hasClass( 'selected' ) && ! sidebarIsRendered ) {
797 selectSidebarItem( selfSidebarItem );
798 }
799 } );
800
801 if ( renderedSidebarCount > 1 ) {
802 self.container.find( '.move-widget' ).show();
803 } else {
804 self.container.find( '.move-widget' ).hide();
805 }
806 };
807
808 updateAvailableSidebars();
809 api.Widgets.registeredSidebars.on( 'change:is_rendered', updateAvailableSidebars );
810
811 /**
812 * Handle clicks for up/down/move on the reorder nav
813 */
814 $reorderNav = this.container.find( '.widget-reorder-nav' );
815 $reorderNav.find( '.move-widget, .move-widget-down, .move-widget-up' ).each( function() {
816 $( this ).prepend( self.container.find( '.widget-title' ).text() + ': ' );
817 } ).on( 'click keypress', function( event ) {
818 if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
819 return;
820 }
821 $( this ).trigger( 'focus' );
822
823 if ( $( this ).is( '.move-widget' ) ) {
824 self.toggleWidgetMoveArea();
825 } else {
826 var isMoveDown = $( this ).is( '.move-widget-down' ),
827 isMoveUp = $( this ).is( '.move-widget-up' ),
828 i = self.getWidgetSidebarPosition();
829
830 if ( ( isMoveUp && i === 0 ) || ( isMoveDown && i === self.getSidebarWidgetsControl().setting().length - 1 ) ) {
831 return;
832 }
833
834 if ( isMoveUp ) {
835 self.moveUp();
836 wp.a11y.speak( l10n.widgetMovedUp );
837 } else {
838 self.moveDown();
839 wp.a11y.speak( l10n.widgetMovedDown );
840 }
841
842 $( this ).trigger( 'focus' ); // Re-focus after the container was moved.
843 }
844 } );
845
846 /**
847 * Handle selecting a sidebar to move to
848 */
849 this.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function( event ) {
850 if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
851 return;
852 }
853 event.preventDefault();
854 selectSidebarItem( $( this ) );
855 } );
856
857 /**
858 * Move widget to another sidebar
859 */
860 this.container.find( '.move-widget-btn' ).click( function() {
861 self.getSidebarWidgetsControl().toggleReordering( false );
862
863 var oldSidebarId = self.params.sidebar_id,
864 newSidebarId = self.container.find( '.widget-area-select li.selected' ).data( 'id' ),
865 oldSidebarWidgetsSetting, newSidebarWidgetsSetting,
866 oldSidebarWidgetIds, newSidebarWidgetIds, i;
867
868 oldSidebarWidgetsSetting = api( 'sidebars_widgets[' + oldSidebarId + ']' );
869 newSidebarWidgetsSetting = api( 'sidebars_widgets[' + newSidebarId + ']' );
870 oldSidebarWidgetIds = Array.prototype.slice.call( oldSidebarWidgetsSetting() );
871 newSidebarWidgetIds = Array.prototype.slice.call( newSidebarWidgetsSetting() );
872
873 i = self.getWidgetSidebarPosition();
874 oldSidebarWidgetIds.splice( i, 1 );
875 newSidebarWidgetIds.push( self.params.widget_id );
876
877 oldSidebarWidgetsSetting( oldSidebarWidgetIds );
878 newSidebarWidgetsSetting( newSidebarWidgetIds );
879
880 self.focus();
881 } );
882 },
883
884 /**
885 * Highlight widgets in preview when interacted with in the Customizer
886 */
887 _setupHighlightEffects: function() {
888 var self = this;
889
890 // Highlight whenever hovering or clicking over the form.
891 this.container.on( 'mouseenter click', function() {
892 self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
893 } );
894
895 // Highlight when the setting is updated.
896 this.setting.bind( function() {
897 self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
898 } );
899 },
900
901 /**
902 * Set up event handlers for widget updating
903 */
904 _setupUpdateUI: function() {
905 var self = this, $widgetRoot, $widgetContent,
906 $saveBtn, updateWidgetDebounced, formSyncHandler;
907
908 $widgetRoot = this.container.find( '.widget:first' );
909 $widgetContent = $widgetRoot.find( '.widget-content:first' );
910
911 // Configure update button.
912 $saveBtn = this.container.find( '.widget-control-save' );
913 $saveBtn.val( l10n.saveBtnLabel );
914 $saveBtn.attr( 'title', l10n.saveBtnTooltip );
915 $saveBtn.removeClass( 'button-primary' );
916 $saveBtn.on( 'click', function( e ) {
917 e.preventDefault();
918 self.updateWidget( { disable_form: true } ); // @todo disable_form is unused?
919 } );
920
921 updateWidgetDebounced = _.debounce( function() {
922 self.updateWidget();
923 }, 250 );
924
925 // Trigger widget form update when hitting Enter within an input.
926 $widgetContent.on( 'keydown', 'input', function( e ) {
927 if ( 13 === e.which ) { // Enter.
928 e.preventDefault();
929 self.updateWidget( { ignoreActiveElement: true } );
930 }
931 } );
932
933 // Handle widgets that support live previews.
934 $widgetContent.on( 'change input propertychange', ':input', function( e ) {
935 if ( ! self.liveUpdateMode ) {
936 return;
937 }
938 if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) {
939 updateWidgetDebounced();
940 }
941 } );
942
943 // Remove loading indicators when the setting is saved and the preview updates.
944 this.setting.previewer.channel.bind( 'synced', function() {
945 self.container.removeClass( 'previewer-loading' );
946 } );
947
948 api.previewer.bind( 'widget-updated', function( updatedWidgetId ) {
949 if ( updatedWidgetId === self.params.widget_id ) {
950 self.container.removeClass( 'previewer-loading' );
951 }
952 } );
953
954 formSyncHandler = api.Widgets.formSyncHandlers[ this.params.widget_id_base ];
955 if ( formSyncHandler ) {
956 $( document ).on( 'widget-synced', function( e, widget ) {
957 if ( $widgetRoot.is( widget ) ) {
958 formSyncHandler.apply( document, arguments );
959 }
960 } );
961 }
962 },
963
964 /**
965 * Update widget control to indicate whether it is currently rendered.
966 *
967 * Overrides api.Control.toggle()
968 *
969 * @since 4.1.0
970 *
971 * @param {boolean} active
972 * @param {Object} args
973 * @param {function} args.completeCallback
974 */
975 onChangeActive: function ( active, args ) {
976 // Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments.
977 this.container.toggleClass( 'widget-rendered', active );
978 if ( args.completeCallback ) {
979 args.completeCallback();
980 }
981 },
982
983 /**
984 * Set up event handlers for widget removal
985 */
986 _setupRemoveUI: function() {
987 var self = this, $removeBtn, replaceDeleteWithRemove;
988
989 // Configure remove button.
990 $removeBtn = this.container.find( '.widget-control-remove' );
991 $removeBtn.on( 'click', function() {
992 // Find an adjacent element to add focus to when this widget goes away.
993 var $adjacentFocusTarget;
994 if ( self.container.next().is( '.customize-control-widget_form' ) ) {
995 $adjacentFocusTarget = self.container.next().find( '.widget-action:first' );
996 } else if ( self.container.prev().is( '.customize-control-widget_form' ) ) {
997 $adjacentFocusTarget = self.container.prev().find( '.widget-action:first' );
998 } else {
999 $adjacentFocusTarget = self.container.next( '.customize-control-sidebar_widgets' ).find( '.add-new-widget:first' );
1000 }
1001
1002 self.container.slideUp( function() {
1003 var sidebarsWidgetsControl = api.Widgets.getSidebarWidgetControlContainingWidget( self.params.widget_id ),
1004 sidebarWidgetIds, i;
1005
1006 if ( ! sidebarsWidgetsControl ) {
1007 return;
1008 }
1009
1010 sidebarWidgetIds = sidebarsWidgetsControl.setting().slice();
1011 i = _.indexOf( sidebarWidgetIds, self.params.widget_id );
1012 if ( -1 === i ) {
1013 return;
1014 }
1015
1016 sidebarWidgetIds.splice( i, 1 );
1017 sidebarsWidgetsControl.setting( sidebarWidgetIds );
1018
1019 $adjacentFocusTarget.focus(); // Keyboard accessibility.
1020 } );
1021 } );
1022
1023 replaceDeleteWithRemove = function() {
1024 $removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the button as "Delete".
1025 $removeBtn.attr( 'title', l10n.removeBtnTooltip );
1026 };
1027
1028 if ( this.params.is_new ) {
1029 api.bind( 'saved', replaceDeleteWithRemove );
1030 } else {
1031 replaceDeleteWithRemove();
1032 }
1033 },
1034
1035 /**
1036 * Find all inputs in a widget container that should be considered when
1037 * comparing the loaded form with the sanitized form, whose fields will
1038 * be aligned to copy the sanitized over. The elements returned by this
1039 * are passed into this._getInputsSignature(), and they are iterated
1040 * over when copying sanitized values over to the form loaded.
1041 *
1042 * @param {jQuery} container element in which to look for inputs
1043 * @return {jQuery} inputs
1044 * @private
1045 */
1046 _getInputs: function( container ) {
1047 return $( container ).find( ':input[name]' );
1048 },
1049
1050 /**
1051 * Iterate over supplied inputs and create a signature string for all of them together.
1052 * This string can be used to compare whether or not the form has all of the same fields.
1053 *
1054 * @param {jQuery} inputs
1055 * @return {string}
1056 * @private
1057 */
1058 _getInputsSignature: function( inputs ) {
1059 var inputsSignatures = _( inputs ).map( function( input ) {
1060 var $input = $( input ), signatureParts;
1061
1062 if ( $input.is( ':checkbox, :radio' ) ) {
1063 signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ), $input.prop( 'value' ) ];
1064 } else {
1065 signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ) ];
1066 }
1067
1068 return signatureParts.join( ',' );
1069 } );
1070
1071 return inputsSignatures.join( ';' );
1072 },
1073
1074 /**
1075 * Get the state for an input depending on its type.
1076 *
1077 * @param {jQuery|Element} input
1078 * @return {string|boolean|Array|*}
1079 * @private
1080 */
1081 _getInputState: function( input ) {
1082 input = $( input );
1083 if ( input.is( ':radio, :checkbox' ) ) {
1084 return input.prop( 'checked' );
1085 } else if ( input.is( 'select[multiple]' ) ) {
1086 return input.find( 'option:selected' ).map( function () {
1087 return $( this ).val();
1088 } ).get();
1089 } else {
1090 return input.val();
1091 }
1092 },
1093
1094 /**
1095 * Update an input's state based on its type.
1096 *
1097 * @param {jQuery|Element} input
1098 * @param {string|boolean|Array|*} state
1099 * @private
1100 */
1101 _setInputState: function ( input, state ) {
1102 input = $( input );
1103 if ( input.is( ':radio, :checkbox' ) ) {
1104 input.prop( 'checked', state );
1105 } else if ( input.is( 'select[multiple]' ) ) {
1106 if ( ! Array.isArray( state ) ) {
1107 state = [];
1108 } else {
1109 // Make sure all state items are strings since the DOM value is a string.
1110 state = _.map( state, function ( value ) {
1111 return String( value );
1112 } );
1113 }
1114 input.find( 'option' ).each( function () {
1115 $( this ).prop( 'selected', -1 !== _.indexOf( state, String( this.value ) ) );
1116 } );
1117 } else {
1118 input.val( state );
1119 }
1120 },
1121
1122 /***********************************************************************
1123 * Begin public API methods
1124 **********************************************************************/
1125
1126 /**
1127 * @return {wp.customize.controlConstructor.sidebar_widgets[]}
1128 */
1129 getSidebarWidgetsControl: function() {
1130 var settingId, sidebarWidgetsControl;
1131
1132 settingId = 'sidebars_widgets[' + this.params.sidebar_id + ']';
1133 sidebarWidgetsControl = api.control( settingId );
1134
1135 if ( ! sidebarWidgetsControl ) {
1136 return;
1137 }
1138
1139 return sidebarWidgetsControl;
1140 },
1141
1142 /**
1143 * Submit the widget form via Ajax and get back the updated instance,
1144 * along with the new widget control form to render.
1145 *
1146 * @param {Object} [args]
1147 * @param {Object|null} [args.instance=null] When the model changes, the instance is sent here; otherwise, the inputs from the form are used
1148 * @param {Function|null} [args.complete=null] Function which is called when the request finishes. Context is bound to the control. First argument is any error. Following arguments are for success.
1149 * @param {boolean} [args.ignoreActiveElement=false] Whether or not updating a field will be deferred if focus is still on the element.
1150 */
1151 updateWidget: function( args ) {
1152 var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent,
1153 updateNumber, params, data, $inputs, processing, jqxhr, isChanged;
1154
1155 // The updateWidget logic requires that the form fields to be fully present.
1156 self.embedWidgetContent();
1157
1158 args = $.extend( {
1159 instance: null,
1160 complete: null,
1161 ignoreActiveElement: false
1162 }, args );
1163
1164 instanceOverride = args.instance;
1165 completeCallback = args.complete;
1166
1167 this._updateCount += 1;
1168 updateNumber = this._updateCount;
1169
1170 $widgetRoot = this.container.find( '.widget:first' );
1171 $widgetContent = $widgetRoot.find( '.widget-content:first' );
1172
1173 // Remove a previous error message.
1174 $widgetContent.find( '.widget-error' ).remove();
1175
1176 this.container.addClass( 'widget-form-loading' );
1177 this.container.addClass( 'previewer-loading' );
1178 processing = api.state( 'processing' );
1179 processing( processing() + 1 );
1180
1181 if ( ! this.liveUpdateMode ) {
1182 this.container.addClass( 'widget-form-disabled' );
1183 }
1184
1185 params = {};
1186 params.action = 'update-widget';
1187 params.wp_customize = 'on';
1188 params.nonce = api.settings.nonce['update-widget'];
1189 params.customize_theme = api.settings.theme.stylesheet;
1190 params.customized = wp.customize.previewer.query().customized;
1191
1192 data = $.param( params );
1193 $inputs = this._getInputs( $widgetContent );
1194
1195 /*
1196 * Store the value we're submitting in data so that when the response comes back,
1197 * we know if it got sanitized; if there is no difference in the sanitized value,
1198 * then we do not need to touch the UI and mess up the user's ongoing editing.
1199 */
1200 $inputs.each( function() {
1201 $( this ).data( 'state' + updateNumber, self._getInputState( this ) );
1202 } );
1203
1204 if ( instanceOverride ) {
1205 data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instanceOverride ) } );
1206 } else {
1207 data += '&' + $inputs.serialize();
1208 }
1209 data += '&' + $widgetContent.find( '~ :input' ).serialize();
1210
1211 if ( this._previousUpdateRequest ) {
1212 this._previousUpdateRequest.abort();
1213 }
1214 jqxhr = $.post( wp.ajax.settings.url, data );
1215 this._previousUpdateRequest = jqxhr;
1216
1217 jqxhr.done( function( r ) {
1218 var message, sanitizedForm, $sanitizedInputs, hasSameInputsInResponse,
1219 isLiveUpdateAborted = false;
1220
1221 // Check if the user is logged out.
1222 if ( '0' === r ) {
1223 api.previewer.preview.iframe.hide();
1224 api.previewer.login().done( function() {
1225 self.updateWidget( args );
1226 api.previewer.preview.iframe.show();
1227 } );
1228 return;
1229 }
1230
1231 // Check for cheaters.
1232 if ( '-1' === r ) {
1233 api.previewer.cheatin();
1234 return;
1235 }
1236
1237 if ( r.success ) {
1238 sanitizedForm = $( '<div>' + r.data.form + '</div>' );
1239 $sanitizedInputs = self._getInputs( sanitizedForm );
1240 hasSameInputsInResponse = self._getInputsSignature( $inputs ) === self._getInputsSignature( $sanitizedInputs );
1241
1242 // Restore live update mode if sanitized fields are now aligned with the existing fields.
1243 if ( hasSameInputsInResponse && ! self.liveUpdateMode ) {
1244 self.liveUpdateMode = true;
1245 self.container.removeClass( 'widget-form-disabled' );
1246 self.container.find( 'input[name="savewidget"]' ).hide();
1247 }
1248
1249 // Sync sanitized field states to existing fields if they are aligned.
1250 if ( hasSameInputsInResponse && self.liveUpdateMode ) {
1251 $inputs.each( function( i ) {
1252 var $input = $( this ),
1253 $sanitizedInput = $( $sanitizedInputs[i] ),
1254 submittedState, sanitizedState, canUpdateState;
1255
1256 submittedState = $input.data( 'state' + updateNumber );
1257 sanitizedState = self._getInputState( $sanitizedInput );
1258 $input.data( 'sanitized', sanitizedState );
1259
1260 canUpdateState = ( ! _.isEqual( submittedState, sanitizedState ) && ( args.ignoreActiveElement || ! $input.is( document.activeElement ) ) );
1261 if ( canUpdateState ) {
1262 self._setInputState( $input, sanitizedState );
1263 }
1264 } );
1265
1266 $( document ).trigger( 'widget-synced', [ $widgetRoot, r.data.form ] );
1267
1268 // Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled.
1269 } else if ( self.liveUpdateMode ) {
1270 self.liveUpdateMode = false;
1271 self.container.find( 'input[name="savewidget"]' ).show();
1272 isLiveUpdateAborted = true;
1273
1274 // Otherwise, replace existing form with the sanitized form.
1275 } else {
1276 $widgetContent.html( r.data.form );
1277
1278 self.container.removeClass( 'widget-form-disabled' );
1279
1280 $( document ).trigger( 'widget-updated', [ $widgetRoot ] );
1281 }
1282
1283 /**
1284 * If the old instance is identical to the new one, there is nothing new
1285 * needing to be rendered, and so we can preempt the event for the
1286 * preview finishing loading.
1287 */
1288 isChanged = ! isLiveUpdateAborted && ! _( self.setting() ).isEqual( r.data.instance );
1289 if ( isChanged ) {
1290 self.isWidgetUpdating = true; // Suppress triggering another updateWidget.
1291 self.setting( r.data.instance );
1292 self.isWidgetUpdating = false;
1293 } else {
1294 // No change was made, so stop the spinner now instead of when the preview would updates.
1295 self.container.removeClass( 'previewer-loading' );
1296 }
1297
1298 if ( completeCallback ) {
1299 completeCallback.call( self, null, { noChange: ! isChanged, ajaxFinished: true } );
1300 }
1301 } else {
1302 // General error message.
1303 message = l10n.error;
1304
1305 if ( r.data && r.data.message ) {
1306 message = r.data.message;
1307 }
1308
1309 if ( completeCallback ) {
1310 completeCallback.call( self, message );
1311 } else {
1312 $widgetContent.prepend( '<p class="widget-error"><strong>' + message + '</strong></p>' );
1313 }
1314 }
1315 } );
1316
1317 jqxhr.fail( function( jqXHR, textStatus ) {
1318 if ( completeCallback ) {
1319 completeCallback.call( self, textStatus );
1320 }
1321 } );
1322
1323 jqxhr.always( function() {
1324 self.container.removeClass( 'widget-form-loading' );
1325
1326 $inputs.each( function() {
1327 $( this ).removeData( 'state' + updateNumber );
1328 } );
1329
1330 processing( processing() - 1 );
1331 } );
1332 },
1333
1334 /**
1335 * Expand the accordion section containing a control
1336 */
1337 expandControlSection: function() {
1338 api.Control.prototype.expand.call( this );
1339 },
1340
1341 /**
1342 * @since 4.1.0
1343 *
1344 * @param {Boolean} expanded
1345 * @param {Object} [params]
1346 * @return {Boolean} False if state already applied.
1347 */
1348 _toggleExpanded: api.Section.prototype._toggleExpanded,
1349
1350 /**
1351 * @since 4.1.0
1352 *
1353 * @param {Object} [params]
1354 * @return {Boolean} False if already expanded.
1355 */
1356 expand: api.Section.prototype.expand,
1357
1358 /**
1359 * Expand the widget form control
1360 *
1361 * @deprecated 4.1.0 Use this.expand() instead.
1362 */
1363 expandForm: function() {
1364 this.expand();
1365 },
1366
1367 /**
1368 * @since 4.1.0
1369 *
1370 * @param {Object} [params]
1371 * @return {Boolean} False if already collapsed.
1372 */
1373 collapse: api.Section.prototype.collapse,
1374
1375 /**
1376 * Collapse the widget form control
1377 *
1378 * @deprecated 4.1.0 Use this.collapse() instead.
1379 */
1380 collapseForm: function() {
1381 this.collapse();
1382 },
1383
1384 /**
1385 * Expand or collapse the widget control
1386 *
1387 * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
1388 *
1389 * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility
1390 */
1391 toggleForm: function( showOrHide ) {
1392 if ( typeof showOrHide === 'undefined' ) {
1393 showOrHide = ! this.expanded();
1394 }
1395 this.expanded( showOrHide );
1396 },
1397
1398 /**
1399 * Respond to change in the expanded state.
1400 *
1401 * @param {boolean} expanded
1402 * @param {Object} args merged on top of this.defaultActiveArguments
1403 */
1404 onChangeExpanded: function ( expanded, args ) {
1405 var self = this, $widget, $inside, complete, prevComplete, expandControl, $toggleBtn;
1406
1407 self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI.
1408 if ( expanded ) {
1409 self.embedWidgetContent();
1410 }
1411
1412 // If the expanded state is unchanged only manipulate container expanded states.
1413 if ( args.unchanged ) {
1414 if ( expanded ) {
1415 api.Control.prototype.expand.call( self, {
1416 completeCallback: args.completeCallback
1417 });
1418 }
1419 return;
1420 }
1421
1422 $widget = this.container.find( 'div.widget:first' );
1423 $inside = $widget.find( '.widget-inside:first' );
1424 $toggleBtn = this.container.find( '.widget-top button.widget-action' );
1425
1426 expandControl = function() {
1427
1428 // Close all other widget controls before expanding this one.
1429 api.control.each( function( otherControl ) {
1430 if ( self.params.type === otherControl.params.type && self !== otherControl ) {
1431 otherControl.collapse();
1432 }
1433 } );
1434
1435 complete = function() {
1436 self.container.removeClass( 'expanding' );
1437 self.container.addClass( 'expanded' );
1438 $widget.addClass( 'open' );
1439 $toggleBtn.attr( 'aria-expanded', 'true' );
1440 self.container.trigger( 'expanded' );
1441 };
1442 if ( args.completeCallback ) {
1443 prevComplete = complete;
1444 complete = function () {
1445 prevComplete();
1446 args.completeCallback();
1447 };
1448 }
1449
1450 if ( self.params.is_wide ) {
1451 $inside.fadeIn( args.duration, complete );
1452 } else {
1453 $inside.slideDown( args.duration, complete );
1454 }
1455
1456 self.container.trigger( 'expand' );
1457 self.container.addClass( 'expanding' );
1458 };
1459
1460 if ( $toggleBtn.attr( 'aria-expanded' ) === 'false' ) {
1461 if ( api.section.has( self.section() ) ) {
1462 api.section( self.section() ).expand( {
1463 completeCallback: expandControl
1464 } );
1465 } else {
1466 expandControl();
1467 }
1468 } else {
1469 complete = function() {
1470 self.container.removeClass( 'collapsing' );
1471 self.container.removeClass( 'expanded' );
1472 $widget.removeClass( 'open' );
1473 $toggleBtn.attr( 'aria-expanded', 'false' );
1474 self.container.trigger( 'collapsed' );
1475 };
1476 if ( args.completeCallback ) {
1477 prevComplete = complete;
1478 complete = function () {
1479 prevComplete();
1480 args.completeCallback();
1481 };
1482 }
1483
1484 self.container.trigger( 'collapse' );
1485 self.container.addClass( 'collapsing' );
1486
1487 if ( self.params.is_wide ) {
1488 $inside.fadeOut( args.duration, complete );
1489 } else {
1490 $inside.slideUp( args.duration, function() {
1491 $widget.css( { width:'', margin:'' } );
1492 complete();
1493 } );
1494 }
1495 }
1496 },
1497
1498 /**
1499 * Get the position (index) of the widget in the containing sidebar
1500 *
1501 * @return {number}
1502 */
1503 getWidgetSidebarPosition: function() {
1504 var sidebarWidgetIds, position;
1505
1506 sidebarWidgetIds = this.getSidebarWidgetsControl().setting();
1507 position = _.indexOf( sidebarWidgetIds, this.params.widget_id );
1508
1509 if ( position === -1 ) {
1510 return;
1511 }
1512
1513 return position;
1514 },
1515
1516 /**
1517 * Move widget up one in the sidebar
1518 */
1519 moveUp: function() {
1520 this._moveWidgetByOne( -1 );
1521 },
1522
1523 /**
1524 * Move widget up one in the sidebar
1525 */
1526 moveDown: function() {
1527 this._moveWidgetByOne( 1 );
1528 },
1529
1530 /**
1531 * @private
1532 *
1533 * @param {number} offset 1|-1
1534 */
1535 _moveWidgetByOne: function( offset ) {
1536 var i, sidebarWidgetsSetting, sidebarWidgetIds, adjacentWidgetId;
1537
1538 i = this.getWidgetSidebarPosition();
1539
1540 sidebarWidgetsSetting = this.getSidebarWidgetsControl().setting;
1541 sidebarWidgetIds = Array.prototype.slice.call( sidebarWidgetsSetting() ); // Clone.
1542 adjacentWidgetId = sidebarWidgetIds[i + offset];
1543 sidebarWidgetIds[i + offset] = this.params.widget_id;
1544 sidebarWidgetIds[i] = adjacentWidgetId;
1545
1546 sidebarWidgetsSetting( sidebarWidgetIds );
1547 },
1548
1549 /**
1550 * Toggle visibility of the widget move area
1551 *
1552 * @param {boolean} [showOrHide]
1553 */
1554 toggleWidgetMoveArea: function( showOrHide ) {
1555 var self = this, $moveWidgetArea;
1556
1557 $moveWidgetArea = this.container.find( '.move-widget-area' );
1558
1559 if ( typeof showOrHide === 'undefined' ) {
1560 showOrHide = ! $moveWidgetArea.hasClass( 'active' );
1561 }
1562
1563 if ( showOrHide ) {
1564 // Reset the selected sidebar.
1565 $moveWidgetArea.find( '.selected' ).removeClass( 'selected' );
1566
1567 $moveWidgetArea.find( 'li' ).filter( function() {
1568 return $( this ).data( 'id' ) === self.params.sidebar_id;
1569 } ).addClass( 'selected' );
1570
1571 this.container.find( '.move-widget-btn' ).prop( 'disabled', true );
1572 }
1573
1574 $moveWidgetArea.toggleClass( 'active', showOrHide );
1575 },
1576
1577 /**
1578 * Highlight the widget control and section
1579 */
1580 highlightSectionAndControl: function() {
1581 var $target;
1582
1583 if ( this.container.is( ':hidden' ) ) {
1584 $target = this.container.closest( '.control-section' );
1585 } else {
1586 $target = this.container;
1587 }
1588
1589 $( '.highlighted' ).removeClass( 'highlighted' );
1590 $target.addClass( 'highlighted' );
1591
1592 setTimeout( function() {
1593 $target.removeClass( 'highlighted' );
1594 }, 500 );
1595 }
1596 } );
1597
1598 /**
1599 * wp.customize.Widgets.WidgetsPanel
1600 *
1601 * Customizer panel containing the widget area sections.
1602 *
1603 * @since 4.4.0
1604 *
1605 * @class wp.customize.Widgets.WidgetsPanel
1606 * @augments wp.customize.Panel
1607 */
1608 api.Widgets.WidgetsPanel = api.Panel.extend(/** @lends wp.customize.Widgets.WigetsPanel.prototype */{
1609
1610 /**
1611 * Add and manage the display of the no-rendered-areas notice.
1612 *
1613 * @since 4.4.0
1614 */
1615 ready: function () {
1616 var panel = this;
1617
1618 api.Panel.prototype.ready.call( panel );
1619
1620 panel.deferred.embedded.done(function() {
1621 var panelMetaContainer, noticeContainer, updateNotice, getActiveSectionCount, shouldShowNotice;
1622 panelMetaContainer = panel.container.find( '.panel-meta' );
1623
1624 // @todo This should use the Notifications API introduced to panels. See <https://core.trac.wordpress.org/ticket/38794>.
1625 noticeContainer = $( '<div></div>', {
1626 'class': 'no-widget-areas-rendered-notice',
1627 'role': 'alert'
1628 });
1629 panelMetaContainer.append( noticeContainer );
1630
1631 /**
1632 * Get the number of active sections in the panel.
1633 *
1634 * @return {number} Number of active sidebar sections.
1635 */
1636 getActiveSectionCount = function() {
1637 return _.filter( panel.sections(), function( section ) {
1638 return 'sidebar' === section.params.type && section.active();
1639 } ).length;
1640 };
1641
1642 /**
1643 * Determine whether or not the notice should be displayed.
1644 *
1645 * @return {boolean}
1646 */
1647 shouldShowNotice = function() {
1648 var activeSectionCount = getActiveSectionCount();
1649 if ( 0 === activeSectionCount ) {
1650 return true;
1651 } else {
1652 return activeSectionCount !== api.Widgets.data.registeredSidebars.length;
1653 }
1654 };
1655
1656 /**
1657 * Update the notice.
1658 *
1659 * @return {void}
1660 */
1661 updateNotice = function() {
1662 var activeSectionCount = getActiveSectionCount(), someRenderedMessage, nonRenderedAreaCount, registeredAreaCount;
1663 noticeContainer.empty();
1664
1665 registeredAreaCount = api.Widgets.data.registeredSidebars.length;
1666 if ( activeSectionCount !== registeredAreaCount ) {
1667
1668 if ( 0 !== activeSectionCount ) {
1669 nonRenderedAreaCount = registeredAreaCount - activeSectionCount;
1670 someRenderedMessage = l10n.someAreasShown[ nonRenderedAreaCount ];
1671 } else {
1672 someRenderedMessage = l10n.noAreasShown;
1673 }
1674 if ( someRenderedMessage ) {
1675 noticeContainer.append( $( '<p></p>', {
1676 text: someRenderedMessage
1677 } ) );
1678 }
1679
1680 noticeContainer.append( $( '<p></p>', {
1681 text: l10n.navigatePreview
1682 } ) );
1683 }
1684 };
1685 updateNotice();
1686
1687 /*
1688 * Set the initial visibility state for rendered notice.
1689 * Update the visibility of the notice whenever a reflow happens.
1690 */
1691 noticeContainer.toggle( shouldShowNotice() );
1692 api.previewer.deferred.active.done( function () {
1693 noticeContainer.toggle( shouldShowNotice() );
1694 });
1695 api.bind( 'pane-contents-reflowed', function() {
1696 var duration = ( 'resolved' === api.previewer.deferred.active.state() ) ? 'fast' : 0;
1697 updateNotice();
1698 if ( shouldShowNotice() ) {
1699 noticeContainer.slideDown( duration );
1700 } else {
1701 noticeContainer.slideUp( duration );
1702 }
1703 });
1704 });
1705 },
1706
1707 /**
1708 * Allow an active widgets panel to be contextually active even when it has no active sections (widget areas).
1709 *
1710 * This ensures that the widgets panel appears even when there are no
1711 * sidebars displayed on the URL currently being previewed.
1712 *
1713 * @since 4.4.0
1714 *
1715 * @return {boolean}
1716 */
1717 isContextuallyActive: function() {
1718 var panel = this;
1719 return panel.active();
1720 }
1721 });
1722
1723 /**
1724 * wp.customize.Widgets.SidebarSection
1725 *
1726 * Customizer section representing a widget area widget
1727 *
1728 * @since 4.1.0
1729 *
1730 * @class wp.customize.Widgets.SidebarSection
1731 * @augments wp.customize.Section
1732 */
1733 api.Widgets.SidebarSection = api.Section.extend(/** @lends wp.customize.Widgets.SidebarSection.prototype */{
1734
1735 /**
1736 * Sync the section's active state back to the Backbone model's is_rendered attribute
1737 *
1738 * @since 4.1.0
1739 */
1740 ready: function () {
1741 var section = this, registeredSidebar;
1742 api.Section.prototype.ready.call( this );
1743 registeredSidebar = api.Widgets.registeredSidebars.get( section.params.sidebarId );
1744 section.active.bind( function ( active ) {
1745 registeredSidebar.set( 'is_rendered', active );
1746 });
1747 registeredSidebar.set( 'is_rendered', section.active() );
1748 }
1749 });
1750
1751 /**
1752 * wp.customize.Widgets.SidebarControl
1753 *
1754 * Customizer control for widgets.
1755 * Note that 'sidebar_widgets' must match the WP_Widget_Area_Customize_Control::$type
1756 *
1757 * @since 3.9.0
1758 *
1759 * @class wp.customize.Widgets.SidebarControl
1760 * @augments wp.customize.Control
1761 */
1762 api.Widgets.SidebarControl = api.Control.extend(/** @lends wp.customize.Widgets.SidebarControl.prototype */{
1763
1764 /**
1765 * Set up the control
1766 */
1767 ready: function() {
1768 this.$controlSection = this.container.closest( '.control-section' );
1769 this.$sectionContent = this.container.closest( '.accordion-section-content' );
1770
1771 this._setupModel();
1772 this._setupSortable();
1773 this._setupAddition();
1774 this._applyCardinalOrderClassNames();
1775 },
1776
1777 /**
1778 * Update ordering of widget control forms when the setting is updated
1779 */
1780 _setupModel: function() {
1781 var self = this;
1782
1783 this.setting.bind( function( newWidgetIds, oldWidgetIds ) {
1784 var widgetFormControls, removedWidgetIds, priority;
1785
1786 removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds );
1787
1788 // Filter out any persistent widget IDs for widgets which have been deactivated.
1789 newWidgetIds = _( newWidgetIds ).filter( function( newWidgetId ) {
1790 var parsedWidgetId = parseWidgetId( newWidgetId );
1791
1792 return !! api.Widgets.availableWidgets.findWhere( { id_base: parsedWidgetId.id_base } );
1793 } );
1794
1795 widgetFormControls = _( newWidgetIds ).map( function( widgetId ) {
1796 var widgetFormControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
1797
1798 if ( ! widgetFormControl ) {
1799 widgetFormControl = self.addWidget( widgetId );
1800 }
1801
1802 return widgetFormControl;
1803 } );
1804
1805 // Sort widget controls to their new positions.
1806 widgetFormControls.sort( function( a, b ) {
1807 var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ),
1808 bIndex = _.indexOf( newWidgetIds, b.params.widget_id );
1809 return aIndex - bIndex;
1810 });
1811
1812 priority = 0;
1813 _( widgetFormControls ).each( function ( control ) {
1814 control.priority( priority );
1815 control.section( self.section() );
1816 priority += 1;
1817 });
1818 self.priority( priority ); // Make sure sidebar control remains at end.
1819
1820 // Re-sort widget form controls (including widgets form other sidebars newly moved here).
1821 self._applyCardinalOrderClassNames();
1822
1823 // If the widget was dragged into the sidebar, make sure the sidebar_id param is updated.
1824 _( widgetFormControls ).each( function( widgetFormControl ) {
1825 widgetFormControl.params.sidebar_id = self.params.sidebar_id;
1826 } );
1827
1828 // Cleanup after widget removal.
1829 _( removedWidgetIds ).each( function( removedWidgetId ) {
1830
1831 // Using setTimeout so that when moving a widget to another sidebar,
1832 // the other sidebars_widgets settings get a chance to update.
1833 setTimeout( function() {
1834 var removedControl, wasDraggedToAnotherSidebar, inactiveWidgets, removedIdBase,
1835 widget, isPresentInAnotherSidebar = false;
1836
1837 // Check if the widget is in another sidebar.
1838 api.each( function( otherSetting ) {
1839 if ( otherSetting.id === self.setting.id || 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) || otherSetting.id === 'sidebars_widgets[wp_inactive_widgets]' ) {
1840 return;
1841 }
1842
1843 var otherSidebarWidgets = otherSetting(), i;
1844
1845 i = _.indexOf( otherSidebarWidgets, removedWidgetId );
1846 if ( -1 !== i ) {
1847 isPresentInAnotherSidebar = true;
1848 }
1849 } );
1850
1851 // If the widget is present in another sidebar, abort!
1852 if ( isPresentInAnotherSidebar ) {
1853 return;
1854 }
1855
1856 removedControl = api.Widgets.getWidgetFormControlForWidget( removedWidgetId );
1857
1858 // Detect if widget control was dragged to another sidebar.
1859 wasDraggedToAnotherSidebar = removedControl && $.contains( document, removedControl.container[0] ) && ! $.contains( self.$sectionContent[0], removedControl.container[0] );
1860
1861 // Delete any widget form controls for removed widgets.
1862 if ( removedControl && ! wasDraggedToAnotherSidebar ) {
1863 api.control.remove( removedControl.id );
1864 removedControl.container.remove();
1865 }
1866
1867 // Move widget to inactive widgets sidebar (move it to Trash) if has been previously saved.
1868 // This prevents the inactive widgets sidebar from overflowing with throwaway widgets.
1869 if ( api.Widgets.savedWidgetIds[removedWidgetId] ) {
1870 inactiveWidgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice();
1871 inactiveWidgets.push( removedWidgetId );
1872 api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactiveWidgets ).unique() );
1873 }
1874
1875 // Make old single widget available for adding again.
1876 removedIdBase = parseWidgetId( removedWidgetId ).id_base;
1877 widget = api.Widgets.availableWidgets.findWhere( { id_base: removedIdBase } );
1878 if ( widget && ! widget.get( 'is_multi' ) ) {
1879 widget.set( 'is_disabled', false );
1880 }
1881 } );
1882
1883 } );
1884 } );
1885 },
1886
1887 /**
1888 * Allow widgets in sidebar to be re-ordered, and for the order to be previewed
1889 */
1890 _setupSortable: function() {
1891 var self = this;
1892
1893 this.isReordering = false;
1894
1895 /**
1896 * Update widget order setting when controls are re-ordered
1897 */
1898 this.$sectionContent.sortable( {
1899 items: '> .customize-control-widget_form',
1900 handle: '.widget-top',
1901 axis: 'y',
1902 tolerance: 'pointer',
1903 connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)',
1904 update: function() {
1905 var widgetContainerIds = self.$sectionContent.sortable( 'toArray' ), widgetIds;
1906
1907 widgetIds = $.map( widgetContainerIds, function( widgetContainerId ) {
1908 return $( '#' + widgetContainerId ).find( ':input[name=widget-id]' ).val();
1909 } );
1910
1911 self.setting( widgetIds );
1912 }
1913 } );
1914
1915 /**
1916 * Expand other Customizer sidebar section when dragging a control widget over it,
1917 * allowing the control to be dropped into another section
1918 */
1919 this.$controlSection.find( '.accordion-section-title' ).droppable({
1920 accept: '.customize-control-widget_form',
1921 over: function() {
1922 var section = api.section( self.section.get() );
1923 section.expand({
1924 allowMultiple: true, // Prevent the section being dragged from to be collapsed.
1925 completeCallback: function () {
1926 // @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed.
1927 api.section.each( function ( otherSection ) {
1928 if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) {
1929 otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' );
1930 }
1931 } );
1932 }
1933 });
1934 }
1935 });
1936
1937 /**
1938 * Keyboard-accessible reordering
1939 */
1940 this.container.find( '.reorder-toggle' ).on( 'click', function() {
1941 self.toggleReordering( ! self.isReordering );
1942 } );
1943 },
1944
1945 /**
1946 * Set up UI for adding a new widget
1947 */
1948 _setupAddition: function() {
1949 var self = this;
1950
1951 this.container.find( '.add-new-widget' ).on( 'click', function() {
1952 var addNewWidgetBtn = $( this );
1953
1954 if ( self.$sectionContent.hasClass( 'reordering' ) ) {
1955 return;
1956 }
1957
1958 if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) {
1959 addNewWidgetBtn.attr( 'aria-expanded', 'true' );
1960 api.Widgets.availableWidgetsPanel.open( self );
1961 } else {
1962 addNewWidgetBtn.attr( 'aria-expanded', 'false' );
1963 api.Widgets.availableWidgetsPanel.close();
1964 }
1965 } );
1966 },
1967
1968 /**
1969 * Add classes to the widget_form controls to assist with styling
1970 */
1971 _applyCardinalOrderClassNames: function() {
1972 var widgetControls = [];
1973 _.each( this.setting(), function ( widgetId ) {
1974 var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
1975 if ( widgetControl ) {
1976 widgetControls.push( widgetControl );
1977 }
1978 });
1979
1980 if ( 0 === widgetControls.length || ( 1 === api.Widgets.registeredSidebars.length && widgetControls.length <= 1 ) ) {
1981 this.container.find( '.reorder-toggle' ).hide();
1982 return;
1983 } else {
1984 this.container.find( '.reorder-toggle' ).show();
1985 }
1986
1987 $( widgetControls ).each( function () {
1988 $( this.container )
1989 .removeClass( 'first-widget' )
1990 .removeClass( 'last-widget' )
1991 .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
1992 });
1993
1994 _.first( widgetControls ).container
1995 .addClass( 'first-widget' )
1996 .find( '.move-widget-up' ).prop( 'tabIndex', -1 );
1997
1998 _.last( widgetControls ).container
1999 .addClass( 'last-widget' )
2000 .find( '.move-widget-down' ).prop( 'tabIndex', -1 );
2001 },
2002
2003
2004 /***********************************************************************
2005 * Begin public API methods
2006 **********************************************************************/
2007
2008 /**
2009 * Enable/disable the reordering UI
2010 *
2011 * @param {boolean} showOrHide to enable/disable reordering
2012 *
2013 * @todo We should have a reordering state instead and rename this to onChangeReordering
2014 */
2015 toggleReordering: function( showOrHide ) {
2016 var addNewWidgetBtn = this.$sectionContent.find( '.add-new-widget' ),
2017 reorderBtn = this.container.find( '.reorder-toggle' ),
2018 widgetsTitle = this.$sectionContent.find( '.widget-title' );
2019
2020 showOrHide = Boolean( showOrHide );
2021
2022 if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) {
2023 return;
2024 }
2025
2026 this.isReordering = showOrHide;
2027 this.$sectionContent.toggleClass( 'reordering', showOrHide );
2028
2029 if ( showOrHide ) {
2030 _( this.getWidgetFormControls() ).each( function( formControl ) {
2031 formControl.collapse();
2032 } );
2033
2034 addNewWidgetBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
2035 reorderBtn.attr( 'aria-label', l10n.reorderLabelOff );
2036 wp.a11y.speak( l10n.reorderModeOn );
2037 // Hide widget titles while reordering: title is already in the reorder controls.
2038 widgetsTitle.attr( 'aria-hidden', 'true' );
2039 } else {
2040 addNewWidgetBtn.removeAttr( 'tabindex aria-hidden' );
2041 reorderBtn.attr( 'aria-label', l10n.reorderLabelOn );
2042 wp.a11y.speak( l10n.reorderModeOff );
2043 widgetsTitle.attr( 'aria-hidden', 'false' );
2044 }
2045 },
2046
2047 /**
2048 * Get the widget_form Customize controls associated with the current sidebar.
2049 *
2050 * @since 3.9.0
2051 * @return {wp.customize.controlConstructor.widget_form[]}
2052 */
2053 getWidgetFormControls: function() {
2054 var formControls = [];
2055
2056 _( this.setting() ).each( function( widgetId ) {
2057 var settingId = widgetIdToSettingId( widgetId ),
2058 formControl = api.control( settingId );
2059 if ( formControl ) {
2060 formControls.push( formControl );
2061 }
2062 } );
2063
2064 return formControls;
2065 },
2066
2067 /**
2068 * @param {string} widgetId or an id_base for adding a previously non-existing widget.
2069 * @return {Object|false} widget_form control instance, or false on error.
2070 */
2071 addWidget: function( widgetId ) {
2072 var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor,
2073 parsedWidgetId = parseWidgetId( widgetId ),
2074 widgetNumber = parsedWidgetId.number,
2075 widgetIdBase = parsedWidgetId.id_base,
2076 widget = api.Widgets.availableWidgets.findWhere( {id_base: widgetIdBase} ),
2077 settingId, isExistingWidget, widgetFormControl, sidebarWidgets, settingArgs, setting;
2078
2079 if ( ! widget ) {
2080 return false;
2081 }
2082
2083 if ( widgetNumber && ! widget.get( 'is_multi' ) ) {
2084 return false;
2085 }
2086
2087 // Set up new multi widget.
2088 if ( widget.get( 'is_multi' ) && ! widgetNumber ) {
2089 widget.set( 'multi_number', widget.get( 'multi_number' ) + 1 );
2090 widgetNumber = widget.get( 'multi_number' );
2091 }
2092
2093 controlHtml = $( '#widget-tpl-' + widget.get( 'id' ) ).html().trim();
2094 if ( widget.get( 'is_multi' ) ) {
2095 controlHtml = controlHtml.replace( /<[^<>]+>/g, function( m ) {
2096 return m.replace( /__i__|%i%/g, widgetNumber );
2097 } );
2098 } else {
2099 widget.set( 'is_disabled', true ); // Prevent single widget from being added again now.
2100 }
2101
2102 $widget = $( controlHtml );
2103
2104 controlContainer = $( '<li/>' )
2105 .addClass( 'customize-control' )
2106 .addClass( 'customize-control-' + controlType )
2107 .append( $widget );
2108
2109 // Remove icon which is visible inside the panel.
2110 controlContainer.find( '> .widget-icon' ).remove();
2111
2112 if ( widget.get( 'is_multi' ) ) {
2113 controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber );
2114 controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber );
2115 }
2116
2117 widgetId = controlContainer.find( '[name="widget-id"]' ).val();
2118
2119 controlContainer.hide(); // To be slid-down below.
2120
2121 settingId = 'widget_' + widget.get( 'id_base' );
2122 if ( widget.get( 'is_multi' ) ) {
2123 settingId += '[' + widgetNumber + ']';
2124 }
2125 controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
2126
2127 // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget).
2128 isExistingWidget = api.has( settingId );
2129 if ( ! isExistingWidget ) {
2130 settingArgs = {
2131 transport: api.Widgets.data.selectiveRefreshableWidgets[ widget.get( 'id_base' ) ] ? 'postMessage' : 'refresh',
2132 previewer: this.setting.previewer
2133 };
2134 setting = api.create( settingId, settingId, '', settingArgs );
2135 setting.set( {} ); // Mark dirty, changing from '' to {}.
2136 }
2137
2138 controlConstructor = api.controlConstructor[controlType];
2139 widgetFormControl = new controlConstructor( settingId, {
2140 settings: {
2141 'default': settingId
2142 },
2143 content: controlContainer,
2144 sidebar_id: self.params.sidebar_id,
2145 widget_id: widgetId,
2146 widget_id_base: widget.get( 'id_base' ),
2147 type: controlType,
2148 is_new: ! isExistingWidget,
2149 width: widget.get( 'width' ),
2150 height: widget.get( 'height' ),
2151 is_wide: widget.get( 'is_wide' )
2152 } );
2153 api.control.add( widgetFormControl );
2154
2155 // Make sure widget is removed from the other sidebars.
2156 api.each( function( otherSetting ) {
2157 if ( otherSetting.id === self.setting.id ) {
2158 return;
2159 }
2160
2161 if ( 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) ) {
2162 return;
2163 }
2164
2165 var otherSidebarWidgets = otherSetting().slice(),
2166 i = _.indexOf( otherSidebarWidgets, widgetId );
2167
2168 if ( -1 !== i ) {
2169 otherSidebarWidgets.splice( i );
2170 otherSetting( otherSidebarWidgets );
2171 }
2172 } );
2173
2174 // Add widget to this sidebar.
2175 sidebarWidgets = this.setting().slice();
2176 if ( -1 === _.indexOf( sidebarWidgets, widgetId ) ) {
2177 sidebarWidgets.push( widgetId );
2178 this.setting( sidebarWidgets );
2179 }
2180
2181 controlContainer.slideDown( function() {
2182 if ( isExistingWidget ) {
2183 widgetFormControl.updateWidget( {
2184 instance: widgetFormControl.setting()
2185 } );
2186 }
2187 } );
2188
2189 return widgetFormControl;
2190 }
2191 } );
2192
2193 // Register models for custom panel, section, and control types.
2194 $.extend( api.panelConstructor, {
2195 widgets: api.Widgets.WidgetsPanel
2196 });
2197 $.extend( api.sectionConstructor, {
2198 sidebar: api.Widgets.SidebarSection
2199 });
2200 $.extend( api.controlConstructor, {
2201 widget_form: api.Widgets.WidgetControl,
2202 sidebar_widgets: api.Widgets.SidebarControl
2203 });
2204
2205 /**
2206 * Init Customizer for widgets.
2207 */
2208 api.bind( 'ready', function() {
2209 // Set up the widgets panel.
2210 api.Widgets.availableWidgetsPanel = new api.Widgets.AvailableWidgetsPanelView({
2211 collection: api.Widgets.availableWidgets
2212 });
2213
2214 // Highlight widget control.
2215 api.previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl );
2216
2217 // Open and focus widget control.
2218 api.previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl );
2219 } );
2220
2221 /**
2222 * Highlight a widget control.
2223 *
2224 * @param {string} widgetId
2225 */
2226 api.Widgets.highlightWidgetFormControl = function( widgetId ) {
2227 var control = api.Widgets.getWidgetFormControlForWidget( widgetId );
2228
2229 if ( control ) {
2230 control.highlightSectionAndControl();
2231 }
2232 },
2233
2234 /**
2235 * Focus a widget control.
2236 *
2237 * @param {string} widgetId
2238 */
2239 api.Widgets.focusWidgetFormControl = function( widgetId ) {
2240 var control = api.Widgets.getWidgetFormControlForWidget( widgetId );
2241
2242 if ( control ) {
2243 control.focus();
2244 }
2245 },
2246
2247 /**
2248 * Given a widget control, find the sidebar widgets control that contains it.
2249 * @param {string} widgetId
2250 * @return {Object|null}
2251 */
2252 api.Widgets.getSidebarWidgetControlContainingWidget = function( widgetId ) {
2253 var foundControl = null;
2254
2255 // @todo This can use widgetIdToSettingId(), then pass into wp.customize.control( x ).getSidebarWidgetsControl().
2256 api.control.each( function( control ) {
2257 if ( control.params.type === 'sidebar_widgets' && -1 !== _.indexOf( control.setting(), widgetId ) ) {
2258 foundControl = control;
2259 }
2260 } );
2261
2262 return foundControl;
2263 };
2264
2265 /**
2266 * Given a widget ID for a widget appearing in the preview, get the widget form control associated with it.
2267 *
2268 * @param {string} widgetId
2269 * @return {Object|null}
2270 */
2271 api.Widgets.getWidgetFormControlForWidget = function( widgetId ) {
2272 var foundControl = null;
2273
2274 // @todo We can just use widgetIdToSettingId() here.
2275 api.control.each( function( control ) {
2276 if ( control.params.type === 'widget_form' && control.params.widget_id === widgetId ) {
2277 foundControl = control;
2278 }
2279 } );
2280
2281 return foundControl;
2282 };
2283
2284 /**
2285 * Initialize Edit Menu button in Nav Menu widget.
2286 */
2287 $( document ).on( 'widget-added', function( event, widgetContainer ) {
2288 var parsedWidgetId, widgetControl, navMenuSelect, editMenuButton;
2289 parsedWidgetId = parseWidgetId( widgetContainer.find( '> .widget-inside > .form > .widget-id' ).val() );
2290 if ( 'nav_menu' !== parsedWidgetId.id_base ) {
2291 return;
2292 }
2293 widgetControl = api.control( 'widget_nav_menu[' + String( parsedWidgetId.number ) + ']' );
2294 if ( ! widgetControl ) {
2295 return;
2296 }
2297 navMenuSelect = widgetContainer.find( 'select[name*="nav_menu"]' );
2298 editMenuButton = widgetContainer.find( '.edit-selected-nav-menu > button' );
2299 if ( 0 === navMenuSelect.length || 0 === editMenuButton.length ) {
2300 return;
2301 }
2302 navMenuSelect.on( 'change', function() {
2303 if ( api.section.has( 'nav_menu[' + navMenuSelect.val() + ']' ) ) {
2304 editMenuButton.parent().show();
2305 } else {
2306 editMenuButton.parent().hide();
2307 }
2308 });
2309 editMenuButton.on( 'click', function() {
2310 var section = api.section( 'nav_menu[' + navMenuSelect.val() + ']' );
2311 if ( section ) {
2312 focusConstructWithBreadcrumb( section, widgetControl );
2313 }
2314 } );
2315 } );
2316
2317 /**
2318 * Focus (expand) one construct and then focus on another construct after the first is collapsed.
2319 *
2320 * This overrides the back button to serve the purpose of breadcrumb navigation.
2321 *
2322 * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} focusConstruct - The object to initially focus.
2323 * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} returnConstruct - The object to return focus.
2324 */
2325 function focusConstructWithBreadcrumb( focusConstruct, returnConstruct ) {
2326 focusConstruct.focus();
2327 function onceCollapsed( isExpanded ) {
2328 if ( ! isExpanded ) {
2329 focusConstruct.expanded.unbind( onceCollapsed );
2330 returnConstruct.focus();
2331 }
2332 }
2333 focusConstruct.expanded.bind( onceCollapsed );
2334 }
2335
2336 /**
2337 * @param {string} widgetId
2338 * @return {Object}
2339 */
2340 function parseWidgetId( widgetId ) {
2341 var matches, parsed = {
2342 number: null,
2343 id_base: null
2344 };
2345
2346 matches = widgetId.match( /^(.+)-(\d+)$/ );
2347 if ( matches ) {
2348 parsed.id_base = matches[1];
2349 parsed.number = parseInt( matches[2], 10 );
2350 } else {
2351 // Likely an old single widget.
2352 parsed.id_base = widgetId;
2353 }
2354
2355 return parsed;
2356 }
2357
2358 /**
2359 * @param {string} widgetId
2360 * @return {string} settingId
2361 */
2362 function widgetIdToSettingId( widgetId ) {
2363 var parsed = parseWidgetId( widgetId ), settingId;
2364
2365 settingId = 'widget_' + parsed.id_base;
2366 if ( parsed.number ) {
2367 settingId += '[' + parsed.number + ']';
2368 }
2369
2370 return settingId;
2371 }
2372
2373})( window.wp, jQuery );
2374
Ui Ux Design – Teachers Night Out https://cardgames4educators.com Wed, 16 Oct 2024 22:24:18 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.4 https://cardgames4educators.com/wp-content/uploads/2024/06/cropped-Card-4-Educators-logo-32x32.png Ui Ux Design – Teachers Night Out https://cardgames4educators.com 32 32 Masters In English How English Speaker https://cardgames4educators.com/masters-in-english-how-english-speaker/ https://cardgames4educators.com/masters-in-english-how-english-speaker/#comments Mon, 27 May 2024 08:54:45 +0000 https://themexriver.com/wp/kadu/?p=1

Erat himenaeos neque id sagittis massa. Hac suscipit pulvinar dignissim platea magnis eu. Don tellus a pharetra inceptos efficitur dui pulvinar. Feugiat facilisis penatibus pulvinar nunc dictumst donec odio platea habitasse. Lacus porta dolor purus elit ante bibendum tortor netus taciti nullam cubilia. Erat per suspendisse placerat morbi egestas pulvinar bibendum sollicitudin nec. Euismod cubilia eleifend velit himenaeos sodales lectus. Leo maximus cras ac porttitor aliquam torquent pulvinar odio volutpat parturient. Quisque risus finibus suspendisse mus purus magnis facilisi condimentum consectetur dui. Curae elit suspendisse cursus vehicula.

Turpis taciti class non vel pretium quis pulvinar tempor lobortis nunc. Libero phasellus parturient sapien volutpat malesuada ornare. Cubilia dignissim sollicitudin rhoncus lacinia maximus. Cras lorem fermentum bibendum pellentesque nisl etiam ligula enim cubilia. Vulputate pede sapien torquent montes tempus malesuada in mattis dis turpis vitae. Porta est tempor ex eget feugiat vulputate ipsum. Justo nec iaculis habitant diam arcu fermentum.

We offer comprehen sive emplo ment services such as assistance wit employer compliance.Our company is your strategic HR partner as instead of HR. john smithson

Cubilia dignissim sollicitudin rhoncus lacinia maximus. Cras lorem fermentum bibendum pellentesque nisl etiam ligula enim cubilia. Vulputate pede sapien torquent montes tempus malesuada in mattis dis turpis vitae.

Exploring Learning Landscapes in Academic

Feugiat facilisis penatibus pulvinar nunc dictumst donec odio platea habitasse. Lacus porta dolor purus elit ante bibendum tortor netus taciti nullam cubilia. Erat per suspendisse placerat morbi egestas pulvinar bibendum sollicitudin nec. Euismod cubilia eleifend velit himenaeos sodales lectus. Leo maximus cras ac porttitor aliquam torquent.

]]>
https://cardgames4educators.com/masters-in-english-how-english-speaker/feed/ 1