Diff: STRATO-apps/wordpress_03/app/wp-admin/js/customize-widgets.js

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
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 +