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

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
1 + /**
2 + * @output wp-admin/js/customize-controls.js
3 + */
4 +
5 + /* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer, console, confirm */
6 + (function( exports, $ ){
7 + var Container, focus, normalizedTransitionendEventName, api = wp.customize;
8 +
9 + var reducedMotionMediaQuery = window.matchMedia( '(prefers-reduced-motion: reduce)' );
10 + var isReducedMotion = reducedMotionMediaQuery.matches;
11 + reducedMotionMediaQuery.addEventListener( 'change' , function handleReducedMotionChange( event ) {
12 + isReducedMotion = event.matches;
13 + });
14 +
15 + api.OverlayNotification = api.Notification.extend(/** @lends wp.customize.OverlayNotification.prototype */{
16 +
17 + /**
18 + * Whether the notification should show a loading spinner.
19 + *
20 + * @since 4.9.0
21 + * @var {boolean}
22 + */
23 + loading: false,
24 +
25 + /**
26 + * A notification that is displayed in a full-screen overlay.
27 + *
28 + * @constructs wp.customize.OverlayNotification
29 + * @augments wp.customize.Notification
30 + *
31 + * @since 4.9.0
32 + *
33 + * @param {string} code - Code.
34 + * @param {Object} params - Params.
35 + */
36 + initialize: function( code, params ) {
37 + var notification = this;
38 + api.Notification.prototype.initialize.call( notification, code, params );
39 + notification.containerClasses += ' notification-overlay';
40 + if ( notification.loading ) {
41 + notification.containerClasses += ' notification-loading';
42 + }
43 + },
44 +
45 + /**
46 + * Render notification.
47 + *
48 + * @since 4.9.0
49 + *
50 + * @return {jQuery} Notification container.
51 + */
52 + render: function() {
53 + var li = api.Notification.prototype.render.call( this );
54 + li.on( 'keydown', _.bind( this.handleEscape, this ) );
55 + return li;
56 + },
57 +
58 + /**
59 + * Stop propagation on escape key presses, but also dismiss notification if it is dismissible.
60 + *
61 + * @since 4.9.0
62 + *
63 + * @param {jQuery.Event} event - Event.
64 + * @return {void}
65 + */
66 + handleEscape: function( event ) {
67 + var notification = this;
68 + if ( 27 === event.which ) {
69 + event.stopPropagation();
70 + if ( notification.dismissible && notification.parent ) {
71 + notification.parent.remove( notification.code );
72 + }
73 + }
74 + }
75 + });
76 +
77 + api.Notifications = api.Values.extend(/** @lends wp.customize.Notifications.prototype */{
78 +
79 + /**
80 + * Whether the alternative style should be used.
81 + *
82 + * @since 4.9.0
83 + * @type {boolean}
84 + */
85 + alt: false,
86 +
87 + /**
88 + * The default constructor for items of the collection.
89 + *
90 + * @since 4.9.0
91 + * @type {object}
92 + */
93 + defaultConstructor: api.Notification,
94 +
95 + /**
96 + * A collection of observable notifications.
97 + *
98 + * @since 4.9.0
99 + *
100 + * @constructs wp.customize.Notifications
101 + * @augments wp.customize.Values
102 + *
103 + * @param {Object} options - Options.
104 + * @param {jQuery} [options.container] - Container element for notifications. This can be injected later.
105 + * @param {boolean} [options.alt] - Whether alternative style should be used when rendering notifications.
106 + *
107 + * @return {void}
108 + */
109 + initialize: function( options ) {
110 + var collection = this;
111 +
112 + api.Values.prototype.initialize.call( collection, options );
113 +
114 + _.bindAll( collection, 'constrainFocus' );
115 +
116 + // Keep track of the order in which the notifications were added for sorting purposes.
117 + collection._addedIncrement = 0;
118 + collection._addedOrder = {};
119 +
120 + // Trigger change event when notification is added or removed.
121 + collection.bind( 'add', function( notification ) {
122 + collection.trigger( 'change', notification );
123 + });
124 + collection.bind( 'removed', function( notification ) {
125 + collection.trigger( 'change', notification );
126 + });
127 + },
128 +
129 + /**
130 + * Get the number of notifications added.
131 + *
132 + * @since 4.9.0
133 + * @return {number} Count of notifications.
134 + */
135 + count: function() {
136 + return _.size( this._value );
137 + },
138 +
139 + /**
140 + * Add notification to the collection.
141 + *
142 + * @since 4.9.0
143 + *
144 + * @param {string|wp.customize.Notification} notification - Notification object to add. Alternatively code may be supplied, and in that case the second notificationObject argument must be supplied.
145 + * @param {wp.customize.Notification} [notificationObject] - Notification to add when first argument is the code string.
146 + * @return {wp.customize.Notification} Added notification (or existing instance if it was already added).
147 + */
148 + add: function( notification, notificationObject ) {
149 + var collection = this, code, instance;
150 + if ( 'string' === typeof notification ) {
151 + code = notification;
152 + instance = notificationObject;
153 + } else {
154 + code = notification.code;
155 + instance = notification;
156 + }
157 + if ( ! collection.has( code ) ) {
158 + collection._addedIncrement += 1;
159 + collection._addedOrder[ code ] = collection._addedIncrement;
160 + }
161 + return api.Values.prototype.add.call( collection, code, instance );
162 + },
163 +
164 + /**
165 + * Add notification to the collection.
166 + *
167 + * @since 4.9.0
168 + * @param {string} code - Notification code to remove.
169 + * @return {api.Notification} Added instance (or existing instance if it was already added).
170 + */
171 + remove: function( code ) {
172 + var collection = this;
173 + delete collection._addedOrder[ code ];
174 + return api.Values.prototype.remove.call( this, code );
175 + },
176 +
177 + /**
178 + * Get list of notifications.
179 + *
180 + * Notifications may be sorted by type followed by added time.
181 + *
182 + * @since 4.9.0
183 + * @param {Object} args - Args.
184 + * @param {boolean} [args.sort=false] - Whether to return the notifications sorted.
185 + * @return {Array.<wp.customize.Notification>} Notifications.
186 + */
187 + get: function( args ) {
188 + var collection = this, notifications, errorTypePriorities, params;
189 + notifications = _.values( collection._value );
190 +
191 + params = _.extend(
192 + { sort: false },
193 + args
194 + );
195 +
196 + if ( params.sort ) {
197 + errorTypePriorities = { error: 4, warning: 3, success: 2, info: 1 };
198 + notifications.sort( function( a, b ) {
199 + var aPriority = 0, bPriority = 0;
200 + if ( ! _.isUndefined( errorTypePriorities[ a.type ] ) ) {
201 + aPriority = errorTypePriorities[ a.type ];
202 + }
203 + if ( ! _.isUndefined( errorTypePriorities[ b.type ] ) ) {
204 + bPriority = errorTypePriorities[ b.type ];
205 + }
206 + if ( aPriority !== bPriority ) {
207 + return bPriority - aPriority; // Show errors first.
208 + }
209 + return collection._addedOrder[ b.code ] - collection._addedOrder[ a.code ]; // Show newer notifications higher.
210 + });
211 + }
212 +
213 + return notifications;
214 + },
215 +
216 + /**
217 + * Render notifications area.
218 + *
219 + * @since 4.9.0
220 + * @return {void}
221 + */
222 + render: function() {
223 + var collection = this,
224 + notifications, hadOverlayNotification = false, hasOverlayNotification, overlayNotifications = [],
225 + previousNotificationsByCode = {},
226 + listElement, focusableElements;
227 +
228 + // Short-circuit if there are no container to render into.
229 + if ( ! collection.container || ! collection.container.length ) {
230 + return;
231 + }
232 +
233 + notifications = collection.get( { sort: true } );
234 + collection.container.toggle( 0 !== notifications.length );
235 +
236 + // Short-circuit if there are no changes to the notifications.
237 + if ( collection.container.is( collection.previousContainer ) && _.isEqual( notifications, collection.previousNotifications ) ) {
238 + return;
239 + }
240 +
241 + // Make sure list is part of the container.
242 + listElement = collection.container.children( 'ul' ).first();
243 + if ( ! listElement.length ) {
244 + listElement = $( '<ul></ul>' );
245 + collection.container.append( listElement );
246 + }
247 +
248 + // Remove all notifications prior to re-rendering.
249 + listElement.find( '> [data-code]' ).remove();
250 +
251 + _.each( collection.previousNotifications, function( notification ) {
252 + previousNotificationsByCode[ notification.code ] = notification;
253 + });
254 +
255 + // Add all notifications in the sorted order.
256 + _.each( notifications, function( notification ) {
257 + var notificationContainer;
258 + if ( wp.a11y && ( ! previousNotificationsByCode[ notification.code ] || ! _.isEqual( notification.message, previousNotificationsByCode[ notification.code ].message ) ) ) {
259 + wp.a11y.speak( notification.message, 'assertive' );
260 + }
261 + notificationContainer = $( notification.render() );
262 + notification.container = notificationContainer;
263 + listElement.append( notificationContainer ); // @todo Consider slideDown() as enhancement.
264 +
265 + if ( notification.extended( api.OverlayNotification ) ) {
266 + overlayNotifications.push( notification );
267 + }
268 + });
269 + hasOverlayNotification = Boolean( overlayNotifications.length );
270 +
271 + if ( collection.previousNotifications ) {
272 + hadOverlayNotification = Boolean( _.find( collection.previousNotifications, function( notification ) {
273 + return notification.extended( api.OverlayNotification );
274 + } ) );
275 + }
276 +
277 + if ( hasOverlayNotification !== hadOverlayNotification ) {
278 + $( document.body ).toggleClass( 'customize-loading', hasOverlayNotification );
279 + collection.container.toggleClass( 'has-overlay-notifications', hasOverlayNotification );
280 + if ( hasOverlayNotification ) {
281 + collection.previousActiveElement = document.activeElement;
282 + $( document ).on( 'keydown', collection.constrainFocus );
283 + } else {
284 + $( document ).off( 'keydown', collection.constrainFocus );
285 + }
286 + }
287 +
288 + if ( hasOverlayNotification ) {
289 + collection.focusContainer = overlayNotifications[ overlayNotifications.length - 1 ].container;
290 + collection.focusContainer.prop( 'tabIndex', -1 );
291 + focusableElements = collection.focusContainer.find( ':focusable' );
292 + if ( focusableElements.length ) {
293 + focusableElements.first().focus();
294 + } else {
295 + collection.focusContainer.focus();
296 + }
297 + } else if ( collection.previousActiveElement ) {
298 + $( collection.previousActiveElement ).trigger( 'focus' );
299 + collection.previousActiveElement = null;
300 + }
301 +
302 + collection.previousNotifications = notifications;
303 + collection.previousContainer = collection.container;
304 + collection.trigger( 'rendered' );
305 + },
306 +
307 + /**
308 + * Constrain focus on focus container.
309 + *
310 + * @since 4.9.0
311 + *
312 + * @param {jQuery.Event} event - Event.
313 + * @return {void}
314 + */
315 + constrainFocus: function constrainFocus( event ) {
316 + var collection = this, focusableElements;
317 +
318 + // Prevent keys from escaping.
319 + event.stopPropagation();
320 +
321 + if ( 9 !== event.which ) { // Tab key.
322 + return;
323 + }
324 +
325 + focusableElements = collection.focusContainer.find( ':focusable' );
326 + if ( 0 === focusableElements.length ) {
327 + focusableElements = collection.focusContainer;
328 + }
329 +
330 + if ( ! $.contains( collection.focusContainer[0], event.target ) || ! $.contains( collection.focusContainer[0], document.activeElement ) ) {
331 + event.preventDefault();
332 + focusableElements.first().focus();
333 + } else if ( focusableElements.last().is( event.target ) && ! event.shiftKey ) {
334 + event.preventDefault();
335 + focusableElements.first().focus();
336 + } else if ( focusableElements.first().is( event.target ) && event.shiftKey ) {
337 + event.preventDefault();
338 + focusableElements.last().focus();
339 + }
340 + }
341 + });
342 +
343 + api.Setting = api.Value.extend(/** @lends wp.customize.Setting.prototype */{
344 +
345 + /**
346 + * Default params.
347 + *
348 + * @since 4.9.0
349 + * @var {object}
350 + */
351 + defaults: {
352 + transport: 'refresh',
353 + dirty: false
354 + },
355 +
356 + /**
357 + * A Customizer Setting.
358 + *
359 + * A setting is WordPress data (theme mod, option, menu, etc.) that the user can
360 + * draft changes to in the Customizer.
361 + *
362 + * @see PHP class WP_Customize_Setting.
363 + *
364 + * @constructs wp.customize.Setting
365 + * @augments wp.customize.Value
366 + *
367 + * @since 3.4.0
368 + *
369 + * @param {string} id - The setting ID.
370 + * @param {*} value - The initial value of the setting.
371 + * @param {Object} [options={}] - Options.
372 + * @param {string} [options.transport=refresh] - The transport to use for previewing. Supports 'refresh' and 'postMessage'.
373 + * @param {boolean} [options.dirty=false] - Whether the setting should be considered initially dirty.
374 + * @param {Object} [options.previewer] - The Previewer instance to sync with. Defaults to wp.customize.previewer.
375 + */
376 + initialize: function( id, value, options ) {
377 + var setting = this, params;
378 + params = _.extend(
379 + { previewer: api.previewer },
380 + setting.defaults,
381 + options || {}
382 + );
383 +
384 + api.Value.prototype.initialize.call( setting, value, params );
385 +
386 + setting.id = id;
387 + setting._dirty = params.dirty; // The _dirty property is what the Customizer reads from.
388 + setting.notifications = new api.Notifications();
389 +
390 + // Whenever the setting's value changes, refresh the preview.
391 + setting.bind( setting.preview );
392 + },
393 +
394 + /**
395 + * Refresh the preview, respective of the setting's refresh policy.
396 + *
397 + * If the preview hasn't sent a keep-alive message and is likely
398 + * disconnected by having navigated to a non-allowed URL, then the
399 + * refresh transport will be forced when postMessage is the transport.
400 + * Note that postMessage does not throw an error when the recipient window
401 + * fails to match the origin window, so using try/catch around the
402 + * previewer.send() call to then fallback to refresh will not work.
403 + *
404 + * @since 3.4.0
405 + * @access public
406 + *
407 + * @return {void}
408 + */
409 + preview: function() {
410 + var setting = this, transport;
411 + transport = setting.transport;
412 +
413 + if ( 'postMessage' === transport && ! api.state( 'previewerAlive' ).get() ) {
414 + transport = 'refresh';
415 + }
416 +
417 + if ( 'postMessage' === transport ) {
418 + setting.previewer.send( 'setting', [ setting.id, setting() ] );
419 + } else if ( 'refresh' === transport ) {
420 + setting.previewer.refresh();
421 + }
422 + },
423 +
424 + /**
425 + * Find controls associated with this setting.
426 + *
427 + * @since 4.6.0
428 + * @return {wp.customize.Control[]} Controls associated with setting.
429 + */
430 + findControls: function() {
431 + var setting = this, controls = [];
432 + api.control.each( function( control ) {
433 + _.each( control.settings, function( controlSetting ) {
434 + if ( controlSetting.id === setting.id ) {
435 + controls.push( control );
436 + }
437 + } );
438 + } );
439 + return controls;
440 + }
441 + });
442 +
443 + /**
444 + * Current change count.
445 + *
446 + * @alias wp.customize._latestRevision
447 + *
448 + * @since 4.7.0
449 + * @type {number}
450 + * @protected
451 + */
452 + api._latestRevision = 0;
453 +
454 + /**
455 + * Last revision that was saved.
456 + *
457 + * @alias wp.customize._lastSavedRevision
458 + *
459 + * @since 4.7.0
460 + * @type {number}
461 + * @protected
462 + */
463 + api._lastSavedRevision = 0;
464 +
465 + /**
466 + * Latest revisions associated with the updated setting.
467 + *
468 + * @alias wp.customize._latestSettingRevisions
469 + *
470 + * @since 4.7.0
471 + * @type {object}
472 + * @protected
473 + */
474 + api._latestSettingRevisions = {};
475 +
476 + /*
477 + * Keep track of the revision associated with each updated setting so that
478 + * requestChangesetUpdate knows which dirty settings to include. Also, once
479 + * ready is triggered and all initial settings have been added, increment
480 + * revision for each newly-created initially-dirty setting so that it will
481 + * also be included in changeset update requests.
482 + */
483 + api.bind( 'change', function incrementChangedSettingRevision( setting ) {
484 + api._latestRevision += 1;
485 + api._latestSettingRevisions[ setting.id ] = api._latestRevision;
486 + } );
487 + api.bind( 'ready', function() {
488 + api.bind( 'add', function incrementCreatedSettingRevision( setting ) {
489 + if ( setting._dirty ) {
490 + api._latestRevision += 1;
491 + api._latestSettingRevisions[ setting.id ] = api._latestRevision;
492 + }
493 + } );
494 + } );
495 +
496 + /**
497 + * Get the dirty setting values.
498 + *
499 + * @alias wp.customize.dirtyValues
500 + *
501 + * @since 4.7.0
502 + * @access public
503 + *
504 + * @param {Object} [options] Options.
505 + * @param {boolean} [options.unsaved=false] Whether only values not saved yet into a changeset will be returned (differential changes).
506 + * @return {Object} Dirty setting values.
507 + */
508 + api.dirtyValues = function dirtyValues( options ) {
509 + var values = {};
510 + api.each( function( setting ) {
511 + var settingRevision;
512 +
513 + if ( ! setting._dirty ) {
514 + return;
515 + }
516 +
517 + settingRevision = api._latestSettingRevisions[ setting.id ];
518 +
519 + // Skip including settings that have already been included in the changeset, if only requesting unsaved.
520 + if ( api.state( 'changesetStatus' ).get() && ( options && options.unsaved ) && ( _.isUndefined( settingRevision ) || settingRevision <= api._lastSavedRevision ) ) {
521 + return;
522 + }
523 +
524 + values[ setting.id ] = setting.get();
525 + } );
526 + return values;
527 + };
528 +
529 + /**
530 + * Request updates to the changeset.
531 + *
532 + * @alias wp.customize.requestChangesetUpdate
533 + *
534 + * @since 4.7.0
535 + * @access public
536 + *
537 + * @param {Object} [changes] - Mapping of setting IDs to setting params each normally including a value property, or mapping to null.
538 + * If not provided, then the changes will still be obtained from unsaved dirty settings.
539 + * @param {Object} [args] - Additional options for the save request.
540 + * @param {boolean} [args.autosave=false] - Whether changes will be stored in autosave revision if the changeset has been promoted from an auto-draft.
541 + * @param {boolean} [args.force=false] - Send request to update even when there are no changes to submit. This can be used to request the latest status of the changeset on the server.
542 + * @param {string} [args.title] - Title to update in the changeset. Optional.
543 + * @param {string} [args.date] - Date to update in the changeset. Optional.
544 + * @return {jQuery.Promise} Promise resolving with the response data.
545 + */
546 + api.requestChangesetUpdate = function requestChangesetUpdate( changes, args ) {
547 + var deferred, request, submittedChanges = {}, data, submittedArgs;
548 + deferred = new $.Deferred();
549 +
550 + // Prevent attempting changeset update while request is being made.
551 + if ( 0 !== api.state( 'processing' ).get() ) {
552 + deferred.reject( 'already_processing' );
553 + return deferred.promise();
554 + }
555 +
556 + submittedArgs = _.extend( {
557 + title: null,
558 + date: null,
559 + autosave: false,
560 + force: false
561 + }, args );
562 +
563 + if ( changes ) {
564 + _.extend( submittedChanges, changes );
565 + }
566 +
567 + // Ensure all revised settings (changes pending save) are also included, but not if marked for deletion in changes.
568 + _.each( api.dirtyValues( { unsaved: true } ), function( dirtyValue, settingId ) {
569 + if ( ! changes || null !== changes[ settingId ] ) {
570 + submittedChanges[ settingId ] = _.extend(
571 + {},
572 + submittedChanges[ settingId ] || {},
573 + { value: dirtyValue }
574 + );
575 + }
576 + } );
577 +
578 + // Allow plugins to attach additional params to the settings.
579 + api.trigger( 'changeset-save', submittedChanges, submittedArgs );
580 +
581 + // Short-circuit when there are no pending changes.
582 + if ( ! submittedArgs.force && _.isEmpty( submittedChanges ) && null === submittedArgs.title && null === submittedArgs.date ) {
583 + deferred.resolve( {} );
584 + return deferred.promise();
585 + }
586 +
587 + // A status would cause a revision to be made, and for this wp.customize.previewer.save() should be used.
588 + // Status is also disallowed for revisions regardless.
589 + if ( submittedArgs.status ) {
590 + return deferred.reject( { code: 'illegal_status_in_changeset_update' } ).promise();
591 + }
592 +
593 + // Dates not beung allowed for revisions are is a technical limitation of post revisions.
594 + if ( submittedArgs.date && submittedArgs.autosave ) {
595 + return deferred.reject( { code: 'illegal_autosave_with_date_gmt' } ).promise();
596 + }
597 +
598 + // Make sure that publishing a changeset waits for all changeset update requests to complete.
599 + api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
600 + deferred.always( function() {
601 + api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
602 + } );
603 +
604 + // Ensure that if any plugins add data to save requests by extending query() that they get included here.
605 + data = api.previewer.query( { excludeCustomizedSaved: true } );
606 + delete data.customized; // Being sent in customize_changeset_data instead.
607 + _.extend( data, {
608 + nonce: api.settings.nonce.save,
609 + customize_theme: api.settings.theme.stylesheet,
610 + customize_changeset_data: JSON.stringify( submittedChanges )
611 + } );
612 + if ( null !== submittedArgs.title ) {
613 + data.customize_changeset_title = submittedArgs.title;
614 + }
615 + if ( null !== submittedArgs.date ) {
616 + data.customize_changeset_date = submittedArgs.date;
617 + }
618 + if ( false !== submittedArgs.autosave ) {
619 + data.customize_changeset_autosave = 'true';
620 + }
621 +
622 + // Allow plugins to modify the params included with the save request.
623 + api.trigger( 'save-request-params', data );
624 +
625 + request = wp.ajax.post( 'customize_save', data );
626 +
627 + request.done( function requestChangesetUpdateDone( data ) {
628 + var savedChangesetValues = {};
629 +
630 + // Ensure that all settings updated subsequently will be included in the next changeset update request.
631 + api._lastSavedRevision = Math.max( api._latestRevision, api._lastSavedRevision );
632 +
633 + api.state( 'changesetStatus' ).set( data.changeset_status );
634 +
635 + if ( data.changeset_date ) {
636 + api.state( 'changesetDate' ).set( data.changeset_date );
637 + }
638 +
639 + deferred.resolve( data );
640 + api.trigger( 'changeset-saved', data );
641 +
642 + if ( data.setting_validities ) {
643 + _.each( data.setting_validities, function( validity, settingId ) {
644 + if ( true === validity && _.isObject( submittedChanges[ settingId ] ) && ! _.isUndefined( submittedChanges[ settingId ].value ) ) {
645 + savedChangesetValues[ settingId ] = submittedChanges[ settingId ].value;
646 + }
647 + } );
648 + }
649 +
650 + api.previewer.send( 'changeset-saved', _.extend( {}, data, { saved_changeset_values: savedChangesetValues } ) );
651 + } );
652 + request.fail( function requestChangesetUpdateFail( data ) {
653 + deferred.reject( data );
654 + api.trigger( 'changeset-error', data );
655 + } );
656 + request.always( function( data ) {
657 + if ( data.setting_validities ) {
658 + api._handleSettingValidities( {
659 + settingValidities: data.setting_validities
660 + } );
661 + }
662 + } );
663 +
664 + return deferred.promise();
665 + };
666 +
667 + /**
668 + * Watch all changes to Value properties, and bubble changes to parent Values instance
669 + *
670 + * @alias wp.customize.utils.bubbleChildValueChanges
671 + *
672 + * @since 4.1.0
673 + *
674 + * @param {wp.customize.Class} instance
675 + * @param {Array} properties The names of the Value instances to watch.
676 + */
677 + api.utils.bubbleChildValueChanges = function ( instance, properties ) {
678 + $.each( properties, function ( i, key ) {
679 + instance[ key ].bind( function ( to, from ) {
680 + if ( instance.parent && to !== from ) {
681 + instance.parent.trigger( 'change', instance );
682 + }
683 + } );
684 + } );
685 + };
686 +
687 + /**
688 + * Expand a panel, section, or control and focus on the first focusable element.
689 + *
690 + * @alias wp.customize~focus
691 + *
692 + * @since 4.1.0
693 + *
694 + * @param {Object} [params]
695 + * @param {Function} [params.completeCallback]
696 + */
697 + focus = function ( params ) {
698 + var construct, completeCallback, focus, focusElement, sections;
699 + construct = this;
700 + params = params || {};
701 + focus = function () {
702 + // If a child section is currently expanded, collapse it.
703 + if ( construct.extended( api.Panel ) ) {
704 + sections = construct.sections();
705 + if ( 1 < sections.length ) {
706 + sections.forEach( function ( section ) {
707 + if ( section.expanded() ) {
708 + section.collapse();
709 + }
710 + } );
711 + }
712 + }
713 +
714 + var focusContainer;
715 + if ( ( construct.extended( api.Panel ) || construct.extended( api.Section ) ) && construct.expanded && construct.expanded() ) {
716 + focusContainer = construct.contentContainer;
717 + } else {
718 + focusContainer = construct.container;
719 + }
720 +
721 + focusElement = focusContainer.find( '.control-focus:first' );
722 + if ( 0 === focusElement.length ) {
723 + // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
724 + focusElement = focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first();
725 + }
726 + focusElement.focus();
727 + };
728 + if ( params.completeCallback ) {
729 + completeCallback = params.completeCallback;
730 + params.completeCallback = function () {
731 + focus();
732 + completeCallback();
733 + };
734 + } else {
735 + params.completeCallback = focus;
736 + }
737 +
738 + api.state( 'paneVisible' ).set( true );
739 + if ( construct.expand ) {
740 + construct.expand( params );
741 + } else {
742 + params.completeCallback();
743 + }
744 + };
745 +
746 + /**
747 + * Stable sort for Panels, Sections, and Controls.
748 + *
749 + * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
750 + *
751 + * @alias wp.customize.utils.prioritySort
752 + *
753 + * @since 4.1.0
754 + *
755 + * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
756 + * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
757 + * @return {number}
758 + */
759 + api.utils.prioritySort = function ( a, b ) {
760 + if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
761 + return a.params.instanceNumber - b.params.instanceNumber;
762 + } else {
763 + return a.priority() - b.priority();
764 + }
765 + };
766 +
767 + /**
768 + * Return whether the supplied Event object is for a keydown event but not the Enter key.
769 + *
770 + * @alias wp.customize.utils.isKeydownButNotEnterEvent
771 + *
772 + * @since 4.1.0
773 + *
774 + * @param {jQuery.Event} event
775 + * @return {boolean}
776 + */
777 + api.utils.isKeydownButNotEnterEvent = function ( event ) {
778 + return ( 'keydown' === event.type && 13 !== event.which );
779 + };
780 +
781 + /**
782 + * Return whether the two lists of elements are the same and are in the same order.
783 + *
784 + * @alias wp.customize.utils.areElementListsEqual
785 + *
786 + * @since 4.1.0
787 + *
788 + * @param {Array|jQuery} listA
789 + * @param {Array|jQuery} listB
790 + * @return {boolean}
791 + */
792 + api.utils.areElementListsEqual = function ( listA, listB ) {
793 + var equal = (
794 + listA.length === listB.length && // If lists are different lengths, then naturally they are not equal.
795 + -1 === _.indexOf( _.map( // Are there any false values in the list returned by map?
796 + _.zip( listA, listB ), // Pair up each element between the two lists.
797 + function ( pair ) {
798 + return $( pair[0] ).is( pair[1] ); // Compare to see if each pair is equal.
799 + }
800 + ), false ) // Check for presence of false in map's return value.
801 + );
802 + return equal;
803 + };
804 +
805 + /**
806 + * Highlight the existence of a button.
807 + *
808 + * This function reminds the user of a button represented by the specified
809 + * UI element, after an optional delay. If the user focuses the element
810 + * before the delay passes, the reminder is canceled.
811 + *
812 + * @alias wp.customize.utils.highlightButton
813 + *
814 + * @since 4.9.0
815 + *
816 + * @param {jQuery} button - The element to highlight.
817 + * @param {Object} [options] - Options.
818 + * @param {number} [options.delay=0] - Delay in milliseconds.
819 + * @param {jQuery} [options.focusTarget] - A target for user focus that defaults to the highlighted element.
820 + * If the user focuses the target before the delay passes, the reminder
821 + * is canceled. This option exists to accommodate compound buttons
822 + * containing auxiliary UI, such as the Publish button augmented with a
823 + * Settings button.
824 + * @return {Function} An idempotent function that cancels the reminder.
825 + */
826 + api.utils.highlightButton = function highlightButton( button, options ) {
827 + var animationClass = 'button-see-me',
828 + canceled = false,
829 + params;
830 +
831 + params = _.extend(
832 + {
833 + delay: 0,
834 + focusTarget: button
835 + },
836 + options
837 + );
838 +
839 + function cancelReminder() {
840 + canceled = true;
841 + }
842 +
843 + params.focusTarget.on( 'focusin', cancelReminder );
844 + setTimeout( function() {
845 + params.focusTarget.off( 'focusin', cancelReminder );
846 +
847 + if ( ! canceled ) {
848 + button.addClass( animationClass );
849 + button.one( 'animationend', function() {
850 + /*
851 + * Remove animation class to avoid situations in Customizer where
852 + * DOM nodes are moved (re-inserted) and the animation repeats.
853 + */
854 + button.removeClass( animationClass );
855 + } );
856 + }
857 + }, params.delay );
858 +
859 + return cancelReminder;
860 + };
861 +
862 + /**
863 + * Get current timestamp adjusted for server clock time.
864 + *
865 + * Same functionality as the `current_time( 'mysql', false )` function in PHP.
866 + *
867 + * @alias wp.customize.utils.getCurrentTimestamp
868 + *
869 + * @since 4.9.0
870 + *
871 + * @return {number} Current timestamp.
872 + */
873 + api.utils.getCurrentTimestamp = function getCurrentTimestamp() {
874 + var currentDate, currentClientTimestamp, timestampDifferential;
875 + currentClientTimestamp = _.now();
876 + currentDate = new Date( api.settings.initialServerDate.replace( /-/g, '/' ) );
877 + timestampDifferential = currentClientTimestamp - api.settings.initialClientTimestamp;
878 + timestampDifferential += api.settings.initialClientTimestamp - api.settings.initialServerTimestamp;
879 + currentDate.setTime( currentDate.getTime() + timestampDifferential );
880 + return currentDate.getTime();
881 + };
882 +
883 + /**
884 + * Get remaining time of when the date is set.
885 + *
886 + * @alias wp.customize.utils.getRemainingTime
887 + *
888 + * @since 4.9.0
889 + *
890 + * @param {string|number|Date} datetime - Date time or timestamp of the future date.
891 + * @return {number} remainingTime - Remaining time in milliseconds.
892 + */
893 + api.utils.getRemainingTime = function getRemainingTime( datetime ) {
894 + var millisecondsDivider = 1000, remainingTime, timestamp;
895 + if ( datetime instanceof Date ) {
896 + timestamp = datetime.getTime();
897 + } else if ( 'string' === typeof datetime ) {
898 + timestamp = ( new Date( datetime.replace( /-/g, '/' ) ) ).getTime();
899 + } else {
900 + timestamp = datetime;
901 + }
902 +
903 + remainingTime = timestamp - api.utils.getCurrentTimestamp();
904 + remainingTime = Math.ceil( remainingTime / millisecondsDivider );
905 + return remainingTime;
906 + };
907 +
908 + /**
909 + * Return browser supported `transitionend` event name.
910 + *
911 + * @since 4.7.0
912 + *
913 + * @ignore
914 + *
915 + * @return {string|null} Normalized `transitionend` event name or null if CSS transitions are not supported.
916 + */
917 + normalizedTransitionendEventName = (function () {
918 + var el, transitions, prop;
919 + el = document.createElement( 'div' );
920 + transitions = {
921 + 'transition' : 'transitionend',
922 + 'OTransition' : 'oTransitionEnd',
923 + 'MozTransition' : 'transitionend',
924 + 'WebkitTransition': 'webkitTransitionEnd'
925 + };
926 + prop = _.find( _.keys( transitions ), function( prop ) {
927 + return ! _.isUndefined( el.style[ prop ] );
928 + } );
929 + if ( prop ) {
930 + return transitions[ prop ];
931 + } else {
932 + return null;
933 + }
934 + })();
935 +
936 + Container = api.Class.extend(/** @lends wp.customize~Container.prototype */{
937 + defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
938 + defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
939 + containerType: 'container',
940 + defaults: {
941 + title: '',
942 + description: '',
943 + priority: 100,
944 + type: 'default',
945 + content: null,
946 + active: true,
947 + instanceNumber: null
948 + },
949 +
950 + /**
951 + * Base class for Panel and Section.
952 + *
953 + * @constructs wp.customize~Container
954 + * @augments wp.customize.Class
955 + *
956 + * @since 4.1.0
957 + *
958 + * @borrows wp.customize~focus as focus
959 + *
960 + * @param {string} id - The ID for the container.
961 + * @param {Object} options - Object containing one property: params.
962 + * @param {string} options.title - Title shown when panel is collapsed and expanded.
963 + * @param {string} [options.description] - Description shown at the top of the panel.
964 + * @param {number} [options.priority=100] - The sort priority for the panel.
965 + * @param {string} [options.templateId] - Template selector for container.
966 + * @param {string} [options.type=default] - The type of the panel. See wp.customize.panelConstructor.
967 + * @param {string} [options.content] - The markup to be used for the panel container. If empty, a JS template is used.
968 + * @param {boolean} [options.active=true] - Whether the panel is active or not.
969 + * @param {Object} [options.params] - Deprecated wrapper for the above properties.
970 + */
971 + initialize: function ( id, options ) {
972 + var container = this;
973 + container.id = id;
974 +
975 + if ( ! Container.instanceCounter ) {
976 + Container.instanceCounter = 0;
977 + }
978 + Container.instanceCounter++;
979 +
980 + $.extend( container, {
981 + params: _.defaults(
982 + options.params || options, // Passing the params is deprecated.
983 + container.defaults
984 + )
985 + } );
986 + if ( ! container.params.instanceNumber ) {
987 + container.params.instanceNumber = Container.instanceCounter;
988 + }
989 + container.notifications = new api.Notifications();
990 + container.templateSelector = container.params.templateId || 'customize-' + container.containerType + '-' + container.params.type;
991 + container.container = $( container.params.content );
992 + if ( 0 === container.container.length ) {
993 + container.container = $( container.getContainer() );
994 + }
995 + container.headContainer = container.container;
996 + container.contentContainer = container.getContent();
997 + container.container = container.container.add( container.contentContainer );
998 +
999 + container.deferred = {
1000 + embedded: new $.Deferred()
1001 + };
1002 + container.priority = new api.Value();
1003 + container.active = new api.Value();
1004 + container.activeArgumentsQueue = [];
1005 + container.expanded = new api.Value();
1006 + container.expandedArgumentsQueue = [];
1007 +
1008 + container.active.bind( function ( active ) {
1009 + var args = container.activeArgumentsQueue.shift();
1010 + args = $.extend( {}, container.defaultActiveArguments, args );
1011 + active = ( active && container.isContextuallyActive() );
1012 + container.onChangeActive( active, args );
1013 + });
1014 + container.expanded.bind( function ( expanded ) {
1015 + var args = container.expandedArgumentsQueue.shift();
1016 + args = $.extend( {}, container.defaultExpandedArguments, args );
1017 + container.onChangeExpanded( expanded, args );
1018 + });
1019 +
1020 + container.deferred.embedded.done( function () {
1021 + container.setupNotifications();
1022 + container.attachEvents();
1023 + });
1024 +
1025 + api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
1026 +
1027 + container.priority.set( container.params.priority );
1028 + container.active.set( container.params.active );
1029 + container.expanded.set( false );
1030 + },
1031 +
1032 + /**
1033 + * Get the element that will contain the notifications.
1034 + *
1035 + * @since 4.9.0
1036 + * @return {jQuery} Notification container element.
1037 + */
1038 + getNotificationsContainerElement: function() {
1039 + var container = this;
1040 + return container.contentContainer.find( '.customize-control-notifications-container:first' );
1041 + },
1042 +
1043 + /**
1044 + * Set up notifications.
1045 + *
1046 + * @since 4.9.0
1047 + * @return {void}
1048 + */
1049 + setupNotifications: function() {
1050 + var container = this, renderNotifications;
1051 + container.notifications.container = container.getNotificationsContainerElement();
1052 +
1053 + // Render notifications when they change and when the construct is expanded.
1054 + renderNotifications = function() {
1055 + if ( container.expanded.get() ) {
1056 + container.notifications.render();
1057 + }
1058 + };
1059 + container.expanded.bind( renderNotifications );
1060 + renderNotifications();
1061 + container.notifications.bind( 'change', _.debounce( renderNotifications ) );
1062 + },
1063 +
1064 + /**
1065 + * @since 4.1.0
1066 + *
1067 + * @abstract
1068 + */
1069 + ready: function() {},
1070 +
1071 + /**
1072 + * Get the child models associated with this parent, sorting them by their priority Value.
1073 + *
1074 + * @since 4.1.0
1075 + *
1076 + * @param {string} parentType
1077 + * @param {string} childType
1078 + * @return {Array}
1079 + */
1080 + _children: function ( parentType, childType ) {
1081 + var parent = this,
1082 + children = [];
1083 + api[ childType ].each( function ( child ) {
1084 + if ( child[ parentType ].get() === parent.id ) {
1085 + children.push( child );
1086 + }
1087 + } );
1088 + children.sort( api.utils.prioritySort );
1089 + return children;
1090 + },
1091 +
1092 + /**
1093 + * To override by subclass, to return whether the container has active children.
1094 + *
1095 + * @since 4.1.0
1096 + *
1097 + * @abstract
1098 + */
1099 + isContextuallyActive: function () {
1100 + throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
1101 + },
1102 +
1103 + /**
1104 + * Active state change handler.
1105 + *
1106 + * Shows the container if it is active, hides it if not.
1107 + *
1108 + * To override by subclass, update the container's UI to reflect the provided active state.
1109 + *
1110 + * @since 4.1.0
1111 + *
1112 + * @param {boolean} active - The active state to transiution to.
1113 + * @param {Object} [args] - Args.
1114 + * @param {Object} [args.duration] - The duration for the slideUp/slideDown animation.
1115 + * @param {boolean} [args.unchanged] - Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early.
1116 + * @param {Function} [args.completeCallback] - Function to call when the slideUp/slideDown has completed.
1117 + */
1118 + onChangeActive: function( active, args ) {
1119 + var construct = this,
1120 + headContainer = construct.headContainer,
1121 + duration, expandedOtherPanel;
1122 +
1123 + if ( args.unchanged ) {
1124 + if ( args.completeCallback ) {
1125 + args.completeCallback();
1126 + }
1127 + return;
1128 + }
1129 +
1130 + duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
1131 +
1132 + if ( construct.extended( api.Panel ) ) {
1133 + // If this is a panel is not currently expanded but another panel is expanded, do not animate.
1134 + api.panel.each(function ( panel ) {
1135 + if ( panel !== construct && panel.expanded() ) {
1136 + expandedOtherPanel = panel;
1137 + duration = 0;
1138 + }
1139 + });
1140 +
1141 + // Collapse any expanded sections inside of this panel first before deactivating.
1142 + if ( ! active ) {
1143 + _.each( construct.sections(), function( section ) {
1144 + section.collapse( { duration: 0 } );
1145 + } );
1146 + }
1147 + }
1148 +
1149 + if ( ! $.contains( document, headContainer.get( 0 ) ) ) {
1150 + // If the element is not in the DOM, then jQuery.fn.slideUp() does nothing.
1151 + // In this case, a hard toggle is required instead.
1152 + headContainer.toggle( active );
1153 + if ( args.completeCallback ) {
1154 + args.completeCallback();
1155 + }
1156 + } else if ( active ) {
1157 + headContainer.slideDown( duration, args.completeCallback );
1158 + } else {
1159 + if ( construct.expanded() ) {
1160 + construct.collapse({
1161 + duration: duration,
1162 + completeCallback: function() {
1163 + headContainer.slideUp( duration, args.completeCallback );
1164 + }
1165 + });
1166 + } else {
1167 + headContainer.slideUp( duration, args.completeCallback );
1168 + }
1169 + }
1170 + },
1171 +
1172 + /**
1173 + * @since 4.1.0
1174 + *
1175 + * @param {boolean} active
1176 + * @param {Object} [params]
1177 + * @return {boolean} False if state already applied.
1178 + */
1179 + _toggleActive: function ( active, params ) {
1180 + var self = this;
1181 + params = params || {};
1182 + if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
1183 + params.unchanged = true;
1184 + self.onChangeActive( self.active.get(), params );
1185 + return false;
1186 + } else {
1187 + params.unchanged = false;
1188 + this.activeArgumentsQueue.push( params );
1189 + this.active.set( active );
1190 + return true;
1191 + }
1192 + },
1193 +
1194 + /**
1195 + * @param {Object} [params]
1196 + * @return {boolean} False if already active.
1197 + */
1198 + activate: function ( params ) {
1199 + return this._toggleActive( true, params );
1200 + },
1201 +
1202 + /**
1203 + * @param {Object} [params]
1204 + * @return {boolean} False if already inactive.
1205 + */
1206 + deactivate: function ( params ) {
1207 + return this._toggleActive( false, params );
1208 + },
1209 +
1210 + /**
1211 + * To override by subclass, update the container's UI to reflect the provided active state.
1212 + * @abstract
1213 + */
1214 + onChangeExpanded: function () {
1215 + throw new Error( 'Must override with subclass.' );
1216 + },
1217 +
1218 + /**
1219 + * Handle the toggle logic for expand/collapse.
1220 + *
1221 + * @param {boolean} expanded - The new state to apply.
1222 + * @param {Object} [params] - Object containing options for expand/collapse.
1223 + * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete.
1224 + * @return {boolean} False if state already applied or active state is false.
1225 + */
1226 + _toggleExpanded: function( expanded, params ) {
1227 + var instance = this, previousCompleteCallback;
1228 + params = params || {};
1229 + previousCompleteCallback = params.completeCallback;
1230 +
1231 + // Short-circuit expand() if the instance is not active.
1232 + if ( expanded && ! instance.active() ) {
1233 + return false;
1234 + }
1235 +
1236 + api.state( 'paneVisible' ).set( true );
1237 + params.completeCallback = function() {
1238 + if ( previousCompleteCallback ) {
1239 + previousCompleteCallback.apply( instance, arguments );
1240 + }
1241 + if ( expanded ) {
1242 + instance.container.trigger( 'expanded' );
1243 + } else {
1244 + instance.container.trigger( 'collapsed' );
1245 + }
1246 + };
1247 + if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) {
1248 + params.unchanged = true;
1249 + instance.onChangeExpanded( instance.expanded.get(), params );
1250 + return false;
1251 + } else {
1252 + params.unchanged = false;
1253 + instance.expandedArgumentsQueue.push( params );
1254 + instance.expanded.set( expanded );
1255 + return true;
1256 + }
1257 + },
1258 +
1259 + /**
1260 + * @param {Object} [params]
1261 + * @return {boolean} False if already expanded or if inactive.
1262 + */
1263 + expand: function ( params ) {
1264 + return this._toggleExpanded( true, params );
1265 + },
1266 +
1267 + /**
1268 + * @param {Object} [params]
1269 + * @return {boolean} False if already collapsed.
1270 + */
1271 + collapse: function ( params ) {
1272 + return this._toggleExpanded( false, params );
1273 + },
1274 +
1275 + /**
1276 + * Animate container state change if transitions are supported by the browser.
1277 + *
1278 + * @since 4.7.0
1279 + * @private
1280 + *
1281 + * @param {function} completeCallback Function to be called after transition is completed.
1282 + * @return {void}
1283 + */
1284 + _animateChangeExpanded: function( completeCallback ) {
1285 + // Return if CSS transitions are not supported or if reduced motion is enabled.
1286 + if ( ! normalizedTransitionendEventName || isReducedMotion ) {
1287 + // Schedule the callback until the next tick to prevent focus loss.
1288 + _.defer( function () {
1289 + if ( completeCallback ) {
1290 + completeCallback();
1291 + }
1292 + } );
1293 + return;
1294 + }
1295 +
1296 + var construct = this,
1297 + content = construct.contentContainer,
1298 + overlay = content.closest( '.wp-full-overlay' ),
1299 + elements, transitionEndCallback, transitionParentPane;
1300 +
1301 + // Determine set of elements that are affected by the animation.
1302 + elements = overlay.add( content );
1303 +
1304 + if ( ! construct.panel || '' === construct.panel() ) {
1305 + transitionParentPane = true;
1306 + } else if ( api.panel( construct.panel() ).contentContainer.hasClass( 'skip-transition' ) ) {
1307 + transitionParentPane = true;
1308 + } else {
1309 + transitionParentPane = false;
1310 + }
1311 + if ( transitionParentPane ) {
1312 + elements = elements.add( '#customize-info, .customize-pane-parent' );
1313 + }
1314 +
1315 + // Handle `transitionEnd` event.
1316 + transitionEndCallback = function( e ) {
1317 + if ( 2 !== e.eventPhase || ! $( e.target ).is( content ) ) {
1318 + return;
1319 + }
1320 + content.off( normalizedTransitionendEventName, transitionEndCallback );
1321 + elements.removeClass( 'busy' );
1322 + if ( completeCallback ) {
1323 + completeCallback();
1324 + }
1325 + };
1326 + content.on( normalizedTransitionendEventName, transitionEndCallback );
1327 + elements.addClass( 'busy' );
1328 +
1329 + // Prevent screen flicker when pane has been scrolled before expanding.
1330 + _.defer( function() {
1331 + var container = content.closest( '.wp-full-overlay-sidebar-content' ),
1332 + currentScrollTop = container.scrollTop(),
1333 + previousScrollTop = content.data( 'previous-scrollTop' ) || 0,
1334 + expanded = construct.expanded();
1335 +
1336 + if ( expanded && 0 < currentScrollTop ) {
1337 + content.css( 'top', currentScrollTop + 'px' );
1338 + content.data( 'previous-scrollTop', currentScrollTop );
1339 + } else if ( ! expanded && 0 < currentScrollTop + previousScrollTop ) {
1340 + content.css( 'top', previousScrollTop - currentScrollTop + 'px' );
1341 + container.scrollTop( previousScrollTop );
1342 + }
1343 + } );
1344 + },
1345 +
1346 + /*
1347 + * is documented using @borrows in the constructor.
1348 + */
1349 + focus: focus,
1350 +
1351 + /**
1352 + * Return the container html, generated from its JS template, if it exists.
1353 + *
1354 + * @since 4.3.0
1355 + */
1356 + getContainer: function () {
1357 + var template,
1358 + container = this;
1359 +
1360 + if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) {
1361 + template = wp.template( container.templateSelector );
1362 + } else {
1363 + template = wp.template( 'customize-' + container.containerType + '-default' );
1364 + }
1365 + if ( template && container.container ) {
1366 + return template( _.extend(
1367 + { id: container.id },
1368 + container.params
1369 + ) ).toString().trim();
1370 + }
1371 +
1372 + return '<li></li>';
1373 + },
1374 +
1375 + /**
1376 + * Find content element which is displayed when the section is expanded.
1377 + *
1378 + * After a construct is initialized, the return value will be available via the `contentContainer` property.
1379 + * By default the element will be related it to the parent container with `aria-owns` and detached.
1380 + * Custom panels and sections (such as the `NewMenuSection`) that do not have a sliding pane should
1381 + * just return the content element without needing to add the `aria-owns` element or detach it from
1382 + * the container. Such non-sliding pane custom sections also need to override the `onChangeExpanded`
1383 + * method to handle animating the panel/section into and out of view.
1384 + *
1385 + * @since 4.7.0
1386 + * @access public
1387 + *
1388 + * @return {jQuery} Detached content element.
1389 + */
1390 + getContent: function() {
1391 + var construct = this,
1392 + container = construct.container,
1393 + content = container.find( '.accordion-section-content, .control-panel-content' ).first(),
1394 + contentId = 'sub-' + container.attr( 'id' ),
1395 + ownedElements = contentId,
1396 + alreadyOwnedElements = container.attr( 'aria-owns' );
1397 +
1398 + if ( alreadyOwnedElements ) {
1399 + ownedElements = ownedElements + ' ' + alreadyOwnedElements;
1400 + }
1401 + container.attr( 'aria-owns', ownedElements );
1402 +
1403 + return content.detach().attr( {
1404 + 'id': contentId,
1405 + 'class': 'customize-pane-child ' + content.attr( 'class' ) + ' ' + container.attr( 'class' )
1406 + } );
1407 + }
1408 + });
1409 +
1410 + api.Section = Container.extend(/** @lends wp.customize.Section.prototype */{
1411 + containerType: 'section',
1412 + containerParent: '#customize-theme-controls',
1413 + containerPaneParent: '.customize-pane-parent',
1414 + defaults: {
1415 + title: '',
1416 + description: '',
1417 + priority: 100,
1418 + type: 'default',
1419 + content: null,
1420 + active: true,
1421 + instanceNumber: null,
1422 + panel: null,
1423 + customizeAction: ''
1424 + },
1425 +
1426 + /**
1427 + * @constructs wp.customize.Section
1428 + * @augments wp.customize~Container
1429 + *
1430 + * @since 4.1.0
1431 + *
1432 + * @param {string} id - The ID for the section.
1433 + * @param {Object} options - Options.
1434 + * @param {string} options.title - Title shown when section is collapsed and expanded.
1435 + * @param {string} [options.description] - Description shown at the top of the section.
1436 + * @param {number} [options.priority=100] - The sort priority for the section.
1437 + * @param {string} [options.type=default] - The type of the section. See wp.customize.sectionConstructor.
1438 + * @param {string} [options.content] - The markup to be used for the section container. If empty, a JS template is used.
1439 + * @param {boolean} [options.active=true] - Whether the section is active or not.
1440 + * @param {string} options.panel - The ID for the panel this section is associated with.
1441 + * @param {string} [options.customizeAction] - Additional context information shown before the section title when expanded.
1442 + * @param {Object} [options.params] - Deprecated wrapper for the above properties.
1443 + */
1444 + initialize: function ( id, options ) {
1445 + var section = this, params;
1446 + params = options.params || options;
1447 +
1448 + // Look up the type if one was not supplied.
1449 + if ( ! params.type ) {
1450 + _.find( api.sectionConstructor, function( Constructor, type ) {
1451 + if ( Constructor === section.constructor ) {
1452 + params.type = type;
1453 + return true;
1454 + }
1455 + return false;
1456 + } );
1457 + }
1458 +
1459 + Container.prototype.initialize.call( section, id, params );
1460 +
1461 + section.id = id;
1462 + section.panel = new api.Value();
1463 + section.panel.bind( function ( id ) {
1464 + $( section.headContainer ).toggleClass( 'control-subsection', !! id );
1465 + });
1466 + section.panel.set( section.params.panel || '' );
1467 + api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
1468 +
1469 + section.embed();
1470 + section.deferred.embedded.done( function () {
1471 + section.ready();
1472 + });
1473 + },
1474 +
1475 + /**
1476 + * Embed the container in the DOM when any parent panel is ready.
1477 + *
1478 + * @since 4.1.0
1479 + */
1480 + embed: function () {
1481 + var inject,
1482 + section = this;
1483 +
1484 + section.containerParent = api.ensure( section.containerParent );
1485 +
1486 + // Watch for changes to the panel state.
1487 + inject = function ( panelId ) {
1488 + var parentContainer;
1489 + if ( panelId ) {
1490 + // The panel has been supplied, so wait until the panel object is registered.
1491 + api.panel( panelId, function ( panel ) {
1492 + // The panel has been registered, wait for it to become ready/initialized.
1493 + panel.deferred.embedded.done( function () {
1494 + parentContainer = panel.contentContainer;
1495 + if ( ! section.headContainer.parent().is( parentContainer ) ) {
1496 + parentContainer.append( section.headContainer );
1497 + }
1498 + if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
1499 + section.containerParent.append( section.contentContainer );
1500 + }
1501 + section.deferred.embedded.resolve();
1502 + });
1503 + } );
1504 + } else {
1505 + // There is no panel, so embed the section in the root of the customizer.
1506 + parentContainer = api.ensure( section.containerPaneParent );
1507 + if ( ! section.headContainer.parent().is( parentContainer ) ) {
1508 + parentContainer.append( section.headContainer );
1509 + }
1510 + if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
1511 + section.containerParent.append( section.contentContainer );
1512 + }
1513 + section.deferred.embedded.resolve();
1514 + }
1515 + };
1516 + section.panel.bind( inject );
1517 + inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one.
1518 + },
1519 +
1520 + /**
1521 + * Add behaviors for the accordion section.
1522 + *
1523 + * @since 4.1.0
1524 + */
1525 + attachEvents: function () {
1526 + var meta, content, section = this;
1527 +
1528 + if ( section.container.hasClass( 'cannot-expand' ) ) {
1529 + return;
1530 + }
1531 +
1532 + // Expand/Collapse accordion sections on click.
1533 + section.container.find( '.accordion-section-title button, .customize-section-back, .accordion-section-title[tabindex]' ).on( 'click keydown', function( event ) {
1534 + if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1535 + return;
1536 + }
1537 + event.preventDefault(); // Keep this AFTER the key filter above.
1538 +
1539 + if ( section.expanded() ) {
1540 + section.collapse();
1541 + } else {
1542 + section.expand();
1543 + }
1544 + });
1545 +
1546 + // This is very similar to what is found for api.Panel.attachEvents().
1547 + section.container.find( '.customize-section-title .customize-help-toggle' ).on( 'click', function() {
1548 +
1549 + meta = section.container.find( '.section-meta' );
1550 + if ( meta.hasClass( 'cannot-expand' ) ) {
1551 + return;
1552 + }
1553 + content = meta.find( '.customize-section-description:first' );
1554 + content.toggleClass( 'open' );
1555 + content.slideToggle( section.defaultExpandedArguments.duration, function() {
1556 + content.trigger( 'toggled' );
1557 + } );
1558 + $( this ).attr( 'aria-expanded', function( i, attr ) {
1559 + return 'true' === attr ? 'false' : 'true';
1560 + });
1561 + });
1562 + },
1563 +
1564 + /**
1565 + * Return whether this section has any active controls.
1566 + *
1567 + * @since 4.1.0
1568 + *
1569 + * @return {boolean}
1570 + */
1571 + isContextuallyActive: function () {
1572 + var section = this,
1573 + controls = section.controls(),
1574 + activeCount = 0;
1575 + _( controls ).each( function ( control ) {
1576 + if ( control.active() ) {
1577 + activeCount += 1;
1578 + }
1579 + } );
1580 + return ( activeCount !== 0 );
1581 + },
1582 +
1583 + /**
1584 + * Get the controls that are associated with this section, sorted by their priority Value.
1585 + *
1586 + * @since 4.1.0
1587 + *
1588 + * @return {Array}
1589 + */
1590 + controls: function () {
1591 + return this._children( 'section', 'control' );
1592 + },
1593 +
1594 + /**
1595 + * Update UI to reflect expanded state.
1596 + *
1597 + * @since 4.1.0
1598 + *
1599 + * @param {boolean} expanded
1600 + * @param {Object} args
1601 + */
1602 + onChangeExpanded: function ( expanded, args ) {
1603 + var section = this,
1604 + container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ),
1605 + content = section.contentContainer,
1606 + overlay = section.headContainer.closest( '.wp-full-overlay' ),
1607 + backBtn = content.find( '.customize-section-back' ),
1608 + sectionTitle = section.headContainer.find( '.accordion-section-title button, .accordion-section-title[tabindex]' ).first(),
1609 + expand, panel;
1610 +
1611 + if ( expanded && ! content.hasClass( 'open' ) ) {
1612 +
1613 + if ( args.unchanged ) {
1614 + expand = args.completeCallback;
1615 + } else {
1616 + expand = function() {
1617 + section._animateChangeExpanded( function() {
1618 + backBtn.attr( 'tabindex', '0' );
1619 + backBtn.trigger( 'focus' );
1620 + content.css( 'top', '' );
1621 + container.scrollTop( 0 );
1622 +
1623 + if ( args.completeCallback ) {
1624 + args.completeCallback();
1625 + }
1626 + } );
1627 +
1628 + content.addClass( 'open' );
1629 + overlay.addClass( 'section-open' );
1630 + api.state( 'expandedSection' ).set( section );
1631 + }.bind( this );
1632 + }
1633 +
1634 + if ( ! args.allowMultiple ) {
1635 + api.section.each( function ( otherSection ) {
1636 + if ( otherSection !== section ) {
1637 + otherSection.collapse( { duration: args.duration } );
1638 + }
1639 + });
1640 + }
1641 +
1642 + if ( section.panel() ) {
1643 + api.panel( section.panel() ).expand({
1644 + duration: args.duration,
1645 + completeCallback: expand
1646 + });
1647 + } else {
1648 + if ( ! args.allowMultiple ) {
1649 + api.panel.each( function( panel ) {
1650 + panel.collapse();
1651 + });
1652 + }
1653 + expand();
1654 + }
1655 +
1656 + } else if ( ! expanded && content.hasClass( 'open' ) ) {
1657 + if ( section.panel() ) {
1658 + panel = api.panel( section.panel() );
1659 + if ( panel.contentContainer.hasClass( 'skip-transition' ) ) {
1660 + panel.collapse();
1661 + }
1662 + }
1663 + section._animateChangeExpanded( function() {
1664 + backBtn.attr( 'tabindex', '-1' );
1665 + sectionTitle.trigger( 'focus' );
1666 + content.css( 'top', '' );
1667 +
1668 + if ( args.completeCallback ) {
1669 + args.completeCallback();
1670 + }
1671 + } );
1672 +
1673 + content.removeClass( 'open' );
1674 + overlay.removeClass( 'section-open' );
1675 + if ( section === api.state( 'expandedSection' ).get() ) {
1676 + api.state( 'expandedSection' ).set( false );
1677 + }
1678 +
1679 + } else {
1680 + if ( args.completeCallback ) {
1681 + args.completeCallback();
1682 + }
1683 + }
1684 + }
1685 + });
1686 +
1687 + api.ThemesSection = api.Section.extend(/** @lends wp.customize.ThemesSection.prototype */{
1688 + currentTheme: '',
1689 + overlay: '',
1690 + template: '',
1691 + screenshotQueue: null,
1692 + $window: null,
1693 + $body: null,
1694 + loaded: 0,
1695 + loading: false,
1696 + fullyLoaded: false,
1697 + term: '',
1698 + tags: '',
1699 + nextTerm: '',
1700 + nextTags: '',
1701 + filtersHeight: 0,
1702 + headerContainer: null,
1703 + updateCountDebounced: null,
1704 +
1705 + /**
1706 + * wp.customize.ThemesSection
1707 + *
1708 + * Custom section for themes that loads themes by category, and also
1709 + * handles the theme-details view rendering and navigation.
1710 + *
1711 + * @constructs wp.customize.ThemesSection
1712 + * @augments wp.customize.Section
1713 + *
1714 + * @since 4.9.0
1715 + *
1716 + * @param {string} id - ID.
1717 + * @param {Object} options - Options.
1718 + * @return {void}
1719 + */
1720 + initialize: function( id, options ) {
1721 + var section = this;
1722 + section.headerContainer = $();
1723 + section.$window = $( window );
1724 + section.$body = $( document.body );
1725 + api.Section.prototype.initialize.call( section, id, options );
1726 + section.updateCountDebounced = _.debounce( section.updateCount, 500 );
1727 + },
1728 +
1729 + /**
1730 + * Embed the section in the DOM when the themes panel is ready.
1731 + *
1732 + * Insert the section before the themes container. Assume that a themes section is within a panel, but not necessarily the themes panel.
1733 + *
1734 + * @since 4.9.0
1735 + */
1736 + embed: function() {
1737 + var inject,
1738 + section = this;
1739 +
1740 + // Watch for changes to the panel state.
1741 + inject = function( panelId ) {
1742 + var parentContainer;
1743 + api.panel( panelId, function( panel ) {
1744 +
1745 + // The panel has been registered, wait for it to become ready/initialized.
1746 + panel.deferred.embedded.done( function() {
1747 + parentContainer = panel.contentContainer;
1748 + if ( ! section.headContainer.parent().is( parentContainer ) ) {
1749 + parentContainer.find( '.customize-themes-full-container-container' ).before( section.headContainer );
1750 + }
1751 + if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
1752 + section.containerParent.append( section.contentContainer );
1753 + }
1754 + section.deferred.embedded.resolve();
1755 + });
1756 + } );
1757 + };
1758 + section.panel.bind( inject );
1759 + inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one.
1760 + },
1761 +
1762 + /**
1763 + * Set up.
1764 + *
1765 + * @since 4.2.0
1766 + *
1767 + * @return {void}
1768 + */
1769 + ready: function() {
1770 + var section = this;
1771 + section.overlay = section.container.find( '.theme-overlay' );
1772 + section.template = wp.template( 'customize-themes-details-view' );
1773 +
1774 + // Bind global keyboard events.
1775 + section.container.on( 'keydown', function( event ) {
1776 + if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
1777 + return;
1778 + }
1779 +
1780 + // Pressing the right arrow key fires a theme:next event.
1781 + if ( 39 === event.keyCode ) {
1782 + section.nextTheme();
1783 + }
1784 +
1785 + // Pressing the left arrow key fires a theme:previous event.
1786 + if ( 37 === event.keyCode ) {
1787 + section.previousTheme();
1788 + }
1789 +
1790 + // Pressing the escape key fires a theme:collapse event.
1791 + if ( 27 === event.keyCode ) {
1792 + if ( section.$body.hasClass( 'modal-open' ) ) {
1793 +
1794 + // Escape from the details modal.
1795 + section.closeDetails();
1796 + } else {
1797 +
1798 + // Escape from the infinite scroll list.
1799 + section.headerContainer.find( '.customize-themes-section-title' ).focus();
1800 + }
1801 + event.stopPropagation(); // Prevent section from being collapsed.
1802 + }
1803 + });
1804 +
1805 + section.renderScreenshots = _.throttle( section.renderScreenshots, 100 );
1806 +
1807 + _.bindAll( section, 'renderScreenshots', 'loadMore', 'checkTerm', 'filtersChecked' );
1808 + },
1809 +
1810 + /**
1811 + * Override Section.isContextuallyActive method.
1812 + *
1813 + * Ignore the active states' of the contained theme controls, and just
1814 + * use the section's own active state instead. This prevents empty search
1815 + * results for theme sections from causing the section to become inactive.
1816 + *
1817 + * @since 4.2.0
1818 + *
1819 + * @return {boolean}
1820 + */
1821 + isContextuallyActive: function () {
1822 + return this.active();
1823 + },
1824 +
1825 + /**
1826 + * Attach events.
1827 + *
1828 + * @since 4.2.0
1829 + *
1830 + * @return {void}
1831 + */
1832 + attachEvents: function () {
1833 + var section = this, debounced;
1834 +
1835 + // Expand/Collapse accordion sections on click.
1836 + section.container.find( '.customize-section-back' ).on( 'click keydown', function( event ) {
1837 + if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1838 + return;
1839 + }
1840 + event.preventDefault(); // Keep this AFTER the key filter above.
1841 + section.collapse();
1842 + });
1843 +
1844 + section.headerContainer = $( '#accordion-section-' + section.id );
1845 +
1846 + // Expand section/panel. Only collapse when opening another section.
1847 + section.headerContainer.on( 'click', '.customize-themes-section-title', function() {
1848 +
1849 + // Toggle accordion filters under section headers.
1850 + if ( section.headerContainer.find( '.filter-details' ).length ) {
1851 + section.headerContainer.find( '.customize-themes-section-title' )
1852 + .toggleClass( 'details-open' )
1853 + .attr( 'aria-expanded', function( i, attr ) {
1854 + return 'true' === attr ? 'false' : 'true';
1855 + });
1856 + section.headerContainer.find( '.filter-details' ).slideToggle( 180 );
1857 + }
1858 +
1859 + // Open the section.
1860 + if ( ! section.expanded() ) {
1861 + section.expand();
1862 + }
1863 + });
1864 +
1865 + // Preview installed themes.
1866 + section.container.on( 'click', '.theme-actions .preview-theme', function() {
1867 + api.panel( 'themes' ).loadThemePreview( $( this ).data( 'slug' ) );
1868 + });
1869 +
1870 + // Theme navigation in details view.
1871 + section.container.on( 'click', '.left', function() {
1872 + section.previousTheme();
1873 + });
1874 +
1875 + section.container.on( 'click', '.right', function() {
1876 + section.nextTheme();
1877 + });
1878 +
1879 + section.container.on( 'click', '.theme-backdrop, .close', function() {
1880 + section.closeDetails();
1881 + });
1882 +
1883 + if ( 'local' === section.params.filter_type ) {
1884 +
1885 + // Filter-search all theme objects loaded in the section.
1886 + section.container.on( 'input', '.wp-filter-search-themes', function( event ) {
1887 + section.filterSearch( event.currentTarget.value );
1888 + });
1889 +
1890 + } else if ( 'remote' === section.params.filter_type ) {
1891 +
1892 + // Event listeners for remote queries with user-entered terms.
1893 + // Search terms.
1894 + debounced = _.debounce( section.checkTerm, 500 ); // Wait until there is no input for 500 milliseconds to initiate a search.
1895 + section.contentContainer.on( 'input', '.wp-filter-search', function() {
1896 + if ( ! api.panel( 'themes' ).expanded() ) {
1897 + return;
1898 + }
1899 + debounced( section );
1900 + if ( ! section.expanded() ) {
1901 + section.expand();
1902 + }
1903 + });
1904 +
1905 + // Feature filters.
1906 + section.contentContainer.on( 'click', '.filter-group input', function() {
1907 + section.filtersChecked();
1908 + section.checkTerm( section );
1909 + });
1910 + }
1911 +
1912 + // Toggle feature filters.
1913 + section.contentContainer.on( 'click', '.feature-filter-toggle', function( e ) {
1914 + var $themeContainer = $( '.customize-themes-full-container' ),
1915 + $filterToggle = $( e.currentTarget );
1916 + section.filtersHeight = $filterToggle.parents( '.themes-filter-bar' ).next( '.filter-drawer' ).height();
1917 +
1918 + if ( 0 < $themeContainer.scrollTop() ) {
1919 + $themeContainer.animate( { scrollTop: 0 }, 400 );
1920 +
1921 + if ( $filterToggle.hasClass( 'open' ) ) {
1922 + return;
1923 + }
1924 + }
1925 +
1926 + $filterToggle
1927 + .toggleClass( 'open' )
1928 + .attr( 'aria-expanded', function( i, attr ) {
1929 + return 'true' === attr ? 'false' : 'true';
1930 + })
1931 + .parents( '.themes-filter-bar' ).next( '.filter-drawer' ).slideToggle( 180, 'linear' );
1932 +
1933 + if ( $filterToggle.hasClass( 'open' ) ) {
1934 + var marginOffset = 1018 < window.innerWidth ? 50 : 76;
1935 +
1936 + section.contentContainer.find( '.themes' ).css( 'margin-top', section.filtersHeight + marginOffset );
1937 + } else {
1938 + section.contentContainer.find( '.themes' ).css( 'margin-top', 0 );
1939 + }
1940 + });
1941 +
1942 + // Setup section cross-linking.
1943 + section.contentContainer.on( 'click', '.no-themes-local .search-dotorg-themes', function() {
1944 + api.section( 'wporg_themes' ).focus();
1945 + });
1946 +
1947 + function updateSelectedState() {
1948 + var el = section.headerContainer.find( '.customize-themes-section-title' );
1949 + el.toggleClass( 'selected', section.expanded() );
1950 + el.attr( 'aria-expanded', section.expanded() ? 'true' : 'false' );
1951 + if ( ! section.expanded() ) {
1952 + el.removeClass( 'details-open' );
1953 + }
1954 + }
1955 + section.expanded.bind( updateSelectedState );
1956 + updateSelectedState();
1957 +
1958 + // Move section controls to the themes area.
1959 + api.bind( 'ready', function () {
1960 + section.contentContainer = section.container.find( '.customize-themes-section' );
1961 + section.contentContainer.appendTo( $( '.customize-themes-full-container' ) );
1962 + section.container.add( section.headerContainer );
1963 + });
1964 + },
1965 +
1966 + /**
1967 + * Update UI to reflect expanded state
1968 + *
1969 + * @since 4.2.0
1970 + *
1971 + * @param {boolean} expanded
1972 + * @param {Object} args
1973 + * @param {boolean} args.unchanged
1974 + * @param {Function} args.completeCallback
1975 + * @return {void}
1976 + */
1977 + onChangeExpanded: function ( expanded, args ) {
1978 +
1979 + // Note: there is a second argument 'args' passed.
1980 + var section = this,
1981 + container = section.contentContainer.closest( '.customize-themes-full-container' );
1982 +
1983 + // Immediately call the complete callback if there were no changes.
1984 + if ( args.unchanged ) {
1985 + if ( args.completeCallback ) {
1986 + args.completeCallback();
1987 + }
1988 + return;
1989 + }
1990 +
1991 + function expand() {
1992 +
1993 + // Try to load controls if none are loaded yet.
1994 + if ( 0 === section.loaded ) {
1995 + section.loadThemes();
1996 + }
1997 +
1998 + // Collapse any sibling sections/panels.
1999 + api.section.each( function ( otherSection ) {
2000 + var searchTerm;
2001 +
2002 + if ( otherSection !== section ) {
2003 +
2004 + // Try to sync the current search term to the new section.
2005 + if ( 'themes' === otherSection.params.type ) {
2006 + searchTerm = otherSection.contentContainer.find( '.wp-filter-search' ).val();
2007 + section.contentContainer.find( '.wp-filter-search' ).val( searchTerm );
2008 +
2009 + // Directly initialize an empty remote search to avoid a race condition.
2010 + if ( '' === searchTerm && '' !== section.term && 'local' !== section.params.filter_type ) {
2011 + section.term = '';
2012 + section.initializeNewQuery( section.term, section.tags );
2013 + } else {
2014 + if ( 'remote' === section.params.filter_type ) {
2015 + section.checkTerm( section );
2016 + } else if ( 'local' === section.params.filter_type ) {
2017 + section.filterSearch( searchTerm );
2018 + }
2019 + }
2020 + otherSection.collapse( { duration: args.duration } );
2021 + }
2022 + }
2023 + });
2024 +
2025 + section.contentContainer.addClass( 'current-section' );
2026 + container.scrollTop();
2027 +
2028 + container.on( 'scroll', _.throttle( section.renderScreenshots, 300 ) );
2029 + container.on( 'scroll', _.throttle( section.loadMore, 300 ) );
2030 +
2031 + if ( args.completeCallback ) {
2032 + args.completeCallback();
2033 + }
2034 + section.updateCount(); // Show this section's count.
2035 + }
2036 +
2037 + if ( expanded ) {
2038 + if ( section.panel() && api.panel.has( section.panel() ) ) {
2039 + api.panel( section.panel() ).expand({
2040 + duration: args.duration,
2041 + completeCallback: expand
2042 + });
2043 + } else {
2044 + expand();
2045 + }
2046 + } else {
2047 + section.contentContainer.removeClass( 'current-section' );
2048 +
2049 + // Always hide, even if they don't exist or are already hidden.
2050 + section.headerContainer.find( '.filter-details' ).slideUp( 180 );
2051 +
2052 + container.off( 'scroll' );
2053 +
2054 + if ( args.completeCallback ) {
2055 + args.completeCallback();
2056 + }
2057 + }
2058 + },
2059 +
2060 + /**
2061 + * Return the section's content element without detaching from the parent.
2062 + *
2063 + * @since 4.9.0
2064 + *
2065 + * @return {jQuery}
2066 + */
2067 + getContent: function() {
2068 + return this.container.find( '.control-section-content' );
2069 + },
2070 +
2071 + /**
2072 + * Load theme data via Ajax and add themes to the section as controls.
2073 + *
2074 + * @since 4.9.0
2075 + *
2076 + * @return {void}
2077 + */
2078 + loadThemes: function() {
2079 + var section = this, params, page, request;
2080 +
2081 + if ( section.loading ) {
2082 + return; // We're already loading a batch of themes.
2083 + }
2084 +
2085 + // Parameters for every API query. Additional params are set in PHP.
2086 + page = Math.ceil( section.loaded / 100 ) + 1;
2087 + params = {
2088 + 'nonce': api.settings.nonce.switch_themes,
2089 + 'wp_customize': 'on',
2090 + 'theme_action': section.params.action,
2091 + 'customized_theme': api.settings.theme.stylesheet,
2092 + 'page': page
2093 + };
2094 +
2095 + // Add fields for remote filtering.
2096 + if ( 'remote' === section.params.filter_type ) {
2097 + params.search = section.term;
2098 + params.tags = section.tags;
2099 + }
2100 +
2101 + // Load themes.
2102 + section.headContainer.closest( '.wp-full-overlay' ).addClass( 'loading' );
2103 + section.loading = true;
2104 + section.container.find( '.no-themes' ).hide();
2105 + request = wp.ajax.post( 'customize_load_themes', params );
2106 + request.done(function( data ) {
2107 + var themes = data.themes;
2108 +
2109 + // Stop and try again if the term changed while loading.
2110 + if ( '' !== section.nextTerm || '' !== section.nextTags ) {
2111 + if ( section.nextTerm ) {
2112 + section.term = section.nextTerm;
2113 + }
2114 + if ( section.nextTags ) {
2115 + section.tags = section.nextTags;
2116 + }
2117 + section.nextTerm = '';
2118 + section.nextTags = '';
2119 + section.loading = false;
2120 + section.loadThemes();
2121 + return;
2122 + }
2123 +
2124 + if ( 0 !== themes.length ) {
2125 +
2126 + section.loadControls( themes, page );
2127 +
2128 + if ( 1 === page ) {
2129 +
2130 + // Pre-load the first 3 theme screenshots.
2131 + _.each( section.controls().slice( 0, 3 ), function( control ) {
2132 + var img, src = control.params.theme.screenshot[0];
2133 + if ( src ) {
2134 + img = new Image();
2135 + img.src = src;
2136 + }
2137 + });
2138 + if ( 'local' !== section.params.filter_type ) {
2139 + wp.a11y.speak( api.settings.l10n.themeSearchResults.replace( '%d', data.info.results ) );
2140 + }
2141 + }
2142 +
2143 + _.delay( section.renderScreenshots, 100 ); // Wait for the controls to become visible.
2144 +
2145 + if ( 'local' === section.params.filter_type || 100 > themes.length ) {
2146 + // If we have less than the requested 100 themes, it's the end of the list.
2147 + section.fullyLoaded = true;
2148 + }
2149 + } else {
2150 + if ( 0 === section.loaded ) {
2151 + section.container.find( '.no-themes' ).show();
2152 + wp.a11y.speak( section.container.find( '.no-themes' ).text() );
2153 + } else {
2154 + section.fullyLoaded = true;
2155 + }
2156 + }
2157 + if ( 'local' === section.params.filter_type ) {
2158 + section.updateCount(); // Count of visible theme controls.
2159 + } else {
2160 + section.updateCount( data.info.results ); // Total number of results including pages not yet loaded.
2161 + }
2162 + section.container.find( '.unexpected-error' ).hide(); // Hide error notice in case it was previously shown.
2163 +
2164 + // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
2165 + section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
2166 + section.loading = false;
2167 + });
2168 + request.fail(function( data ) {
2169 + if ( 'undefined' === typeof data ) {
2170 + section.container.find( '.unexpected-error' ).show();
2171 + wp.a11y.speak( section.container.find( '.unexpected-error' ).text() );
2172 + } else if ( 'undefined' !== typeof console && console.error ) {
2173 + console.error( data );
2174 + }
2175 +
2176 + // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
2177 + section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
2178 + section.loading = false;
2179 + });
2180 + },
2181 +
2182 + /**
2183 + * Loads controls into the section from data received from loadThemes().
2184 + *
2185 + * @since 4.9.0
2186 + * @param {Array} themes - Array of theme data to create controls with.
2187 + * @param {number} page - Page of results being loaded.
2188 + * @return {void}
2189 + */
2190 + loadControls: function( themes, page ) {
2191 + var newThemeControls = [],
2192 + section = this;
2193 +
2194 + // Add controls for each theme.
2195 + _.each( themes, function( theme ) {
2196 + var themeControl = new api.controlConstructor.theme( section.params.action + '_theme_' + theme.id, {
2197 + type: 'theme',
2198 + section: section.params.id,
2199 + theme: theme,
2200 + priority: section.loaded + 1
2201 + } );
2202 +
2203 + api.control.add( themeControl );
2204 + newThemeControls.push( themeControl );
2205 + section.loaded = section.loaded + 1;
2206 + });
2207 +
2208 + if ( 1 !== page ) {
2209 + Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue.
2210 + }
2211 + },
2212 +
2213 + /**
2214 + * Determines whether more themes should be loaded, and loads them.
2215 + *
2216 + * @since 4.9.0
2217 + * @return {void}
2218 + */
2219 + loadMore: function() {
2220 + var section = this, container, bottom, threshold;
2221 + if ( ! section.fullyLoaded && ! section.loading ) {
2222 + container = section.container.closest( '.customize-themes-full-container' );
2223 +
2224 + bottom = container.scrollTop() + container.height();
2225 + // Use a fixed distance to the bottom of loaded results to avoid unnecessarily
2226 + // loading results sooner when using a percentage of scroll distance.
2227 + threshold = container.prop( 'scrollHeight' ) - 3000;
2228 +
2229 + if ( bottom > threshold ) {
2230 + section.loadThemes();
2231 + }
2232 + }
2233 + },
2234 +
2235 + /**
2236 + * Event handler for search input that filters visible controls.
2237 + *
2238 + * @since 4.9.0
2239 + *
2240 + * @param {string} term - The raw search input value.
2241 + * @return {void}
2242 + */
2243 + filterSearch: function( term ) {
2244 + var count = 0,
2245 + visible = false,
2246 + section = this,
2247 + noFilter = ( api.section.has( 'wporg_themes' ) && 'remote' !== section.params.filter_type ) ? '.no-themes-local' : '.no-themes',
2248 + controls = section.controls(),
2249 + terms;
2250 +
2251 + if ( section.loading ) {
2252 + return;
2253 + }
2254 +
2255 + // Standardize search term format and split into an array of individual words.
2256 + terms = term.toLowerCase().trim().replace( /-/g, ' ' ).split( ' ' );
2257 +
2258 + _.each( controls, function( control ) {
2259 + visible = control.filter( terms ); // Shows/hides and sorts control based on the applicability of the search term.
2260 + if ( visible ) {
2261 + count = count + 1;
2262 + }
2263 + });
2264 +
2265 + if ( 0 === count ) {
2266 + section.container.find( noFilter ).show();
2267 + wp.a11y.speak( section.container.find( noFilter ).text() );
2268 + } else {
2269 + section.container.find( noFilter ).hide();
2270 + }
2271 +
2272 + section.renderScreenshots();
2273 + api.reflowPaneContents();
2274 +
2275 + // Update theme count.
2276 + section.updateCountDebounced( count );
2277 + },
2278 +
2279 + /**
2280 + * Event handler for search input that determines if the terms have changed and loads new controls as needed.
2281 + *
2282 + * @since 4.9.0
2283 + *
2284 + * @param {wp.customize.ThemesSection} section - The current theme section, passed through the debouncer.
2285 + * @return {void}
2286 + */
2287 + checkTerm: function( section ) {
2288 + var newTerm;
2289 + if ( 'remote' === section.params.filter_type ) {
2290 + newTerm = section.contentContainer.find( '.wp-filter-search' ).val();
2291 + if ( section.term !== newTerm.trim() ) {
2292 + section.initializeNewQuery( newTerm, section.tags );
2293 + }
2294 + }
2295 + },
2296 +
2297 + /**
2298 + * Check for filters checked in the feature filter list and initialize a new query.
2299 + *
2300 + * @since 4.9.0
2301 + *
2302 + * @return {void}
2303 + */
2304 + filtersChecked: function() {
2305 + var section = this,
2306 + items = section.container.find( '.filter-group' ).find( ':checkbox' ),
2307 + tags = [];
2308 +
2309 + _.each( items.filter( ':checked' ), function( item ) {
2310 + tags.push( $( item ).prop( 'value' ) );
2311 + });
2312 +
2313 + // When no filters are checked, restore initial state. Update filter count.
2314 + if ( 0 === tags.length ) {
2315 + tags = '';
2316 + section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).show();
2317 + section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).hide();
2318 + } else {
2319 + section.contentContainer.find( '.feature-filter-toggle .theme-filter-count' ).text( tags.length );
2320 + section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).hide();
2321 + section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).show();
2322 + }
2323 +
2324 + // Check whether tags have changed, and either load or queue them.
2325 + if ( ! _.isEqual( section.tags, tags ) ) {
2326 + if ( section.loading ) {
2327 + section.nextTags = tags;
2328 + } else {
2329 + if ( 'remote' === section.params.filter_type ) {
2330 + section.initializeNewQuery( section.term, tags );
2331 + } else if ( 'local' === section.params.filter_type ) {
2332 + section.filterSearch( tags.join( ' ' ) );
2333 + }
2334 + }
2335 + }
2336 + },
2337 +
2338 + /**
2339 + * Reset the current query and load new results.
2340 + *
2341 + * @since 4.9.0
2342 + *
2343 + * @param {string} newTerm - New term.
2344 + * @param {Array} newTags - New tags.
2345 + * @return {void}
2346 + */
2347 + initializeNewQuery: function( newTerm, newTags ) {
2348 + var section = this;
2349 +
2350 + // Clear the controls in the section.
2351 + _.each( section.controls(), function( control ) {
2352 + control.container.remove();
2353 + api.control.remove( control.id );
2354 + });
2355 + section.loaded = 0;
2356 + section.fullyLoaded = false;
2357 + section.screenshotQueue = null;
2358 +
2359 + // Run a new query, with loadThemes handling paging, etc.
2360 + if ( ! section.loading ) {
2361 + section.term = newTerm;
2362 + section.tags = newTags;
2363 + section.loadThemes();
2364 + } else {
2365 + section.nextTerm = newTerm; // This will reload from loadThemes() with the newest term once the current batch is loaded.
2366 + section.nextTags = newTags; // This will reload from loadThemes() with the newest tags once the current batch is loaded.
2367 + }
2368 + if ( ! section.expanded() ) {
2369 + section.expand(); // Expand the section if it isn't expanded.
2370 + }
2371 + },
2372 +
2373 + /**
2374 + * Render control's screenshot if the control comes into view.
2375 + *
2376 + * @since 4.2.0
2377 + *
2378 + * @return {void}
2379 + */
2380 + renderScreenshots: function() {
2381 + var section = this;
2382 +
2383 + // Fill queue initially, or check for more if empty.
2384 + if ( null === section.screenshotQueue || 0 === section.screenshotQueue.length ) {
2385 +
2386 + // Add controls that haven't had their screenshots rendered.
2387 + section.screenshotQueue = _.filter( section.controls(), function( control ) {
2388 + return ! control.screenshotRendered;
2389 + });
2390 + }
2391 +
2392 + // Are all screenshots rendered (for now)?
2393 + if ( ! section.screenshotQueue.length ) {
2394 + return;
2395 + }
2396 +
2397 + section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
2398 + var $imageWrapper = control.container.find( '.theme-screenshot' ),
2399 + $image = $imageWrapper.find( 'img' );
2400 +
2401 + if ( ! $image.length ) {
2402 + return false;
2403 + }
2404 +
2405 + if ( $image.is( ':hidden' ) ) {
2406 + return true;
2407 + }
2408 +
2409 + // Based on unveil.js.
2410 + var wt = section.$window.scrollTop(),
2411 + wb = wt + section.$window.height(),
2412 + et = $image.offset().top,
2413 + ih = $imageWrapper.height(),
2414 + eb = et + ih,
2415 + threshold = ih * 3,
2416 + inView = eb >= wt - threshold && et <= wb + threshold;
2417 +
2418 + if ( inView ) {
2419 + control.container.trigger( 'render-screenshot' );
2420 + }
2421 +
2422 + // If the image is in view return false so it's cleared from the queue.
2423 + return ! inView;
2424 + } );
2425 + },
2426 +
2427 + /**
2428 + * Get visible count.
2429 + *
2430 + * @since 4.9.0
2431 + *
2432 + * @return {number} Visible count.
2433 + */
2434 + getVisibleCount: function() {
2435 + return this.contentContainer.find( 'li.customize-control:visible' ).length;
2436 + },
2437 +
2438 + /**
2439 + * Update the number of themes in the section.
2440 + *
2441 + * @since 4.9.0
2442 + *
2443 + * @return {void}
2444 + */
2445 + updateCount: function( count ) {
2446 + var section = this, countEl, displayed;
2447 +
2448 + if ( ! count && 0 !== count ) {
2449 + count = section.getVisibleCount();
2450 + }
2451 +
2452 + displayed = section.contentContainer.find( '.themes-displayed' );
2453 + countEl = section.contentContainer.find( '.theme-count' );
2454 +
2455 + if ( 0 === count ) {
2456 + countEl.text( '0' );
2457 + } else {
2458 +
2459 + // Animate the count change for emphasis.
2460 + displayed.fadeOut( 180, function() {
2461 + countEl.text( count );
2462 + displayed.fadeIn( 180 );
2463 + } );
2464 + wp.a11y.speak( api.settings.l10n.announceThemeCount.replace( '%d', count ) );
2465 + }
2466 + },
2467 +
2468 + /**
2469 + * Advance the modal to the next theme.
2470 + *
2471 + * @since 4.2.0
2472 + *
2473 + * @return {void}
2474 + */
2475 + nextTheme: function () {
2476 + var section = this;
2477 + if ( section.getNextTheme() ) {
2478 + section.showDetails( section.getNextTheme(), function() {
2479 + section.overlay.find( '.right' ).focus();
2480 + } );
2481 + }
2482 + },
2483 +
2484 + /**
2485 + * Get the next theme model.
2486 + *
2487 + * @since 4.2.0
2488 + *
2489 + * @return {wp.customize.ThemeControl|boolean} Next theme.
2490 + */
2491 + getNextTheme: function () {
2492 + var section = this, control, nextControl, sectionControls, i;
2493 + control = api.control( section.params.action + '_theme_' + section.currentTheme );
2494 + sectionControls = section.controls();
2495 + i = _.indexOf( sectionControls, control );
2496 + if ( -1 === i ) {
2497 + return false;
2498 + }
2499 +
2500 + nextControl = sectionControls[ i + 1 ];
2501 + if ( ! nextControl ) {
2502 + return false;
2503 + }
2504 + return nextControl.params.theme;
2505 + },
2506 +
2507 + /**
2508 + * Advance the modal to the previous theme.
2509 + *
2510 + * @since 4.2.0
2511 + * @return {void}
2512 + */
2513 + previousTheme: function () {
2514 + var section = this;
2515 + if ( section.getPreviousTheme() ) {
2516 + section.showDetails( section.getPreviousTheme(), function() {
2517 + section.overlay.find( '.left' ).focus();
2518 + } );
2519 + }
2520 + },
2521 +
2522 + /**
2523 + * Get the previous theme model.
2524 + *
2525 + * @since 4.2.0
2526 + * @return {wp.customize.ThemeControl|boolean} Previous theme.
2527 + */
2528 + getPreviousTheme: function () {
2529 + var section = this, control, nextControl, sectionControls, i;
2530 + control = api.control( section.params.action + '_theme_' + section.currentTheme );
2531 + sectionControls = section.controls();
2532 + i = _.indexOf( sectionControls, control );
2533 + if ( -1 === i ) {
2534 + return false;
2535 + }
2536 +
2537 + nextControl = sectionControls[ i - 1 ];
2538 + if ( ! nextControl ) {
2539 + return false;
2540 + }
2541 + return nextControl.params.theme;
2542 + },
2543 +
2544 + /**
2545 + * Disable buttons when we're viewing the first or last theme.
2546 + *
2547 + * @since 4.2.0
2548 + *
2549 + * @return {void}
2550 + */
2551 + updateLimits: function () {
2552 + if ( ! this.getNextTheme() ) {
2553 + this.overlay.find( '.right' ).addClass( 'disabled' );
2554 + }
2555 + if ( ! this.getPreviousTheme() ) {
2556 + this.overlay.find( '.left' ).addClass( 'disabled' );
2557 + }
2558 + },
2559 +
2560 + /**
2561 + * Load theme preview.
2562 + *
2563 + * @since 4.7.0
2564 + * @access public
2565 + *
2566 + * @deprecated
2567 + * @param {string} themeId Theme ID.
2568 + * @return {jQuery.promise} Promise.
2569 + */
2570 + loadThemePreview: function( themeId ) {
2571 + return api.ThemesPanel.prototype.loadThemePreview.call( this, themeId );
2572 + },
2573 +
2574 + /**
2575 + * Render & show the theme details for a given theme model.
2576 + *
2577 + * @since 4.2.0
2578 + *
2579 + * @param {Object} theme - Theme.
2580 + * @param {Function} [callback] - Callback once the details have been shown.
2581 + * @return {void}
2582 + */
2583 + showDetails: function ( theme, callback ) {
2584 + var section = this, panel = api.panel( 'themes' );
2585 + section.currentTheme = theme.id;
2586 + section.overlay.html( section.template( theme ) )
2587 + .fadeIn( 'fast' )
2588 + .focus();
2589 +
2590 + function disableSwitchButtons() {
2591 + return ! panel.canSwitchTheme( theme.id );
2592 + }
2593 +
2594 + // Temporary special function since supplying SFTP credentials does not work yet. See #42184.
2595 + function disableInstallButtons() {
2596 + return disableSwitchButtons() || false === api.settings.theme._canInstall || true === api.settings.theme._filesystemCredentialsNeeded;
2597 + }
2598 +
2599 + section.overlay.find( 'button.preview, button.preview-theme' ).toggleClass( 'disabled', disableSwitchButtons() );
2600 + section.overlay.find( 'button.theme-install' ).toggleClass( 'disabled', disableInstallButtons() );
2601 +
2602 + section.$body.addClass( 'modal-open' );
2603 + section.containFocus( section.overlay );
2604 + section.updateLimits();
2605 + wp.a11y.speak( api.settings.l10n.announceThemeDetails.replace( '%s', theme.name ) );
2606 + if ( callback ) {
2607 + callback();
2608 + }
2609 + },
2610 +
2611 + /**
2612 + * Close the theme details modal.
2613 + *
2614 + * @since 4.2.0
2615 + *
2616 + * @return {void}
2617 + */
2618 + closeDetails: function () {
2619 + var section = this;
2620 + section.$body.removeClass( 'modal-open' );
2621 + section.overlay.fadeOut( 'fast' );
2622 + api.control( section.params.action + '_theme_' + section.currentTheme ).container.find( '.theme' ).focus();
2623 + },
2624 +
2625 + /**
2626 + * Keep tab focus within the theme details modal.
2627 + *
2628 + * @since 4.2.0
2629 + *
2630 + * @param {jQuery} el - Element to contain focus.
2631 + * @return {void}
2632 + */
2633 + containFocus: function( el ) {
2634 + var tabbables;
2635 +
2636 + el.on( 'keydown', function( event ) {
2637 +
2638 + // Return if it's not the tab key
2639 + // When navigating with prev/next focus is already handled.
2640 + if ( 9 !== event.keyCode ) {
2641 + return;
2642 + }
2643 +
2644 + // Uses jQuery UI to get the tabbable elements.
2645 + tabbables = $( ':tabbable', el );
2646 +
2647 + // Keep focus within the overlay.
2648 + if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
2649 + tabbables.first().focus();
2650 + return false;
2651 + } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
2652 + tabbables.last().focus();
2653 + return false;
2654 + }
2655 + });
2656 + }
2657 + });
2658 +
2659 + api.OuterSection = api.Section.extend(/** @lends wp.customize.OuterSection.prototype */{
2660 +
2661 + /**
2662 + * Class wp.customize.OuterSection.
2663 + *
2664 + * Creates section outside of the sidebar, there is no ui to trigger collapse/expand so
2665 + * it would require custom handling.
2666 + *
2667 + * @constructs wp.customize.OuterSection
2668 + * @augments wp.customize.Section
2669 + *
2670 + * @since 4.9.0
2671 + *
2672 + * @return {void}
2673 + */
2674 + initialize: function() {
2675 + var section = this;
2676 + section.containerParent = '#customize-outer-theme-controls';
2677 + section.containerPaneParent = '.customize-outer-pane-parent';
2678 + api.Section.prototype.initialize.apply( section, arguments );
2679 + },
2680 +
2681 + /**
2682 + * Overrides api.Section.prototype.onChangeExpanded to prevent collapse/expand effect
2683 + * on other sections and panels.
2684 + *
2685 + * @since 4.9.0
2686 + *
2687 + * @param {boolean} expanded - The expanded state to transition to.
2688 + * @param {Object} [args] - Args.
2689 + * @param {boolean} [args.unchanged] - Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early.
2690 + * @param {Function} [args.completeCallback] - Function to call when the slideUp/slideDown has completed.
2691 + * @param {Object} [args.duration] - The duration for the animation.
2692 + */
2693 + onChangeExpanded: function( expanded, args ) {
2694 + var section = this,
2695 + container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ),
2696 + content = section.contentContainer,
2697 + backBtn = content.find( '.customize-section-back' ),
2698 + sectionTitle = section.headContainer.find( '.accordion-section-title button, .accordion-section-title[tabindex]' ).first(),
2699 + body = $( document.body ),
2700 + expand, panel;
2701 +
2702 + body.toggleClass( 'outer-section-open', expanded );
2703 + section.container.toggleClass( 'open', expanded );
2704 + section.container.removeClass( 'busy' );
2705 + api.section.each( function( _section ) {
2706 + if ( 'outer' === _section.params.type && _section.id !== section.id ) {
2707 + _section.container.removeClass( 'open' );
2708 + }
2709 + } );
2710 +
2711 + if ( expanded && ! content.hasClass( 'open' ) ) {
2712 +
2713 + if ( args.unchanged ) {
2714 + expand = args.completeCallback;
2715 + } else {
2716 + expand = function() {
2717 + section._animateChangeExpanded( function() {
2718 + backBtn.attr( 'tabindex', '0' );
2719 + backBtn.trigger( 'focus' );
2720 + content.css( 'top', '' );
2721 + container.scrollTop( 0 );
2722 +
2723 + if ( args.completeCallback ) {
2724 + args.completeCallback();
2725 + }
2726 + } );
2727 +
2728 + content.addClass( 'open' );
2729 + }.bind( this );
2730 + }
2731 +
2732 + if ( section.panel() ) {
2733 + api.panel( section.panel() ).expand({
2734 + duration: args.duration,
2735 + completeCallback: expand
2736 + });
2737 + } else {
2738 + expand();
2739 + }
2740 +
2741 + } else if ( ! expanded && content.hasClass( 'open' ) ) {
2742 + if ( section.panel() ) {
2743 + panel = api.panel( section.panel() );
2744 + if ( panel.contentContainer.hasClass( 'skip-transition' ) ) {
2745 + panel.collapse();
2746 + }
2747 + }
2748 + section._animateChangeExpanded( function() {
2749 + backBtn.attr( 'tabindex', '-1' );
2750 + sectionTitle.trigger( 'focus' );
2751 + content.css( 'top', '' );
2752 +
2753 + if ( args.completeCallback ) {
2754 + args.completeCallback();
2755 + }
2756 + } );
2757 +
2758 + content.removeClass( 'open' );
2759 +
2760 + } else {
2761 + if ( args.completeCallback ) {
2762 + args.completeCallback();
2763 + }
2764 + }
2765 + }
2766 + });
2767 +
2768 + api.Panel = Container.extend(/** @lends wp.customize.Panel.prototype */{
2769 + containerType: 'panel',
2770 +
2771 + /**
2772 + * @constructs wp.customize.Panel
2773 + * @augments wp.customize~Container
2774 + *
2775 + * @since 4.1.0
2776 + *
2777 + * @param {string} id - The ID for the panel.
2778 + * @param {Object} options - Object containing one property: params.
2779 + * @param {string} options.title - Title shown when panel is collapsed and expanded.
2780 + * @param {string} [options.description] - Description shown at the top of the panel.
2781 + * @param {number} [options.priority=100] - The sort priority for the panel.
2782 + * @param {string} [options.type=default] - The type of the panel. See wp.customize.panelConstructor.
2783 + * @param {string} [options.content] - The markup to be used for the panel container. If empty, a JS template is used.
2784 + * @param {boolean} [options.active=true] - Whether the panel is active or not.
2785 + * @param {Object} [options.params] - Deprecated wrapper for the above properties.
2786 + */
2787 + initialize: function ( id, options ) {
2788 + var panel = this, params;
2789 + params = options.params || options;
2790 +
2791 + // Look up the type if one was not supplied.
2792 + if ( ! params.type ) {
2793 + _.find( api.panelConstructor, function( Constructor, type ) {
2794 + if ( Constructor === panel.constructor ) {
2795 + params.type = type;
2796 + return true;
2797 + }
2798 + return false;
2799 + } );
2800 + }
2801 +
2802 + Container.prototype.initialize.call( panel, id, params );
2803 +
2804 + panel.embed();
2805 + panel.deferred.embedded.done( function () {
2806 + panel.ready();
2807 + });
2808 + },
2809 +
2810 + /**
2811 + * Embed the container in the DOM when any parent panel is ready.
2812 + *
2813 + * @since 4.1.0
2814 + */
2815 + embed: function () {
2816 + var panel = this,
2817 + container = $( '#customize-theme-controls' ),
2818 + parentContainer = $( '.customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable.
2819 +
2820 + if ( ! panel.headContainer.parent().is( parentContainer ) ) {
2821 + parentContainer.append( panel.headContainer );
2822 + }
2823 + if ( ! panel.contentContainer.parent().is( panel.headContainer ) ) {
2824 + container.append( panel.contentContainer );
2825 + }
2826 + panel.renderContent();
2827 +
2828 + panel.deferred.embedded.resolve();
2829 + },
2830 +
2831 + /**
2832 + * @since 4.1.0
2833 + */
2834 + attachEvents: function () {
2835 + var meta, panel = this;
2836 +
2837 + // Expand/Collapse accordion sections on click.
2838 + panel.headContainer.find( '.accordion-section-title button, .accordion-section-title[tabindex]' ).on( 'click keydown', function( event ) {
2839 + if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2840 + return;
2841 + }
2842 + event.preventDefault(); // Keep this AFTER the key filter above.
2843 +
2844 + if ( ! panel.expanded() ) {
2845 + panel.expand();
2846 + }
2847 + });
2848 +
2849 + // Close panel.
2850 + panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) {
2851 + if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2852 + return;
2853 + }
2854 + event.preventDefault(); // Keep this AFTER the key filter above.
2855 +
2856 + if ( panel.expanded() ) {
2857 + panel.collapse();
2858 + }
2859 + });
2860 +
2861 + meta = panel.container.find( '.panel-meta:first' );
2862 +
2863 + meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
2864 + if ( meta.hasClass( 'cannot-expand' ) ) {
2865 + return;
2866 + }
2867 +
2868 + var content = meta.find( '.customize-panel-description:first' );
2869 + if ( meta.hasClass( 'open' ) ) {
2870 + meta.toggleClass( 'open' );
2871 + content.slideUp( panel.defaultExpandedArguments.duration, function() {
2872 + content.trigger( 'toggled' );
2873 + } );
2874 + $( this ).attr( 'aria-expanded', false );
2875 + } else {
2876 + content.slideDown( panel.defaultExpandedArguments.duration, function() {
2877 + content.trigger( 'toggled' );
2878 + } );
2879 + meta.toggleClass( 'open' );
2880 + $( this ).attr( 'aria-expanded', true );
2881 + }
2882 + });
2883 +
2884 + },
2885 +
2886 + /**
2887 + * Get the sections that are associated with this panel, sorted by their priority Value.
2888 + *
2889 + * @since 4.1.0
2890 + *
2891 + * @return {Array}
2892 + */
2893 + sections: function () {
2894 + return this._children( 'panel', 'section' );
2895 + },
2896 +
2897 + /**
2898 + * Return whether this panel has any active sections.
2899 + *
2900 + * @since 4.1.0
2901 + *
2902 + * @return {boolean} Whether contextually active.
2903 + */
2904 + isContextuallyActive: function () {
2905 + var panel = this,
2906 + sections = panel.sections(),
2907 + activeCount = 0;
2908 + _( sections ).each( function ( section ) {
2909 + if ( section.active() && section.isContextuallyActive() ) {
2910 + activeCount += 1;
2911 + }
2912 + } );
2913 + return ( activeCount !== 0 );
2914 + },
2915 +
2916 + /**
2917 + * Update UI to reflect expanded state.
2918 + *
2919 + * @since 4.1.0
2920 + *
2921 + * @param {boolean} expanded
2922 + * @param {Object} args
2923 + * @param {boolean} args.unchanged
2924 + * @param {Function} args.completeCallback
2925 + * @return {void}
2926 + */
2927 + onChangeExpanded: function ( expanded, args ) {
2928 +
2929 + // Immediately call the complete callback if there were no changes.
2930 + if ( args.unchanged ) {
2931 + if ( args.completeCallback ) {
2932 + args.completeCallback();
2933 + }
2934 + return;
2935 + }
2936 +
2937 + // Note: there is a second argument 'args' passed.
2938 + var panel = this,
2939 + accordionSection = panel.contentContainer,
2940 + overlay = accordionSection.closest( '.wp-full-overlay' ),
2941 + container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ),
2942 + topPanel = panel.headContainer.find( '.accordion-section-title button, .accordion-section-title[tabindex]' ),
2943 + backBtn = accordionSection.find( '.customize-panel-back' ),
2944 + childSections = panel.sections(),
2945 + skipTransition;
2946 +
2947 + if ( expanded && ! accordionSection.hasClass( 'current-panel' ) ) {
2948 + // Collapse any sibling sections/panels.
2949 + api.section.each( function ( section ) {
2950 + if ( panel.id !== section.panel() ) {
2951 + section.collapse( { duration: 0 } );
2952 + }
2953 + });
2954 + api.panel.each( function ( otherPanel ) {
2955 + if ( panel !== otherPanel ) {
2956 + otherPanel.collapse( { duration: 0 } );
2957 + }
2958 + });
2959 +
2960 + if ( panel.params.autoExpandSoleSection && 1 === childSections.length && childSections[0].active.get() ) {
2961 + accordionSection.addClass( 'current-panel skip-transition' );
2962 + overlay.addClass( 'in-sub-panel' );
2963 +
2964 + childSections[0].expand( {
2965 + completeCallback: args.completeCallback
2966 + } );
2967 + } else {
2968 + panel._animateChangeExpanded( function() {
2969 + backBtn.attr( 'tabindex', '0' );
2970 + backBtn.trigger( 'focus' );
2971 + accordionSection.css( 'top', '' );
2972 + container.scrollTop( 0 );
2973 +
2974 + if ( args.completeCallback ) {
2975 + args.completeCallback();
2976 + }
2977 + } );
2978 +
2979 + accordionSection.addClass( 'current-panel' );
2980 + overlay.addClass( 'in-sub-panel' );
2981 + }
2982 +
2983 + api.state( 'expandedPanel' ).set( panel );
2984 +
2985 + } else if ( ! expanded && accordionSection.hasClass( 'current-panel' ) ) {
2986 + skipTransition = accordionSection.hasClass( 'skip-transition' );
2987 + if ( ! skipTransition ) {
2988 + panel._animateChangeExpanded( function() {
2989 +
2990 + topPanel.focus();
2991 + accordionSection.css( 'top', '' );
2992 +
2993 + if ( args.completeCallback ) {
2994 + args.completeCallback();
2995 + }
2996 + } );
2997 + } else {
2998 + accordionSection.removeClass( 'skip-transition' );
2999 + }
3000 +
3001 + overlay.removeClass( 'in-sub-panel' );
3002 + accordionSection.removeClass( 'current-panel' );
3003 + if ( panel === api.state( 'expandedPanel' ).get() ) {
3004 + api.state( 'expandedPanel' ).set( false );
3005 + }
3006 + }
3007 + },
3008 +
3009 + /**
3010 + * Render the panel from its JS template, if it exists.
3011 + *
3012 + * The panel's container must already exist in the DOM.
3013 + *
3014 + * @since 4.3.0
3015 + */
3016 + renderContent: function () {
3017 + var template,
3018 + panel = this;
3019 +
3020 + // Add the content to the container.
3021 + if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) {
3022 + template = wp.template( panel.templateSelector + '-content' );
3023 + } else {
3024 + template = wp.template( 'customize-panel-default-content' );
3025 + }
3026 + if ( template && panel.headContainer ) {
3027 + panel.contentContainer.html( template( _.extend(
3028 + { id: panel.id },
3029 + panel.params
3030 + ) ) );
3031 + }
3032 + }
3033 + });
3034 +
3035 + api.ThemesPanel = api.Panel.extend(/** @lends wp.customize.ThemsPanel.prototype */{
3036 +
3037 + /**
3038 + * Class wp.customize.ThemesPanel.
3039 + *
3040 + * Custom section for themes that displays without the customize preview.
3041 + *
3042 + * @constructs wp.customize.ThemesPanel
3043 + * @augments wp.customize.Panel
3044 + *
3045 + * @since 4.9.0
3046 + *
3047 + * @param {string} id - The ID for the panel.
3048 + * @param {Object} options - Options.
3049 + * @return {void}
3050 + */
3051 + initialize: function( id, options ) {
3052 + var panel = this;
3053 + panel.installingThemes = [];
3054 + api.Panel.prototype.initialize.call( panel, id, options );
3055 + },
3056 +
3057 + /**
3058 + * Determine whether a given theme can be switched to, or in general.
3059 + *
3060 + * @since 4.9.0
3061 + *
3062 + * @param {string} [slug] - Theme slug.
3063 + * @return {boolean} Whether the theme can be switched to.
3064 + */
3065 + canSwitchTheme: function canSwitchTheme( slug ) {
3066 + if ( slug && slug === api.settings.theme.stylesheet ) {
3067 + return true;
3068 + }
3069 + return 'publish' === api.state( 'selectedChangesetStatus' ).get() && ( '' === api.state( 'changesetStatus' ).get() || 'auto-draft' === api.state( 'changesetStatus' ).get() );
3070 + },
3071 +
3072 + /**
3073 + * Attach events.
3074 + *
3075 + * @since 4.9.0
3076 + * @return {void}
3077 + */
3078 + attachEvents: function() {
3079 + var panel = this;
3080 +
3081 + // Attach regular panel events.
3082 + api.Panel.prototype.attachEvents.apply( panel );
3083 +
3084 + // Temporary since supplying SFTP credentials does not work yet. See #42184.
3085 + if ( api.settings.theme._canInstall && api.settings.theme._filesystemCredentialsNeeded ) {
3086 + panel.notifications.add( new api.Notification( 'theme_install_unavailable', {
3087 + message: api.l10n.themeInstallUnavailable,
3088 + type: 'info',
3089 + dismissible: true
3090 + } ) );
3091 + }
3092 +
3093 + function toggleDisabledNotifications() {
3094 + if ( panel.canSwitchTheme() ) {
3095 + panel.notifications.remove( 'theme_switch_unavailable' );
3096 + } else {
3097 + panel.notifications.add( new api.Notification( 'theme_switch_unavailable', {
3098 + message: api.l10n.themePreviewUnavailable,
3099 + type: 'warning'
3100 + } ) );
3101 + }
3102 + }
3103 + toggleDisabledNotifications();
3104 + api.state( 'selectedChangesetStatus' ).bind( toggleDisabledNotifications );
3105 + api.state( 'changesetStatus' ).bind( toggleDisabledNotifications );
3106 +
3107 + // Collapse panel to customize the current theme.
3108 + panel.contentContainer.on( 'click', '.customize-theme', function() {
3109 + panel.collapse();
3110 + });
3111 +
3112 + // Toggle between filtering and browsing themes on mobile.
3113 + panel.contentContainer.on( 'click', '.customize-themes-section-title, .customize-themes-mobile-back', function() {
3114 + $( '.wp-full-overlay' ).toggleClass( 'showing-themes' );
3115 + });
3116 +
3117 + // Install (and maybe preview) a theme.
3118 + panel.contentContainer.on( 'click', '.theme-install', function( event ) {
3119 + panel.installTheme( event );
3120 + });
3121 +
3122 + // Update a theme. Theme cards have the class, the details modal has the id.
3123 + panel.contentContainer.on( 'click', '.update-theme, #update-theme', function( event ) {
3124 +
3125 + // #update-theme is a link.
3126 + event.preventDefault();
3127 + event.stopPropagation();
3128 +
3129 + panel.updateTheme( event );
3130 + });
3131 +
3132 + // Delete a theme.
3133 + panel.contentContainer.on( 'click', '.delete-theme', function( event ) {
3134 + panel.deleteTheme( event );
3135 + });
3136 +
3137 + _.bindAll( panel, 'installTheme', 'updateTheme' );
3138 + },
3139 +
3140 + /**
3141 + * Update UI to reflect expanded state
3142 + *
3143 + * @since 4.9.0
3144 + *
3145 + * @param {boolean} expanded - Expanded state.
3146 + * @param {Object} args - Args.
3147 + * @param {boolean} args.unchanged - Whether or not the state changed.
3148 + * @param {Function} args.completeCallback - Callback to execute when the animation completes.
3149 + * @return {void}
3150 + */
3151 + onChangeExpanded: function( expanded, args ) {
3152 + var panel = this, overlay, sections, hasExpandedSection = false;
3153 +
3154 + // Expand/collapse the panel normally.
3155 + api.Panel.prototype.onChangeExpanded.apply( this, [ expanded, args ] );
3156 +
3157 + // Immediately call the complete callback if there were no changes.
3158 + if ( args.unchanged ) {
3159 + if ( args.completeCallback ) {
3160 + args.completeCallback();
3161 + }
3162 + return;
3163 + }
3164 +
3165 + overlay = panel.headContainer.closest( '.wp-full-overlay' );
3166 +
3167 + if ( expanded ) {
3168 + overlay
3169 + .addClass( 'in-themes-panel' )
3170 + .delay( 200 ).find( '.customize-themes-full-container' ).addClass( 'animate' );
3171 +
3172 + _.delay( function() {
3173 + overlay.addClass( 'themes-panel-expanded' );
3174 + }, 200 );
3175 +
3176 + // Automatically open the first section (except on small screens), if one isn't already expanded.
3177 + if ( 600 < window.innerWidth ) {
3178 + sections = panel.sections();
3179 + _.each( sections, function( section ) {
3180 + if ( section.expanded() ) {
3181 + hasExpandedSection = true;
3182 + }
3183 + } );
3184 + if ( ! hasExpandedSection && sections.length > 0 ) {
3185 + sections[0].expand();
3186 + }
3187 + }
3188 + } else {
3189 + overlay
3190 + .removeClass( 'in-themes-panel themes-panel-expanded' )
3191 + .find( '.customize-themes-full-container' ).removeClass( 'animate' );
3192 + }
3193 + },
3194 +
3195 + /**
3196 + * Install a theme via wp.updates.
3197 + *
3198 + * @since 4.9.0
3199 + *
3200 + * @param {jQuery.Event} event - Event.
3201 + * @return {jQuery.promise} Promise.
3202 + */
3203 + installTheme: function( event ) {
3204 + var panel = this, preview, onInstallSuccess, slug = $( event.target ).data( 'slug' ), deferred = $.Deferred(), request;
3205 + preview = $( event.target ).hasClass( 'preview' );
3206 +
3207 + // Temporary since supplying SFTP credentials does not work yet. See #42184.
3208 + if ( api.settings.theme._filesystemCredentialsNeeded ) {
3209 + deferred.reject({
3210 + errorCode: 'theme_install_unavailable'
3211 + });
3212 + return deferred.promise();
3213 + }
3214 +
3215 + // Prevent loading a non-active theme preview when there is a drafted/scheduled changeset.
3216 + if ( ! panel.canSwitchTheme( slug ) ) {
3217 + deferred.reject({
3218 + errorCode: 'theme_switch_unavailable'
3219 + });
3220 + return deferred.promise();
3221 + }
3222 +
3223 + // Theme is already being installed.
3224 + if ( _.contains( panel.installingThemes, slug ) ) {
3225 + deferred.reject({
3226 + errorCode: 'theme_already_installing'
3227 + });
3228 + return deferred.promise();
3229 + }
3230 +
3231 + wp.updates.maybeRequestFilesystemCredentials( event );
3232 +
3233 + onInstallSuccess = function( response ) {
3234 + var theme = false, themeControl;
3235 + if ( preview ) {
3236 + api.notifications.remove( 'theme_installing' );
3237 +
3238 + panel.loadThemePreview( slug );
3239 +
3240 + } else {
3241 + api.control.each( function( control ) {
3242 + if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
3243 + theme = control.params.theme; // Used below to add theme control.
3244 + control.rerenderAsInstalled( true );
3245 + }
3246 + });
3247 +
3248 + // Don't add the same theme more than once.
3249 + if ( ! theme || api.control.has( 'installed_theme_' + theme.id ) ) {
3250 + deferred.resolve( response );
3251 + return;
3252 + }
3253 +
3254 + // Add theme control to installed section.
3255 + theme.type = 'installed';
3256 + themeControl = new api.controlConstructor.theme( 'installed_theme_' + theme.id, {
3257 + type: 'theme',
3258 + section: 'installed_themes',
3259 + theme: theme,
3260 + priority: 0 // Add all newly-installed themes to the top.
3261 + } );
3262 +
3263 + api.control.add( themeControl );
3264 + api.control( themeControl.id ).container.trigger( 'render-screenshot' );
3265 +
3266 + // Close the details modal if it's open to the installed theme.
3267 + api.section.each( function( section ) {
3268 + if ( 'themes' === section.params.type ) {
3269 + if ( theme.id === section.currentTheme ) { // Don't close the modal if the user has navigated elsewhere.
3270 + section.closeDetails();
3271 + }
3272 + }
3273 + });
3274 + }
3275 + deferred.resolve( response );
3276 + };
3277 +
3278 + panel.installingThemes.push( slug ); // Note: we don't remove elements from installingThemes, since they shouldn't be installed again.
3279 + request = wp.updates.installTheme( {
3280 + slug: slug
3281 + } );
3282 +
3283 + // Also preview the theme as the event is triggered on Install & Preview.
3284 + if ( preview ) {
3285 + api.notifications.add( new api.OverlayNotification( 'theme_installing', {
3286 + message: api.l10n.themeDownloading,
3287 + type: 'info',
3288 + loading: true
3289 + } ) );
3290 + }
3291 +
3292 + request.done( onInstallSuccess );
3293 + request.fail( function() {
3294 + api.notifications.remove( 'theme_installing' );
3295 + } );
3296 +
3297 + return deferred.promise();
3298 + },
3299 +
3300 + /**
3301 + * Load theme preview.
3302 + *
3303 + * @since 4.9.0
3304 + *
3305 + * @param {string} themeId Theme ID.
3306 + * @return {jQuery.promise} Promise.
3307 + */
3308 + loadThemePreview: function( themeId ) {
3309 + var panel = this, deferred = $.Deferred(), onceProcessingComplete, urlParser, queryParams;
3310 +
3311 + // Prevent loading a non-active theme preview when there is a drafted/scheduled changeset.
3312 + if ( ! panel.canSwitchTheme( themeId ) ) {
3313 + deferred.reject({
3314 + errorCode: 'theme_switch_unavailable'
3315 + });
3316 + return deferred.promise();
3317 + }
3318 +
3319 + urlParser = document.createElement( 'a' );
3320 + urlParser.href = location.href;
3321 + queryParams = _.extend(
3322 + api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
3323 + {
3324 + theme: themeId,
3325 + changeset_uuid: api.settings.changeset.uuid,
3326 + 'return': api.settings.url['return']
3327 + }
3328 + );
3329 +
3330 + // Include autosaved param to load autosave revision without prompting user to restore it.
3331 + if ( ! api.state( 'saved' ).get() ) {
3332 + queryParams.customize_autosaved = 'on';
3333 + }
3334 +
3335 + urlParser.search = $.param( queryParams );
3336 +
3337 + // Update loading message. Everything else is handled by reloading the page.
3338 + api.notifications.add( new api.OverlayNotification( 'theme_previewing', {
3339 + message: api.l10n.themePreviewWait,
3340 + type: 'info',
3341 + loading: true
3342 + } ) );
3343 +
3344 + onceProcessingComplete = function() {
3345 + var request;
3346 + if ( api.state( 'processing' ).get() > 0 ) {
3347 + return;
3348 + }
3349 +
3350 + api.state( 'processing' ).unbind( onceProcessingComplete );
3351 +
3352 + request = api.requestChangesetUpdate( {}, { autosave: true } );
3353 + request.done( function() {
3354 + deferred.resolve();
3355 + $( window ).off( 'beforeunload.customize-confirm' );
3356 + location.replace( urlParser.href );
3357 + } );
3358 + request.fail( function() {
3359 +
3360 + // @todo Show notification regarding failure.
3361 + api.notifications.remove( 'theme_previewing' );
3362 +
3363 + deferred.reject();
3364 + } );
3365 + };
3366 +
3367 + if ( 0 === api.state( 'processing' ).get() ) {
3368 + onceProcessingComplete();
3369 + } else {
3370 + api.state( 'processing' ).bind( onceProcessingComplete );
3371 + }
3372 +
3373 + return deferred.promise();
3374 + },
3375 +
3376 + /**
3377 + * Update a theme via wp.updates.
3378 + *
3379 + * @since 4.9.0
3380 + *
3381 + * @param {jQuery.Event} event - Event.
3382 + * @return {void}
3383 + */
3384 + updateTheme: function( event ) {
3385 + wp.updates.maybeRequestFilesystemCredentials( event );
3386 +
3387 + $( document ).one( 'wp-theme-update-success', function( e, response ) {
3388 +
3389 + // Rerender the control to reflect the update.
3390 + api.control.each( function( control ) {
3391 + if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
3392 + control.params.theme.hasUpdate = false;
3393 + control.params.theme.version = response.newVersion;
3394 + setTimeout( function() {
3395 + control.rerenderAsInstalled( true );
3396 + }, 2000 );
3397 + }
3398 + });
3399 + } );
3400 +
3401 + wp.updates.updateTheme( {
3402 + slug: $( event.target ).closest( '.notice' ).data( 'slug' )
3403 + } );
3404 + },
3405 +
3406 + /**
3407 + * Delete a theme via wp.updates.
3408 + *
3409 + * @since 4.9.0
3410 + *
3411 + * @param {jQuery.Event} event - Event.
3412 + * @return {void}
3413 + */
3414 + deleteTheme: function( event ) {
3415 + var theme, section;
3416 + theme = $( event.target ).data( 'slug' );
3417 + section = api.section( 'installed_themes' );
3418 +
3419 + event.preventDefault();
3420 +
3421 + // Temporary since supplying SFTP credentials does not work yet. See #42184.
3422 + if ( api.settings.theme._filesystemCredentialsNeeded ) {
3423 + return;
3424 + }
3425 +
3426 + // Confirmation dialog for deleting a theme.
3427 + if ( ! window.confirm( api.settings.l10n.confirmDeleteTheme ) ) {
3428 + return;
3429 + }
3430 +
3431 + wp.updates.maybeRequestFilesystemCredentials( event );
3432 +
3433 + $( document ).one( 'wp-theme-delete-success', function() {
3434 + var control = api.control( 'installed_theme_' + theme );
3435 +
3436 + // Remove theme control.
3437 + control.container.remove();
3438 + api.control.remove( control.id );
3439 +
3440 + // Update installed count.
3441 + section.loaded = section.loaded - 1;
3442 + section.updateCount();
3443 +
3444 + // Rerender any other theme controls as uninstalled.
3445 + api.control.each( function( control ) {
3446 + if ( 'theme' === control.params.type && control.params.theme.id === theme ) {
3447 + control.rerenderAsInstalled( false );
3448 + }
3449 + });
3450 + } );
3451 +
3452 + wp.updates.deleteTheme( {
3453 + slug: theme
3454 + } );
3455 +
3456 + // Close modal and focus the section.
3457 + section.closeDetails();
3458 + section.focus();
3459 + }
3460 + });
3461 +
3462 + api.Control = api.Class.extend(/** @lends wp.customize.Control.prototype */{
3463 + defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
3464 +
3465 + /**
3466 + * Default params.
3467 + *
3468 + * @since 4.9.0
3469 + * @var {object}
3470 + */
3471 + defaults: {
3472 + label: '',
3473 + description: '',
3474 + active: true,
3475 + priority: 10
3476 + },
3477 +
3478 + /**
3479 + * A Customizer Control.
3480 + *
3481 + * A control provides a UI element that allows a user to modify a Customizer Setting.
3482 + *
3483 + * @see PHP class WP_Customize_Control.
3484 + *
3485 + * @constructs wp.customize.Control
3486 + * @augments wp.customize.Class
3487 + *
3488 + * @borrows wp.customize~focus as this#focus
3489 + * @borrows wp.customize~Container#activate as this#activate
3490 + * @borrows wp.customize~Container#deactivate as this#deactivate
3491 + * @borrows wp.customize~Container#_toggleActive as this#_toggleActive
3492 + *
3493 + * @param {string} id - Unique identifier for the control instance.
3494 + * @param {Object} options - Options hash for the control instance.
3495 + * @param {Object} options.type - Type of control (e.g. text, radio, dropdown-pages, etc.)
3496 + * @param {string} [options.content] - The HTML content for the control or at least its container. This should normally be left blank and instead supplying a templateId.
3497 + * @param {string} [options.templateId] - Template ID for control's content.
3498 + * @param {string} [options.priority=10] - Order of priority to show the control within the section.
3499 + * @param {string} [options.active=true] - Whether the control is active.
3500 + * @param {string} options.section - The ID of the section the control belongs to.
3501 + * @param {mixed} [options.setting] - The ID of the main setting or an instance of this setting.
3502 + * @param {mixed} options.settings - An object with keys (e.g. default) that maps to setting IDs or Setting/Value objects, or an array of setting IDs or Setting/Value objects.
3503 + * @param {mixed} options.settings.default - The ID of the setting the control relates to.
3504 + * @param {string} options.settings.data - @todo Is this used?
3505 + * @param {string} options.label - Label.
3506 + * @param {string} options.description - Description.
3507 + * @param {number} [options.instanceNumber] - Order in which this instance was created in relation to other instances.
3508 + * @param {Object} [options.params] - Deprecated wrapper for the above properties.
3509 + * @return {void}
3510 + */
3511 + initialize: function( id, options ) {
3512 + var control = this, deferredSettingIds = [], settings, gatherSettings;
3513 +
3514 + control.params = _.extend(
3515 + {},
3516 + control.defaults,
3517 + control.params || {}, // In case subclass already defines.
3518 + options.params || options || {} // The options.params property is deprecated, but it is checked first for back-compat.
3519 + );
3520 +
3521 + if ( ! api.Control.instanceCounter ) {
3522 + api.Control.instanceCounter = 0;
3523 + }
3524 + api.Control.instanceCounter++;
3525 + if ( ! control.params.instanceNumber ) {
3526 + control.params.instanceNumber = api.Control.instanceCounter;
3527 + }
3528 +
3529 + // Look up the type if one was not supplied.
3530 + if ( ! control.params.type ) {
3531 + _.find( api.controlConstructor, function( Constructor, type ) {
3532 + if ( Constructor === control.constructor ) {
3533 + control.params.type = type;
3534 + return true;
3535 + }
3536 + return false;
3537 + } );
3538 + }
3539 +
3540 + if ( ! control.params.content ) {
3541 + control.params.content = $( '<li></li>', {
3542 + id: 'customize-control-' + id.replace( /]/g, '' ).replace( /\[/g, '-' ),
3543 + 'class': 'customize-control customize-control-' + control.params.type
3544 + } );
3545 + }
3546 +
3547 + control.id = id;
3548 + control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); // Deprecated, likely dead code from time before #28709.
3549 + if ( control.params.content ) {
3550 + control.container = $( control.params.content );
3551 + } else {
3552 + control.container = $( control.selector ); // Likely dead, per above. See #28709.
3553 + }
3554 +
3555 + if ( control.params.templateId ) {
3556 + control.templateSelector = control.params.templateId;
3557 + } else {
3558 + control.templateSelector = 'customize-control-' + control.params.type + '-content';
3559 + }
3560 +
3561 + control.deferred = _.extend( control.deferred || {}, {
3562 + embedded: new $.Deferred()
3563 + } );
3564 + control.section = new api.Value();
3565 + control.priority = new api.Value();
3566 + control.active = new api.Value();
3567 + control.activeArgumentsQueue = [];
3568 + control.notifications = new api.Notifications({
3569 + alt: control.altNotice
3570 + });
3571 +
3572 + control.elements = [];
3573 +
3574 + control.active.bind( function ( active ) {
3575 + var args = control.activeArgumentsQueue.shift();
3576 + args = $.extend( {}, control.defaultActiveArguments, args );
3577 + control.onChangeActive( active, args );
3578 + } );
3579 +
3580 + control.section.set( control.params.section );
3581 + control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
3582 + control.active.set( control.params.active );
3583 +
3584 + api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
3585 +
3586 + control.settings = {};
3587 +
3588 + settings = {};
3589 + if ( control.params.setting ) {
3590 + settings['default'] = control.params.setting;
3591 + }
3592 + _.extend( settings, control.params.settings );
3593 +
3594 + // Note: Settings can be an array or an object, with values being either setting IDs or Setting (or Value) objects.
3595 + _.each( settings, function( value, key ) {
3596 + var setting;
3597 + if ( _.isObject( value ) && _.isFunction( value.extended ) && value.extended( api.Value ) ) {
3598 + control.settings[ key ] = value;
3599 + } else if ( _.isString( value ) ) {
3600 + setting = api( value );
3601 + if ( setting ) {
3602 + control.settings[ key ] = setting;
3603 + } else {
3604 + deferredSettingIds.push( value );
3605 + }
3606 + }
3607 + } );
3608 +
3609 + gatherSettings = function() {
3610 +
3611 + // Fill-in all resolved settings.
3612 + _.each( settings, function ( settingId, key ) {
3613 + if ( ! control.settings[ key ] && _.isString( settingId ) ) {
3614 + control.settings[ key ] = api( settingId );
3615 + }
3616 + } );
3617 +
3618 + // Make sure settings passed as array gets associated with default.
3619 + if ( control.settings[0] && ! control.settings['default'] ) {
3620 + control.settings['default'] = control.settings[0];
3621 + }
3622 +
3623 + // Identify the main setting.
3624 + control.setting = control.settings['default'] || null;
3625 +
3626 + control.linkElements(); // Link initial elements present in server-rendered content.
3627 + control.embed();
3628 + };
3629 +
3630 + if ( 0 === deferredSettingIds.length ) {
3631 + gatherSettings();
3632 + } else {
3633 + api.apply( api, deferredSettingIds.concat( gatherSettings ) );
3634 + }
3635 +
3636 + // After the control is embedded on the page, invoke the "ready" method.
3637 + control.deferred.embedded.done( function () {
3638 + control.linkElements(); // Link any additional elements after template is rendered by renderContent().
3639 + control.setupNotifications();
3640 + control.ready();
3641 + });
3642 + },
3643 +
3644 + /**
3645 + * Link elements between settings and inputs.
3646 + *
3647 + * @since 4.7.0
3648 + * @access public
3649 + *
3650 + * @return {void}
3651 + */
3652 + linkElements: function () {
3653 + var control = this, nodes, radios, element;
3654 +
3655 + nodes = control.container.find( '[data-customize-setting-link], [data-customize-setting-key-link]' );
3656 + radios = {};
3657 +
3658 + nodes.each( function () {
3659 + var node = $( this ), name, setting;
3660 +
3661 + if ( node.data( 'customizeSettingLinked' ) ) {
3662 + return;
3663 + }
3664 + node.data( 'customizeSettingLinked', true ); // Prevent re-linking element.
3665 +
3666 + if ( node.is( ':radio' ) ) {
3667 + name = node.prop( 'name' );
3668 + if ( radios[name] ) {
3669 + return;
3670 + }
3671 +
3672 + radios[name] = true;
3673 + node = nodes.filter( '[name="' + name + '"]' );
3674 + }
3675 +
3676 + // Let link by default refer to setting ID. If it doesn't exist, fallback to looking up by setting key.
3677 + if ( node.data( 'customizeSettingLink' ) ) {
3678 + setting = api( node.data( 'customizeSettingLink' ) );
3679 + } else if ( node.data( 'customizeSettingKeyLink' ) ) {
3680 + setting = control.settings[ node.data( 'customizeSettingKeyLink' ) ];
3681 + }
3682 +
3683 + if ( setting ) {
3684 + element = new api.Element( node );
3685 + control.elements.push( element );
3686 + element.sync( setting );
3687 + element.set( setting() );
3688 + }
3689 + } );
3690 + },
3691 +
3692 + /**
3693 + * Embed the control into the page.
3694 + */
3695 + embed: function () {
3696 + var control = this,
3697 + inject;
3698 +
3699 + // Watch for changes to the section state.
3700 + inject = function ( sectionId ) {
3701 + var parentContainer;
3702 + if ( ! sectionId ) { // @todo Allow a control to be embedded without a section, for instance a control embedded in the front end.
3703 + return;
3704 + }
3705 + // Wait for the section to be registered.
3706 + api.section( sectionId, function ( section ) {
3707 + // Wait for the section to be ready/initialized.
3708 + section.deferred.embedded.done( function () {
3709 + parentContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' );
3710 + if ( ! control.container.parent().is( parentContainer ) ) {
3711 + parentContainer.append( control.container );
3712 + }
3713 + control.renderContent();
3714 + control.deferred.embedded.resolve();
3715 + });
3716 + });
3717 + };
3718 + control.section.bind( inject );
3719 + inject( control.section.get() );
3720 + },
3721 +
3722 + /**
3723 + * Triggered when the control's markup has been injected into the DOM.
3724 + *
3725 + * @return {void}
3726 + */
3727 + ready: function() {
3728 + var control = this, newItem;
3729 + if ( 'dropdown-pages' === control.params.type && control.params.allow_addition ) {
3730 + newItem = control.container.find( '.new-content-item-wrapper' );
3731 + newItem.hide(); // Hide in JS to preserve flex display when showing.
3732 + control.container.on( 'click', '.add-new-toggle', function( e ) {
3733 + $( e.currentTarget ).slideUp( 180 );
3734 + newItem.slideDown( 180 );
3735 + newItem.find( '.create-item-input' ).focus();
3736 + });
3737 + control.container.on( 'click', '.add-content', function() {
3738 + control.addNewPage();
3739 + });
3740 + control.container.on( 'keydown', '.create-item-input', function( e ) {
3741 + if ( 13 === e.which ) { // Enter.
3742 + control.addNewPage();
3743 + }
3744 + });
3745 + }
3746 + },
3747 +
3748 + /**
3749 + * Get the element inside of a control's container that contains the validation error message.
3750 + *
3751 + * Control subclasses may override this to return the proper container to render notifications into.
3752 + * Injects the notification container for existing controls that lack the necessary container,
3753 + * including special handling for nav menu items and widgets.
3754 + *
3755 + * @since 4.6.0
3756 + * @return {jQuery} Setting validation message element.
3757 + */
3758 + getNotificationsContainerElement: function() {
3759 + var control = this, controlTitle, notificationsContainer;
3760 +
3761 + notificationsContainer = control.container.find( '.customize-control-notifications-container:first' );
3762 + if ( notificationsContainer.length ) {
3763 + return notificationsContainer;
3764 + }
3765 +
3766 + notificationsContainer = $( '<div class="customize-control-notifications-container"></div>' );
3767 +
3768 + if ( control.container.hasClass( 'customize-control-nav_menu_item' ) ) {
3769 + control.container.find( '.menu-item-settings:first' ).prepend( notificationsContainer );
3770 + } else if ( control.container.hasClass( 'customize-control-widget_form' ) ) {
3771 + control.container.find( '.widget-inside:first' ).prepend( notificationsContainer );
3772 + } else {
3773 + controlTitle = control.container.find( '.customize-control-title' );
3774 + if ( controlTitle.length ) {
3775 + controlTitle.after( notificationsContainer );
3776 + } else {
3777 + control.container.prepend( notificationsContainer );
3778 + }
3779 + }
3780 + return notificationsContainer;
3781 + },
3782 +
3783 + /**
3784 + * Set up notifications.
3785 + *
3786 + * @since 4.9.0
3787 + * @return {void}
3788 + */
3789 + setupNotifications: function() {
3790 + var control = this, renderNotificationsIfVisible, onSectionAssigned;
3791 +
3792 + // Add setting notifications to the control notification.
3793 + _.each( control.settings, function( setting ) {
3794 + if ( ! setting.notifications ) {
3795 + return;
3796 + }
3797 + setting.notifications.bind( 'add', function( settingNotification ) {
3798 + var params = _.extend(
3799 + {},
3800 + settingNotification,
3801 + {
3802 + setting: setting.id
3803 + }
3804 + );
3805 + control.notifications.add( new api.Notification( setting.id + ':' + settingNotification.code, params ) );
3806 + } );
3807 + setting.notifications.bind( 'remove', function( settingNotification ) {
3808 + control.notifications.remove( setting.id + ':' + settingNotification.code );
3809 + } );
3810 + } );
3811 +
3812 + renderNotificationsIfVisible = function() {
3813 + var sectionId = control.section();
3814 + if ( ! sectionId || ( api.section.has( sectionId ) && api.section( sectionId ).expanded() ) ) {
3815 + control.notifications.render();
3816 + }
3817 + };
3818 +
3819 + control.notifications.bind( 'rendered', function() {
3820 + var notifications = control.notifications.get();
3821 + control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
3822 + control.container.toggleClass( 'has-error', 0 !== _.where( notifications, { type: 'error' } ).length );
3823 + } );
3824 +
3825 + onSectionAssigned = function( newSectionId, oldSectionId ) {
3826 + if ( oldSectionId && api.section.has( oldSectionId ) ) {
3827 + api.section( oldSectionId ).expanded.unbind( renderNotificationsIfVisible );
3828 + }
3829 + if ( newSectionId ) {
3830 + api.section( newSectionId, function( section ) {
3831 + section.expanded.bind( renderNotificationsIfVisible );
3832 + renderNotificationsIfVisible();
3833 + });
3834 + }
3835 + };
3836 +
3837 + control.section.bind( onSectionAssigned );
3838 + onSectionAssigned( control.section.get() );
3839 + control.notifications.bind( 'change', _.debounce( renderNotificationsIfVisible ) );
3840 + },
3841 +
3842 + /**
3843 + * Render notifications.
3844 + *
3845 + * Renders the `control.notifications` into the control's container.
3846 + * Control subclasses may override this method to do their own handling
3847 + * of rendering notifications.
3848 + *
3849 + * @deprecated in favor of `control.notifications.render()`
3850 + * @since 4.6.0
3851 + * @this {wp.customize.Control}
3852 + */
3853 + renderNotifications: function() {
3854 + var control = this, container, notifications, hasError = false;
3855 +
3856 + if ( 'undefined' !== typeof console && console.warn ) {
3857 + console.warn( '[DEPRECATED] wp.customize.Control.prototype.renderNotifications() is deprecated in favor of instantiating a wp.customize.Notifications and calling its render() method.' );
3858 + }
3859 +
3860 + container = control.getNotificationsContainerElement();
3861 + if ( ! container || ! container.length ) {
3862 + return;
3863 + }
3864 + notifications = [];
3865 + control.notifications.each( function( notification ) {
3866 + notifications.push( notification );
3867 + if ( 'error' === notification.type ) {
3868 + hasError = true;
3869 + }
3870 + } );
3871 +
3872 + if ( 0 === notifications.length ) {
3873 + container.stop().slideUp( 'fast' );
3874 + } else {
3875 + container.stop().slideDown( 'fast', null, function() {
3876 + $( this ).css( 'height', 'auto' );
3877 + } );
3878 + }
3879 +
3880 + if ( ! control.notificationsTemplate ) {
3881 + control.notificationsTemplate = wp.template( 'customize-control-notifications' );
3882 + }
3883 +
3884 + control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
3885 + control.container.toggleClass( 'has-error', hasError );
3886 + container.empty().append(
3887 + control.notificationsTemplate( { notifications: notifications, altNotice: Boolean( control.altNotice ) } ).trim()
3888 + );
3889 + },
3890 +
3891 + /**
3892 + * Normal controls do not expand, so just expand its parent
3893 + *
3894 + * @param {Object} [params]
3895 + */
3896 + expand: function ( params ) {
3897 + api.section( this.section() ).expand( params );
3898 + },
3899 +
3900 + /*
3901 + * Documented using @borrows in the constructor.
3902 + */
3903 + focus: focus,
3904 +
3905 + /**
3906 + * Update UI in response to a change in the control's active state.
3907 + * This does not change the active state, it merely handles the behavior
3908 + * for when it does change.
3909 + *
3910 + * @since 4.1.0
3911 + *
3912 + * @param {boolean} active
3913 + * @param {Object} args
3914 + * @param {number} args.duration
3915 + * @param {Function} args.completeCallback
3916 + */
3917 + onChangeActive: function ( active, args ) {
3918 + if ( args.unchanged ) {
3919 + if ( args.completeCallback ) {
3920 + args.completeCallback();
3921 + }
3922 + return;
3923 + }
3924 +
3925 + if ( ! $.contains( document, this.container[0] ) ) {
3926 + // jQuery.fn.slideUp is not hiding an element if it is not in the DOM.
3927 + this.container.toggle( active );
3928 + if ( args.completeCallback ) {
3929 + args.completeCallback();
3930 + }
3931 + } else if ( active ) {
3932 + this.container.slideDown( args.duration, args.completeCallback );
3933 + } else {
3934 + this.container.slideUp( args.duration, args.completeCallback );
3935 + }
3936 + },
3937 +
3938 + /**
3939 + * @deprecated 4.1.0 Use this.onChangeActive() instead.
3940 + */
3941 + toggle: function ( active ) {
3942 + return this.onChangeActive( active, this.defaultActiveArguments );
3943 + },
3944 +
3945 + /*
3946 + * Documented using @borrows in the constructor
3947 + */
3948 + activate: Container.prototype.activate,
3949 +
3950 + /*
3951 + * Documented using @borrows in the constructor
3952 + */
3953 + deactivate: Container.prototype.deactivate,
3954 +
3955 + /*
3956 + * Documented using @borrows in the constructor
3957 + */
3958 + _toggleActive: Container.prototype._toggleActive,
3959 +
3960 + // @todo This function appears to be dead code and can be removed.
3961 + dropdownInit: function() {
3962 + var control = this,
3963 + statuses = this.container.find('.dropdown-status'),
3964 + params = this.params,
3965 + toggleFreeze = false,
3966 + update = function( to ) {
3967 + if ( 'string' === typeof to && params.statuses && params.statuses[ to ] ) {
3968 + statuses.html( params.statuses[ to ] ).show();
3969 + } else {
3970 + statuses.hide();
3971 + }
3972 + };
3973 +
3974 + // Support the .dropdown class to open/close complex elements.
3975 + this.container.on( 'click keydown', '.dropdown', function( event ) {
3976 + if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
3977 + return;
3978 + }
3979 +
3980 + event.preventDefault();
3981 +
3982 + if ( ! toggleFreeze ) {
3983 + control.container.toggleClass( 'open' );
3984 + }
3985 +
3986 + if ( control.container.hasClass( 'open' ) ) {
3987 + control.container.parent().parent().find( 'li.library-selected' ).focus();
3988 + }
3989 +
3990 + // Don't want to fire focus and click at same time.
3991 + toggleFreeze = true;
3992 + setTimeout(function () {
3993 + toggleFreeze = false;
3994 + }, 400);
3995 + });
3996 +
3997 + this.setting.bind( update );
3998 + update( this.setting() );
3999 + },
4000 +
4001 + /**
4002 + * Render the control from its JS template, if it exists.
4003 + *
4004 + * The control's container must already exist in the DOM.
4005 + *
4006 + * @since 4.1.0
4007 + */
4008 + renderContent: function () {
4009 + var control = this, template, standardTypes, templateId, sectionId;
4010 +
4011 + standardTypes = [
4012 + 'button',
4013 + 'checkbox',
4014 + 'date',
4015 + 'datetime-local',
4016 + 'email',
4017 + 'month',
4018 + 'number',
4019 + 'password',
4020 + 'radio',
4021 + 'range',
4022 + 'search',
4023 + 'select',
4024 + 'tel',
4025 + 'time',
4026 + 'text',
4027 + 'textarea',
4028 + 'week',
4029 + 'url'
4030 + ];
4031 +
4032 + templateId = control.templateSelector;
4033 +
4034 + // Use default content template when a standard HTML type is used,
4035 + // there isn't a more specific template existing, and the control container is empty.
4036 + if ( templateId === 'customize-control-' + control.params.type + '-content' &&
4037 + _.contains( standardTypes, control.params.type ) &&
4038 + ! document.getElementById( 'tmpl-' + templateId ) &&
4039 + 0 === control.container.children().length )
4040 + {
4041 + templateId = 'customize-control-default-content';
4042 + }
4043 +
4044 + // Replace the container element's content with the control.
4045 + if ( document.getElementById( 'tmpl-' + templateId ) ) {
4046 + template = wp.template( templateId );
4047 + if ( template && control.container ) {
4048 + control.container.html( template( control.params ) );
4049 + }
4050 + }
4051 +
4052 + // Re-render notifications after content has been re-rendered.
4053 + control.notifications.container = control.getNotificationsContainerElement();
4054 + sectionId = control.section();
4055 + if ( ! sectionId || ( api.section.has( sectionId ) && api.section( sectionId ).expanded() ) ) {
4056 + control.notifications.render();
4057 + }
4058 + },
4059 +
4060 + /**
4061 + * Add a new page to a dropdown-pages control reusing menus code for this.
4062 + *
4063 + * @since 4.7.0
4064 + * @access private
4065 + *
4066 + * @return {void}
4067 + */
4068 + addNewPage: function () {
4069 + var control = this, promise, toggle, container, input, inputError, title, select;
4070 +
4071 + if ( 'dropdown-pages' !== control.params.type || ! control.params.allow_addition || ! api.Menus ) {
4072 + return;
4073 + }
4074 +
4075 + toggle = control.container.find( '.add-new-toggle' );
4076 + container = control.container.find( '.new-content-item-wrapper' );
4077 + input = control.container.find( '.create-item-input' );
4078 + inputError = control.container.find('.create-item-error');
4079 + title = input.val();
4080 + select = control.container.find( 'select' );
4081 +
4082 + if ( ! title ) {
4083 + container.addClass( 'form-invalid' );
4084 + input.attr('aria-invalid', 'true');
4085 + input.attr('aria-describedby', inputError.attr('id'));
4086 + inputError.slideDown( 'fast' );
4087 + wp.a11y.speak( inputError.text() );
4088 + return;
4089 + }
4090 +
4091 + container.removeClass( 'form-invalid' );
4092 + input.attr('aria-invalid', 'false');
4093 + input.removeAttr('aria-describedby');
4094 + inputError.hide();
4095 + input.attr( 'disabled', 'disabled' );
4096 +
4097 + // The menus functions add the page, publish when appropriate,
4098 + // and also add the new page to the dropdown-pages controls.
4099 + promise = api.Menus.insertAutoDraftPost( {
4100 + post_title: title,
4101 + post_type: 'page'
4102 + } );
4103 + promise.done( function( data ) {
4104 + var availableItem, $content, itemTemplate;
4105 +
4106 + // Prepare the new page as an available menu item.
4107 + // See api.Menus.submitNew().
4108 + availableItem = new api.Menus.AvailableItemModel( {
4109 + 'id': 'post-' + data.post_id, // Used for available menu item Backbone models.
4110 + 'title': title,
4111 + 'type': 'post_type',
4112 + 'type_label': api.Menus.data.l10n.page_label,
4113 + 'object': 'page',
4114 + 'object_id': data.post_id,
4115 + 'url': data.url
4116 + } );
4117 +
4118 + // Add the new item to the list of available menu items.
4119 + api.Menus.availableMenuItemsPanel.collection.add( availableItem );
4120 + $content = $( '#available-menu-items-post_type-page' ).find( '.available-menu-items-list' );
4121 + itemTemplate = wp.template( 'available-menu-item' );
4122 + $content.prepend( itemTemplate( availableItem.attributes ) );
4123 +
4124 + // Focus the select control.
4125 + select.focus();
4126 + control.setting.set( String( data.post_id ) ); // Triggers a preview refresh and updates the setting.
4127 +
4128 + // Reset the create page form.
4129 + container.slideUp( 180 );
4130 + toggle.slideDown( 180 );
4131 + } );
4132 + promise.always( function() {
4133 + input.val( '' ).removeAttr( 'disabled' );
4134 + } );
4135 + }
4136 + });
4137 +
4138 + /**
4139 + * A colorpicker control.
4140 + *
4141 + * @class wp.customize.ColorControl
4142 + * @augments wp.customize.Control
4143 + */
4144 + api.ColorControl = api.Control.extend(/** @lends wp.customize.ColorControl.prototype */{
4145 + ready: function() {
4146 + var control = this,
4147 + isHueSlider = this.params.mode === 'hue',
4148 + updating = false,
4149 + picker;
4150 +
4151 + if ( isHueSlider ) {
4152 + picker = this.container.find( '.color-picker-hue' );
4153 + picker.val( control.setting() ).wpColorPicker({
4154 + change: function( event, ui ) {
4155 + updating = true;
4156 + control.setting( ui.color.h() );
4157 + updating = false;
4158 + }
4159 + });
4160 + } else {
4161 + picker = this.container.find( '.color-picker-hex' );
4162 + picker.val( control.setting() ).wpColorPicker({
4163 + change: function() {
4164 + updating = true;
4165 + control.setting.set( picker.wpColorPicker( 'color' ) );
4166 + updating = false;
4167 + },
4168 + clear: function() {
4169 + updating = true;
4170 + control.setting.set( '' );
4171 + updating = false;
4172 + }
4173 + });
4174 + }
4175 +
4176 + control.setting.bind( function ( value ) {
4177 + // Bail if the update came from the control itself.
4178 + if ( updating ) {
4179 + return;
4180 + }
4181 + picker.val( value );
4182 + picker.wpColorPicker( 'color', value );
4183 + } );
4184 +
4185 + // Collapse color picker when hitting Esc instead of collapsing the current section.
4186 + control.container.on( 'keydown', function( event ) {
4187 + var pickerContainer;
4188 + if ( 27 !== event.which ) { // Esc.
4189 + return;
4190 + }
4191 + pickerContainer = control.container.find( '.wp-picker-container' );
4192 + if ( pickerContainer.hasClass( 'wp-picker-active' ) ) {
4193 + picker.wpColorPicker( 'close' );
4194 + control.container.find( '.wp-color-result' ).focus();
4195 + event.stopPropagation(); // Prevent section from being collapsed.
4196 + }
4197 + } );
4198 + }
4199 + });
4200 +
4201 + /**
4202 + * A control that implements the media modal.
4203 + *
4204 + * @class wp.customize.MediaControl
4205 + * @augments wp.customize.Control
4206 + */
4207 + api.MediaControl = api.Control.extend(/** @lends wp.customize.MediaControl.prototype */{
4208 +
4209 + /**
4210 + * When the control's DOM structure is ready,
4211 + * set up internal event bindings.
4212 + */
4213 + ready: function() {
4214 + var control = this;
4215 + // Shortcut so that we don't have to use _.bind every time we add a callback.
4216 + _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
4217 +
4218 + // Bind events, with delegation to facilitate re-rendering.
4219 + control.container.on( 'click keydown', '.upload-button', control.openFrame );
4220 + control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
4221 + control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
4222 + control.container.on( 'click keydown', '.default-button', control.restoreDefault );
4223 + control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
4224 + control.container.on( 'click keydown', '.remove-button', control.removeFile );
4225 + control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
4226 +
4227 + // Resize the player controls when it becomes visible (ie when section is expanded).
4228 + api.section( control.section() ).container
4229 + .on( 'expanded', function() {
4230 + if ( control.player ) {
4231 + control.player.setControlsSize();
4232 + }
4233 + })
4234 + .on( 'collapsed', function() {
4235 + control.pausePlayer();
4236 + });
4237 +
4238 + /**
4239 + * Set attachment data and render content.
4240 + *
4241 + * Note that BackgroundImage.prototype.ready applies this ready method
4242 + * to itself. Since BackgroundImage is an UploadControl, the value
4243 + * is the attachment URL instead of the attachment ID. In this case
4244 + * we skip fetching the attachment data because we have no ID available,
4245 + * and it is the responsibility of the UploadControl to set the control's
4246 + * attachmentData before calling the renderContent method.
4247 + *
4248 + * @param {number|string} value Attachment
4249 + */
4250 + function setAttachmentDataAndRenderContent( value ) {
4251 + var hasAttachmentData = $.Deferred();
4252 +
4253 + if ( control.extended( api.UploadControl ) ) {
4254 + hasAttachmentData.resolve();
4255 + } else {
4256 + value = parseInt( value, 10 );
4257 + if ( _.isNaN( value ) || value <= 0 ) {
4258 + delete control.params.attachment;
4259 + hasAttachmentData.resolve();
4260 + } else if ( control.params.attachment && control.params.attachment.id === value ) {
4261 + hasAttachmentData.resolve();
4262 + }
4263 + }
4264 +
4265 + // Fetch the attachment data.
4266 + if ( 'pending' === hasAttachmentData.state() ) {
4267 + wp.media.attachment( value ).fetch().done( function() {
4268 + control.params.attachment = this.attributes;
4269 + hasAttachmentData.resolve();
4270 +
4271 + // Send attachment information to the preview for possible use in `postMessage` transport.
4272 + wp.customize.previewer.send( control.setting.id + '-attachment-data', this.attributes );
4273 + } );
4274 + }
4275 +
4276 + hasAttachmentData.done( function() {
4277 + control.renderContent();
4278 + } );
4279 + }
4280 +
4281 + // Ensure attachment data is initially set (for dynamically-instantiated controls).
4282 + setAttachmentDataAndRenderContent( control.setting() );
4283 +
4284 + // Update the attachment data and re-render the control when the setting changes.
4285 + control.setting.bind( setAttachmentDataAndRenderContent );
4286 + },
4287 +
4288 + pausePlayer: function () {
4289 + this.player && this.player.pause();
4290 + },
4291 +
4292 + cleanupPlayer: function () {
4293 + this.player && wp.media.mixin.removePlayer( this.player );
4294 + },
4295 +
4296 + /**
4297 + * Open the media modal.
4298 + */
4299 + openFrame: function( event ) {
4300 + if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
4301 + return;
4302 + }
4303 +
4304 + event.preventDefault();
4305 +
4306 + if ( ! this.frame ) {
4307 + this.initFrame();
4308 + }
4309 +
4310 + this.frame.open();
4311 + },
4312 +
4313 + /**
4314 + * Create a media modal select frame, and store it so the instance can be reused when needed.
4315 + */
4316 + initFrame: function() {
4317 + this.frame = wp.media({
4318 + button: {
4319 + text: this.params.button_labels.frame_button
4320 + },
4321 + states: [
4322 + new wp.media.controller.Library({
4323 + title: this.params.button_labels.frame_title,
4324 + library: wp.media.query({ type: this.params.mime_type }),
4325 + multiple: false,
4326 + date: false
4327 + })
4328 + ]
4329 + });
4330 +
4331 + // When a file is selected, run a callback.
4332 + this.frame.on( 'select', this.select );
4333 + },
4334 +
4335 + /**
4336 + * Callback handler for when an attachment is selected in the media modal.
4337 + * Gets the selected image information, and sets it within the control.
4338 + */
4339 + select: function() {
4340 + // Get the attachment from the modal frame.
4341 + var node,
4342 + attachment = this.frame.state().get( 'selection' ).first().toJSON(),
4343 + mejsSettings = window._wpmejsSettings || {};
4344 +
4345 + this.params.attachment = attachment;
4346 +
4347 + // Set the Customizer setting; the callback takes care of rendering.
4348 + this.setting( attachment.id );
4349 + node = this.container.find( 'audio, video' ).get(0);
4350 +
4351 + // Initialize audio/video previews.
4352 + if ( node ) {
4353 + this.player = new MediaElementPlayer( node, mejsSettings );
4354 + } else {
4355 + this.cleanupPlayer();
4356 + }
4357 + },
4358 +
4359 + /**
4360 + * Reset the setting to the default value.
4361 + */
4362 + restoreDefault: function( event ) {
4363 + if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
4364 + return;
4365 + }
4366 + event.preventDefault();
4367 +
4368 + this.params.attachment = this.params.defaultAttachment;
4369 + this.setting( this.params.defaultAttachment.url );
4370 + },
4371 +
4372 + /**
4373 + * Called when the "Remove" link is clicked. Empties the setting.
4374 + *
4375 + * @param {Object} event jQuery Event object
4376 + */
4377 + removeFile: function( event ) {
4378 + if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
4379 + return;
4380 + }
4381 + event.preventDefault();
4382 +
4383 + this.params.attachment = {};
4384 + this.setting( '' );
4385 + this.renderContent(); // Not bound to setting change when emptying.
4386 + }
4387 + });
4388 +
4389 + /**
4390 + * An upload control, which utilizes the media modal.
4391 + *
4392 + * @class wp.customize.UploadControl
4393 + * @augments wp.customize.MediaControl
4394 + */
4395 + api.UploadControl = api.MediaControl.extend(/** @lends wp.customize.UploadControl.prototype */{
4396 +
4397 + /**
4398 + * Callback handler for when an attachment is selected in the media modal.
4399 + * Gets the selected image information, and sets it within the control.
4400 + */
4401 + select: function() {
4402 + // Get the attachment from the modal frame.
4403 + var node,
4404 + attachment = this.frame.state().get( 'selection' ).first().toJSON(),
4405 + mejsSettings = window._wpmejsSettings || {};
4406 +
4407 + this.params.attachment = attachment;
4408 +
4409 + // Set the Customizer setting; the callback takes care of rendering.
4410 + this.setting( attachment.url );
4411 + node = this.container.find( 'audio, video' ).get(0);
4412 +
4413 + // Initialize audio/video previews.
4414 + if ( node ) {
4415 + this.player = new MediaElementPlayer( node, mejsSettings );
4416 + } else {
4417 + this.cleanupPlayer();
4418 + }
4419 + },
4420 +
4421 + // @deprecated
4422 + success: function() {},
4423 +
4424 + // @deprecated
4425 + removerVisibility: function() {}
4426 + });
4427 +
4428 + /**
4429 + * A control for uploading images.
4430 + *
4431 + * This control no longer needs to do anything more
4432 + * than what the upload control does in JS.
4433 + *
4434 + * @class wp.customize.ImageControl
4435 + * @augments wp.customize.UploadControl
4436 + */
4437 + api.ImageControl = api.UploadControl.extend(/** @lends wp.customize.ImageControl.prototype */{
4438 + // @deprecated
4439 + thumbnailSrc: function() {}
4440 + });
4441 +
4442 + /**
4443 + * A control for uploading background images.
4444 + *
4445 + * @class wp.customize.BackgroundControl
4446 + * @augments wp.customize.UploadControl
4447 + */
4448 + api.BackgroundControl = api.UploadControl.extend(/** @lends wp.customize.BackgroundControl.prototype */{
4449 +
4450 + /**
4451 + * When the control's DOM structure is ready,
4452 + * set up internal event bindings.
4453 + */
4454 + ready: function() {
4455 + api.UploadControl.prototype.ready.apply( this, arguments );
4456 + },
4457 +
4458 + /**
4459 + * Callback handler for when an attachment is selected in the media modal.
4460 + * Does an additional Ajax request for setting the background context.
4461 + */
4462 + select: function() {
4463 + api.UploadControl.prototype.select.apply( this, arguments );
4464 +
4465 + wp.ajax.post( 'custom-background-add', {
4466 + nonce: _wpCustomizeBackground.nonces.add,
4467 + wp_customize: 'on',
4468 + customize_theme: api.settings.theme.stylesheet,
4469 + attachment_id: this.params.attachment.id
4470 + } );
4471 + }
4472 + });
4473 +
4474 + /**
4475 + * A control for positioning a background image.
4476 + *
4477 + * @since 4.7.0
4478 + *
4479 + * @class wp.customize.BackgroundPositionControl
4480 + * @augments wp.customize.Control
4481 + */
4482 + api.BackgroundPositionControl = api.Control.extend(/** @lends wp.customize.BackgroundPositionControl.prototype */{
4483 +
4484 + /**
4485 + * Set up control UI once embedded in DOM and settings are created.
4486 + *
4487 + * @since 4.7.0
4488 + * @access public
4489 + */
4490 + ready: function() {
4491 + var control = this, updateRadios;
4492 +
4493 + control.container.on( 'change', 'input[name="background-position"]', function() {
4494 + var position = $( this ).val().split( ' ' );
4495 + control.settings.x( position[0] );
4496 + control.settings.y( position[1] );
4497 + } );
4498 +
4499 + updateRadios = _.debounce( function() {
4500 + var x, y, radioInput, inputValue;
4501 + x = control.settings.x.get();
4502 + y = control.settings.y.get();
4503 + inputValue = String( x ) + ' ' + String( y );
4504 + radioInput = control.container.find( 'input[name="background-position"][value="' + inputValue + '"]' );
4505 + radioInput.trigger( 'click' );
4506 + } );
4507 + control.settings.x.bind( updateRadios );
4508 + control.settings.y.bind( updateRadios );
4509 +
4510 + updateRadios(); // Set initial UI.
4511 + }
4512 + } );
4513 +
4514 + /**
4515 + * A control for selecting and cropping an image.
4516 + *
4517 + * @class wp.customize.CroppedImageControl
4518 + * @augments wp.customize.MediaControl
4519 + */
4520 + api.CroppedImageControl = api.MediaControl.extend(/** @lends wp.customize.CroppedImageControl.prototype */{
4521 +
4522 + /**
4523 + * Open the media modal to the library state.
4524 + */
4525 + openFrame: function( event ) {
4526 + if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
4527 + return;
4528 + }
4529 +
4530 + this.initFrame();
4531 + this.frame.setState( 'library' ).open();
4532 + },
4533 +
4534 + /**
4535 + * Create a media modal select frame, and store it so the instance can be reused when needed.
4536 + */
4537 + initFrame: function() {
4538 + var l10n = _wpMediaViewsL10n;
4539 +
4540 + this.frame = wp.media({
4541 + button: {
4542 + text: l10n.select,
4543 + close: false
4544 + },
4545 + states: [
4546 + new wp.media.controller.Library({
4547 + title: this.params.button_labels.frame_title,
4548 + library: wp.media.query({ type: 'image' }),
4549 + multiple: false,
4550 + date: false,
4551 + priority: 20,
4552 + suggestedWidth: this.params.width,
4553 + suggestedHeight: this.params.height
4554 + }),
4555 + new wp.media.controller.CustomizeImageCropper({
4556 + imgSelectOptions: this.calculateImageSelectOptions,
4557 + control: this
4558 + })
4559 + ]
4560 + });
4561 +
4562 + this.frame.on( 'select', this.onSelect, this );
4563 + this.frame.on( 'cropped', this.onCropped, this );
4564 + this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
4565 + },
4566 +
4567 + /**
4568 + * After an image is selected in the media modal, switch to the cropper
4569 + * state if the image isn't the right size.
4570 + */
4571 + onSelect: function() {
4572 + var attachment = this.frame.state().get( 'selection' ).first().toJSON();
4573 +
4574 + if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
4575 + this.setImageFromAttachment( attachment );
4576 + this.frame.close();
4577 + } else {
4578 + this.frame.setState( 'cropper' );
4579 + }
4580 + },
4581 +
4582 + /**
4583 + * After the image has been cropped, apply the cropped image data to the setting.
4584 + *
4585 + * @param {Object} croppedImage Cropped attachment data.
4586 + */
4587 + onCropped: function( croppedImage ) {
4588 + this.setImageFromAttachment( croppedImage );
4589 + },
4590 +
4591 + /**
4592 + * Returns a set of options, computed from the attached image data and
4593 + * control-specific data, to be fed to the imgAreaSelect plugin in
4594 + * wp.media.view.Cropper.
4595 + *
4596 + * @param {wp.media.model.Attachment} attachment
4597 + * @param {wp.media.controller.Cropper} controller
4598 + * @return {Object} Options
4599 + */
4600 + calculateImageSelectOptions: function( attachment, controller ) {
4601 + var control = controller.get( 'control' ),
4602 + flexWidth = !! parseInt( control.params.flex_width, 10 ),
4603 + flexHeight = !! parseInt( control.params.flex_height, 10 ),
4604 + realWidth = attachment.get( 'width' ),
4605 + realHeight = attachment.get( 'height' ),
4606 + xInit = parseInt( control.params.width, 10 ),
4607 + yInit = parseInt( control.params.height, 10 ),
4608 + requiredRatio = xInit / yInit,
4609 + realRatio = realWidth / realHeight,
4610 + xImg = xInit,
4611 + yImg = yInit,
4612 + x1, y1, imgSelectOptions;
4613 +
4614 + controller.set( 'hasRequiredAspectRatio', control.hasRequiredAspectRatio( requiredRatio, realRatio ) );
4615 + controller.set( 'suggestedCropSize', { width: realWidth, height: realHeight, x1: 0, y1: 0, x2: xInit, y2: yInit } );
4616 + controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
4617 +
4618 + if ( realRatio > requiredRatio ) {
4619 + yInit = realHeight;
4620 + xInit = yInit * requiredRatio;
4621 + } else {
4622 + xInit = realWidth;
4623 + yInit = xInit / requiredRatio;
4624 + }
4625 +
4626 + x1 = ( realWidth - xInit ) / 2;
4627 + y1 = ( realHeight - yInit ) / 2;
4628 +
4629 + imgSelectOptions = {
4630 + handles: true,
4631 + keys: true,
4632 + instance: true,
4633 + persistent: true,
4634 + imageWidth: realWidth,
4635 + imageHeight: realHeight,
4636 + minWidth: xImg > xInit ? xInit : xImg,
4637 + minHeight: yImg > yInit ? yInit : yImg,
4638 + x1: x1,
4639 + y1: y1,
4640 + x2: xInit + x1,
4641 + y2: yInit + y1
4642 + };
4643 +
4644 + if ( flexHeight === false && flexWidth === false ) {
4645 + imgSelectOptions.aspectRatio = xInit + ':' + yInit;
4646 + }
4647 +
4648 + if ( true === flexHeight ) {
4649 + delete imgSelectOptions.minHeight;
4650 + imgSelectOptions.maxWidth = realWidth;
4651 + }
4652 +
4653 + if ( true === flexWidth ) {
4654 + delete imgSelectOptions.minWidth;
4655 + imgSelectOptions.maxHeight = realHeight;
4656 + }
4657 +
4658 + return imgSelectOptions;
4659 + },
4660 +
4661 + /**
4662 + * Return whether the image must be cropped, based on required dimensions.
4663 + *
4664 + * @param {boolean} flexW Width is flexible.
4665 + * @param {boolean} flexH Height is flexible.
4666 + * @param {number} dstW Required width.
4667 + * @param {number} dstH Required height.
4668 + * @param {number} imgW Provided image's width.
4669 + * @param {number} imgH Provided image's height.
4670 + * @return {boolean} Whether cropping is required.
4671 + */
4672 + mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
4673 + if ( true === flexW && true === flexH ) {
4674 + return false;
4675 + }
4676 +
4677 + if ( true === flexW && dstH === imgH ) {
4678 + return false;
4679 + }
4680 +
4681 + if ( true === flexH && dstW === imgW ) {
4682 + return false;
4683 + }
4684 +
4685 + if ( dstW === imgW && dstH === imgH ) {
4686 + return false;
4687 + }
4688 +
4689 + if ( imgW <= dstW ) {
4690 + return false;
4691 + }
4692 +
4693 + return true;
4694 + },
4695 +
4696 + /**
4697 + * Check if the image's aspect ratio essentially matches the required aspect ratio.
4698 + *
4699 + * Floating point precision is low, so this allows a small tolerance. This
4700 + * tolerance allows for images over 100,000 px on either side to still trigger
4701 + * the cropping flow.
4702 + *
4703 + * @param {number} requiredRatio Required image ratio.
4704 + * @param {number} realRatio Provided image ratio.
4705 + * @return {boolean} Whether the image has the required aspect ratio.
4706 + */
4707 + hasRequiredAspectRatio: function ( requiredRatio, realRatio ) {
4708 + if ( Math.abs( requiredRatio - realRatio ) < 0.000001 ) {
4709 + return true;
4710 + }
4711 +
4712 + return false;
4713 + },
4714 +
4715 + /**
4716 + * If cropping was skipped, apply the image data directly to the setting.
4717 + */
4718 + onSkippedCrop: function() {
4719 + var attachment = this.frame.state().get( 'selection' ).first().toJSON();
4720 + this.setImageFromAttachment( attachment );
4721 + },
4722 +
4723 + /**
4724 + * Updates the setting and re-renders the control UI.
4725 + *
4726 + * @param {Object} attachment
4727 + */
4728 + setImageFromAttachment: function( attachment ) {
4729 + var control = this;
4730 + this.params.attachment = attachment;
4731 +
4732 + // Set the Customizer setting; the callback takes care of rendering.
4733 + this.setting( attachment.id );
4734 +
4735 + // Set focus to the first relevant button after the icon.
4736 + _.defer( function() {
4737 + var firstButton = control.container.find( '.actions .button' ).first();
4738 + if ( firstButton.length ) {
4739 + firstButton.focus();
4740 + }
4741 + } );
4742 + }
4743 + });
4744 +
4745 + /**
4746 + * A control for selecting and cropping Site Icons.
4747 + *
4748 + * @class wp.customize.SiteIconControl
4749 + * @augments wp.customize.CroppedImageControl
4750 + */
4751 + api.SiteIconControl = api.CroppedImageControl.extend(/** @lends wp.customize.SiteIconControl.prototype */{
4752 +
4753 + /**
4754 + * Create a media modal select frame, and store it so the instance can be reused when needed.
4755 + */
4756 + initFrame: function() {
4757 + var l10n = _wpMediaViewsL10n;
4758 +
4759 + this.frame = wp.media({
4760 + button: {
4761 + text: l10n.select,
4762 + close: false
4763 + },
4764 + states: [
4765 + new wp.media.controller.Library({
4766 + title: this.params.button_labels.frame_title,
4767 + library: wp.media.query({ type: 'image' }),
4768 + multiple: false,
4769 + date: false,
4770 + priority: 20,
4771 + suggestedWidth: this.params.width,
4772 + suggestedHeight: this.params.height
4773 + }),
4774 + new wp.media.controller.SiteIconCropper({
4775 + imgSelectOptions: this.calculateImageSelectOptions,
4776 + control: this
4777 + })
4778 + ]
4779 + });
4780 +
4781 + this.frame.on( 'select', this.onSelect, this );
4782 + this.frame.on( 'cropped', this.onCropped, this );
4783 + this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
4784 + },
4785 +
4786 + /**
4787 + * After an image is selected in the media modal, switch to the cropper
4788 + * state if the image isn't the right size.
4789 + */
4790 + onSelect: function() {
4791 + var attachment = this.frame.state().get( 'selection' ).first().toJSON(),
4792 + controller = this;
4793 +
4794 + if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
4795 + wp.ajax.post( 'crop-image', {
4796 + nonce: attachment.nonces.edit,
4797 + id: attachment.id,
4798 + context: 'site-icon',
4799 + cropDetails: {
4800 + x1: 0,
4801 + y1: 0,
4802 + width: this.params.width,
4803 + height: this.params.height,
4804 + dst_width: this.params.width,
4805 + dst_height: this.params.height
4806 + }
4807 + } ).done( function( croppedImage ) {
4808 + controller.setImageFromAttachment( croppedImage );
4809 + controller.frame.close();
4810 + } ).fail( function() {
4811 + controller.frame.trigger('content:error:crop');
4812 + } );
4813 + } else {
4814 + this.frame.setState( 'cropper' );
4815 + }
4816 + },
4817 +
4818 + /**
4819 + * Updates the setting and re-renders the control UI.
4820 + *
4821 + * @param {Object} attachment
4822 + */
4823 + setImageFromAttachment: function( attachment ) {
4824 + var control = this,
4825 + sizes = [ 'site_icon-32', 'thumbnail', 'full' ], link,
4826 + icon;
4827 +
4828 + _.each( sizes, function( size ) {
4829 + if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) {
4830 + icon = attachment.sizes[ size ];
4831 + }
4832 + } );
4833 +
4834 + this.params.attachment = attachment;
4835 +
4836 + // Set the Customizer setting; the callback takes care of rendering.
4837 + this.setting( attachment.id );
4838 +
4839 + if ( ! icon ) {
4840 + return;
4841 + }
4842 +
4843 + // Update the icon in-browser.
4844 + link = $( 'link[rel="icon"][sizes="32x32"]' );
4845 + link.attr( 'href', icon.url );
4846 +
4847 + // Set focus to the first relevant button after the icon.
4848 + _.defer( function() {
4849 + var firstButton = control.container.find( '.actions .button' ).first();
4850 + if ( firstButton.length ) {
4851 + firstButton.focus();
4852 + }
4853 + } );
4854 + },
4855 +
4856 + /**
4857 + * Called when the "Remove" link is clicked. Empties the setting.
4858 + *
4859 + * @param {Object} event jQuery Event object
4860 + */
4861 + removeFile: function( event ) {
4862 + if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
4863 + return;
4864 + }
4865 + event.preventDefault();
4866 +
4867 + this.params.attachment = {};
4868 + this.setting( '' );
4869 + this.renderContent(); // Not bound to setting change when emptying.
4870 + $( 'link[rel="icon"][sizes="32x32"]' ).attr( 'href', '/favicon.ico' ); // Set to default.
4871 + }
4872 + });
4873 +
4874 + /**
4875 + * @class wp.customize.HeaderControl
4876 + * @augments wp.customize.Control
4877 + */
4878 + api.HeaderControl = api.Control.extend(/** @lends wp.customize.HeaderControl.prototype */{
4879 + ready: function() {
4880 + this.btnRemove = $('#customize-control-header_image .actions .remove');
4881 + this.btnNew = $('#customize-control-header_image .actions .new');
4882 +
4883 + _.bindAll(this, 'openMedia', 'removeImage');
4884 +
4885 + this.btnNew.on( 'click', this.openMedia );
4886 + this.btnRemove.on( 'click', this.removeImage );
4887 +
4888 + api.HeaderTool.currentHeader = this.getInitialHeaderImage();
4889 +
4890 + new api.HeaderTool.CurrentView({
4891 + model: api.HeaderTool.currentHeader,
4892 + el: '#customize-control-header_image .current .container'
4893 + });
4894 +
4895 + new api.HeaderTool.ChoiceListView({
4896 + collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
4897 + el: '#customize-control-header_image .choices .uploaded .list'
4898 + });
4899 +
4900 + new api.HeaderTool.ChoiceListView({
4901 + collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
4902 + el: '#customize-control-header_image .choices .default .list'
4903 + });
4904 +
4905 + api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
4906 + api.HeaderTool.UploadsList,
4907 + api.HeaderTool.DefaultsList
4908 + ]);
4909 +
4910 + // Ensure custom-header-crop Ajax requests bootstrap the Customizer to activate the previewed theme.
4911 + wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize = 'on';
4912 + wp.media.controller.Cropper.prototype.defaults.doCropArgs.customize_theme = api.settings.theme.stylesheet;
4913 + },
4914 +
4915 + /**
4916 + * Returns a new instance of api.HeaderTool.ImageModel based on the currently
4917 + * saved header image (if any).
4918 + *
4919 + * @since 4.2.0
4920 + *
4921 + * @return {Object} Options
4922 + */
4923 + getInitialHeaderImage: function() {
4924 + if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
4925 + return new api.HeaderTool.ImageModel();
4926 + }
4927 +
4928 + // Get the matching uploaded image object.
4929 + var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
4930 + return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
4931 + } );
4932 + // Fall back to raw current header image.
4933 + if ( ! currentHeaderObject ) {
4934 + currentHeaderObject = {
4935 + url: api.get().header_image,
4936 + thumbnail_url: api.get().header_image,
4937 + attachment_id: api.get().header_image_data.attachment_id
4938 + };
4939 + }
4940 +
4941 + return new api.HeaderTool.ImageModel({
4942 + header: currentHeaderObject,
4943 + choice: currentHeaderObject.url.split( '/' ).pop()
4944 + });
4945 + },
4946 +
4947 + /**
4948 + * Returns a set of options, computed from the attached image data and
4949 + * theme-specific data, to be fed to the imgAreaSelect plugin in
4950 + * wp.media.view.Cropper.
4951 + *
4952 + * @param {wp.media.model.Attachment} attachment
4953 + * @param {wp.media.controller.Cropper} controller
4954 + * @return {Object} Options
4955 + */
4956 + calculateImageSelectOptions: function(attachment, controller) {
4957 + var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
4958 + yInit = parseInt(_wpCustomizeHeader.data.height, 10),
4959 + flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
4960 + flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
4961 + ratio, xImg, yImg, realHeight, realWidth,
4962 + imgSelectOptions;
4963 +
4964 + realWidth = attachment.get('width');
4965 + realHeight = attachment.get('height');
4966 +
4967 + this.headerImage = new api.HeaderTool.ImageModel();
4968 + this.headerImage.set({
4969 + themeWidth: xInit,
4970 + themeHeight: yInit,
4971 + themeFlexWidth: flexWidth,
4972 + themeFlexHeight: flexHeight,
4973 + imageWidth: realWidth,
4974 + imageHeight: realHeight
4975 + });
4976 +
4977 + controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
4978 +
4979 + ratio = xInit / yInit;
4980 + xImg = realWidth;
4981 + yImg = realHeight;
4982 +
4983 + if ( xImg / yImg > ratio ) {
4984 + yInit = yImg;
4985 + xInit = yInit * ratio;
4986 + } else {
4987 + xInit = xImg;
4988 + yInit = xInit / ratio;
4989 + }
4990 +
4991 + imgSelectOptions = {
4992 + handles: true,
4993 + keys: true,
4994 + instance: true,
4995 + persistent: true,
4996 + imageWidth: realWidth,
4997 + imageHeight: realHeight,
4998 + x1: 0,
4999 + y1: 0,
5000 + x2: xInit,
5001 + y2: yInit
5002 + };
5003 +
5004 + if (flexHeight === false && flexWidth === false) {
5005 + imgSelectOptions.aspectRatio = xInit + ':' + yInit;
5006 + }
5007 + if (flexHeight === false ) {
5008 + imgSelectOptions.maxHeight = yInit;
5009 + }
5010 + if (flexWidth === false ) {
5011 + imgSelectOptions.maxWidth = xInit;
5012 + }
5013 +
5014 + return imgSelectOptions;
5015 + },
5016 +
5017 + /**
5018 + * Sets up and opens the Media Manager in order to select an image.
5019 + * Depending on both the size of the image and the properties of the
5020 + * current theme, a cropping step after selection may be required or
5021 + * skippable.
5022 + *
5023 + * @param {event} event
5024 + */
5025 + openMedia: function(event) {
5026 + var l10n = _wpMediaViewsL10n;
5027 +
5028 + event.preventDefault();
5029 +
5030 + this.frame = wp.media({
5031 + button: {
5032 + text: l10n.selectAndCrop,
5033 + close: false
5034 + },
5035 + states: [
5036 + new wp.media.controller.Library({
5037 + title: l10n.chooseImage,
5038 + library: wp.media.query({ type: 'image' }),
5039 + multiple: false,
5040 + date: false,
5041 + priority: 20,
5042 + suggestedWidth: _wpCustomizeHeader.data.width,
5043 + suggestedHeight: _wpCustomizeHeader.data.height
5044 + }),
5045 + new wp.media.controller.Cropper({
5046 + imgSelectOptions: this.calculateImageSelectOptions
5047 + })
5048 + ]
5049 + });
5050 +
5051 + this.frame.on('select', this.onSelect, this);
5052 + this.frame.on('cropped', this.onCropped, this);
5053 + this.frame.on('skippedcrop', this.onSkippedCrop, this);
5054 +
5055 + this.frame.open();
5056 + },
5057 +
5058 + /**
5059 + * After an image is selected in the media modal,
5060 + * switch to the cropper state.
5061 + */
5062 + onSelect: function() {
5063 + this.frame.setState('cropper');
5064 + },
5065 +
5066 + /**
5067 + * After the image has been cropped, apply the cropped image data to the setting.
5068 + *
5069 + * @param {Object} croppedImage Cropped attachment data.
5070 + */
5071 + onCropped: function(croppedImage) {
5072 + var url = croppedImage.url,
5073 + attachmentId = croppedImage.attachment_id,
5074 + w = croppedImage.width,
5075 + h = croppedImage.height;
5076 + this.setImageFromURL(url, attachmentId, w, h);
5077 + },
5078 +
5079 + /**
5080 + * If cropping was skipped, apply the image data directly to the setting.
5081 + *
5082 + * @param {Object} selection
5083 + */
5084 + onSkippedCrop: function(selection) {
5085 + var url = selection.get('url'),
5086 + w = selection.get('width'),
5087 + h = selection.get('height');
5088 + this.setImageFromURL(url, selection.id, w, h);
5089 + },
5090 +
5091 + /**
5092 + * Creates a new wp.customize.HeaderTool.ImageModel from provided
5093 + * header image data and inserts it into the user-uploaded headers
5094 + * collection.
5095 + *
5096 + * @param {string} url
5097 + * @param {number} attachmentId
5098 + * @param {number} width
5099 + * @param {number} height
5100 + */
5101 + setImageFromURL: function(url, attachmentId, width, height) {
5102 + var choice, data = {};
5103 +
5104 + data.url = url;
5105 + data.thumbnail_url = url;
5106 + data.timestamp = _.now();
5107 +
5108 + if (attachmentId) {
5109 + data.attachment_id = attachmentId;
5110 + }
5111 +
5112 + if (width) {
5113 + data.width = width;
5114 + }
5115 +
5116 + if (height) {
5117 + data.height = height;
5118 + }
5119 +
5120 + choice = new api.HeaderTool.ImageModel({
5121 + header: data,
5122 + choice: url.split('/').pop()
5123 + });
5124 + api.HeaderTool.UploadsList.add(choice);
5125 + api.HeaderTool.currentHeader.set(choice.toJSON());
5126 + choice.save();
5127 + choice.importImage();
5128 + },
5129 +
5130 + /**
5131 + * Triggers the necessary events to deselect an image which was set as
5132 + * the currently selected one.
5133 + */
5134 + removeImage: function() {
5135 + api.HeaderTool.currentHeader.trigger('hide');
5136 + api.HeaderTool.CombinedList.trigger('control:removeImage');
5137 + }
5138 +
5139 + });
5140 +
5141 + /**
5142 + * wp.customize.ThemeControl
5143 + *
5144 + * @class wp.customize.ThemeControl
5145 + * @augments wp.customize.Control
5146 + */
5147 + api.ThemeControl = api.Control.extend(/** @lends wp.customize.ThemeControl.prototype */{
5148 +
5149 + touchDrag: false,
5150 + screenshotRendered: false,
5151 +
5152 + /**
5153 + * @since 4.2.0
5154 + */
5155 + ready: function() {
5156 + var control = this, panel = api.panel( 'themes' );
5157 +
5158 + function disableSwitchButtons() {
5159 + return ! panel.canSwitchTheme( control.params.theme.id );
5160 + }
5161 +
5162 + // Temporary special function since supplying SFTP credentials does not work yet. See #42184.
5163 + function disableInstallButtons() {
5164 + return disableSwitchButtons() || false === api.settings.theme._canInstall || true === api.settings.theme._filesystemCredentialsNeeded;
5165 + }
5166 + function updateButtons() {
5167 + control.container.find( 'button.preview, button.preview-theme' ).toggleClass( 'disabled', disableSwitchButtons() );
5168 + control.container.find( 'button.theme-install' ).toggleClass( 'disabled', disableInstallButtons() );
5169 + }
5170 +
5171 + api.state( 'selectedChangesetStatus' ).bind( updateButtons );
5172 + api.state( 'changesetStatus' ).bind( updateButtons );
5173 + updateButtons();
5174 +
5175 + control.container.on( 'touchmove', '.theme', function() {
5176 + control.touchDrag = true;
5177 + });
5178 +
5179 + // Bind details view trigger.
5180 + control.container.on( 'click keydown touchend', '.theme', function( event ) {
5181 + var section;
5182 + if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
5183 + return;
5184 + }
5185 +
5186 + // Bail if the user scrolled on a touch device.
5187 + if ( control.touchDrag === true ) {
5188 + return control.touchDrag = false;
5189 + }
5190 +
5191 + // Prevent the modal from showing when the user clicks the action button.
5192 + if ( $( event.target ).is( '.theme-actions .button, .update-theme' ) ) {
5193 + return;
5194 + }
5195 +
5196 + event.preventDefault(); // Keep this AFTER the key filter above.
5197 + section = api.section( control.section() );
5198 + section.showDetails( control.params.theme, function() {
5199 +
5200 + // Temporary special function since supplying SFTP credentials does not work yet. See #42184.
5201 + if ( api.settings.theme._filesystemCredentialsNeeded ) {
5202 + section.overlay.find( '.theme-actions .delete-theme' ).remove();
5203 + }
5204 + } );
5205 + });
5206 +
5207 + control.container.on( 'render-screenshot', function() {
5208 + var $screenshot = $( this ).find( 'img' ),
5209 + source = $screenshot.data( 'src' );
5210 +
5211 + if ( source ) {
5212 + $screenshot.attr( 'src', source );
5213 + }
5214 + control.screenshotRendered = true;
5215 + });
5216 + },
5217 +
5218 + /**
5219 + * Show or hide the theme based on the presence of the term in the title, description, tags, and author.
5220 + *
5221 + * @since 4.2.0
5222 + * @param {Array} terms - An array of terms to search for.
5223 + * @return {boolean} Whether a theme control was activated or not.
5224 + */
5225 + filter: function( terms ) {
5226 + var control = this,
5227 + matchCount = 0,
5228 + haystack = control.params.theme.name + ' ' +
5229 + control.params.theme.description + ' ' +
5230 + control.params.theme.tags + ' ' +
5231 + control.params.theme.author + ' ';
5232 + haystack = haystack.toLowerCase().replace( '-', ' ' );
5233 +
5234 + // Back-compat for behavior in WordPress 4.2.0 to 4.8.X.
5235 + if ( ! _.isArray( terms ) ) {
5236 + terms = [ terms ];
5237 + }
5238 +
5239 + // Always give exact name matches highest ranking.
5240 + if ( control.params.theme.name.toLowerCase() === terms.join( ' ' ) ) {
5241 + matchCount = 100;
5242 + } else {
5243 +
5244 + // Search for and weight (by 10) complete term matches.
5245 + matchCount = matchCount + 10 * ( haystack.split( terms.join( ' ' ) ).length - 1 );
5246 +
5247 + // Search for each term individually (as whole-word and partial match) and sum weighted match counts.
5248 + _.each( terms, function( term ) {
5249 + matchCount = matchCount + 2 * ( haystack.split( term + ' ' ).length - 1 ); // Whole-word, double-weighted.
5250 + matchCount = matchCount + haystack.split( term ).length - 1; // Partial word, to minimize empty intermediate searches while typing.
5251 + });
5252 +
5253 + // Upper limit on match ranking.
5254 + if ( matchCount > 99 ) {
5255 + matchCount = 99;
5256 + }
5257 + }
5258 +
5259 + if ( 0 !== matchCount ) {
5260 + control.activate();
5261 + control.params.priority = 101 - matchCount; // Sort results by match count.
5262 + return true;
5263 + } else {
5264 + control.deactivate(); // Hide control.
5265 + control.params.priority = 101;
5266 + return false;
5267 + }
5268 + },
5269 +
5270 + /**
5271 + * Rerender the theme from its JS template with the installed type.
5272 + *
5273 + * @since 4.9.0
5274 + *
5275 + * @return {void}
5276 + */
5277 + rerenderAsInstalled: function( installed ) {
5278 + var control = this, section;
5279 + if ( installed ) {
5280 + control.params.theme.type = 'installed';
5281 + } else {
5282 + section = api.section( control.params.section );
5283 + control.params.theme.type = section.params.action;
5284 + }
5285 + control.renderContent(); // Replaces existing content.
5286 + control.container.trigger( 'render-screenshot' );
5287 + }
5288 + });
5289 +
5290 + /**
5291 + * Class wp.customize.CodeEditorControl
5292 + *
5293 + * @since 4.9.0
5294 + *
5295 + * @class wp.customize.CodeEditorControl
5296 + * @augments wp.customize.Control
5297 + */
5298 + api.CodeEditorControl = api.Control.extend(/** @lends wp.customize.CodeEditorControl.prototype */{
5299 +
5300 + /**
5301 + * Initialize.
5302 + *
5303 + * @since 4.9.0
5304 + * @param {string} id - Unique identifier for the control instance.
5305 + * @param {Object} options - Options hash for the control instance.
5306 + * @return {void}
5307 + */
5308 + initialize: function( id, options ) {
5309 + var control = this;
5310 + control.deferred = _.extend( control.deferred || {}, {
5311 + codemirror: $.Deferred()
5312 + } );
5313 + api.Control.prototype.initialize.call( control, id, options );
5314 +
5315 + // Note that rendering is debounced so the props will be used when rendering happens after add event.
5316 + control.notifications.bind( 'add', function( notification ) {
5317 +
5318 + // Skip if control notification is not from setting csslint_error notification.
5319 + if ( notification.code !== control.setting.id + ':csslint_error' ) {
5320 + return;
5321 + }
5322 +
5323 + // Customize the template and behavior of csslint_error notifications.
5324 + notification.templateId = 'customize-code-editor-lint-error-notification';
5325 + notification.render = (function( render ) {
5326 + return function() {
5327 + var li = render.call( this );
5328 + li.find( 'input[type=checkbox]' ).on( 'click', function() {
5329 + control.setting.notifications.remove( 'csslint_error' );
5330 + } );
5331 + return li;
5332 + };
5333 + })( notification.render );
5334 + } );
5335 + },
5336 +
5337 + /**
5338 + * Initialize the editor when the containing section is ready and expanded.
5339 + *
5340 + * @since 4.9.0
5341 + * @return {void}
5342 + */
5343 + ready: function() {
5344 + var control = this;
5345 + if ( ! control.section() ) {
5346 + control.initEditor();
5347 + return;
5348 + }
5349 +
5350 + // Wait to initialize editor until section is embedded and expanded.
5351 + api.section( control.section(), function( section ) {
5352 + section.deferred.embedded.done( function() {
5353 + var onceExpanded;
5354 + if ( section.expanded() ) {
5355 + control.initEditor();
5356 + } else {
5357 + onceExpanded = function( isExpanded ) {
5358 + if ( isExpanded ) {
5359 + control.initEditor();
5360 + section.expanded.unbind( onceExpanded );
5361 + }
5362 + };
5363 + section.expanded.bind( onceExpanded );
5364 + }
5365 + } );
5366 + } );
5367 + },
5368 +
5369 + /**
5370 + * Initialize editor.
5371 + *
5372 + * @since 4.9.0
5373 + * @return {void}
5374 + */
5375 + initEditor: function() {
5376 + var control = this, element, editorSettings = false;
5377 +
5378 + // Obtain editorSettings for instantiation.
5379 + if ( wp.codeEditor && ( _.isUndefined( control.params.editor_settings ) || false !== control.params.editor_settings ) ) {
5380 +
5381 + // Obtain default editor settings.
5382 + editorSettings = wp.codeEditor.defaultSettings ? _.clone( wp.codeEditor.defaultSettings ) : {};
5383 + editorSettings.codemirror = _.extend(
5384 + {},
5385 + editorSettings.codemirror,
5386 + {
5387 + indentUnit: 2,
5388 + tabSize: 2
5389 + }
5390 + );
5391 +
5392 + // Merge editor_settings param on top of defaults.
5393 + if ( _.isObject( control.params.editor_settings ) ) {
5394 + _.each( control.params.editor_settings, function( value, key ) {
5395 + if ( _.isObject( value ) ) {
5396 + editorSettings[ key ] = _.extend(
5397 + {},
5398 + editorSettings[ key ],
5399 + value
5400 + );
5401 + }
5402 + } );
5403 + }
5404 + }
5405 +
5406 + element = new api.Element( control.container.find( 'textarea' ) );
5407 + control.elements.push( element );
5408 + element.sync( control.setting );
5409 + element.set( control.setting() );
5410 +
5411 + if ( editorSettings ) {
5412 + control.initSyntaxHighlightingEditor( editorSettings );
5413 + } else {
5414 + control.initPlainTextareaEditor();
5415 + }
5416 + },
5417 +
5418 + /**
5419 + * Make sure editor gets focused when control is focused.
5420 + *
5421 + * @since 4.9.0
5422 + * @param {Object} [params] - Focus params.
5423 + * @param {Function} [params.completeCallback] - Function to call when expansion is complete.
5424 + * @return {void}
5425 + */
5426 + focus: function( params ) {
5427 + var control = this, extendedParams = _.extend( {}, params ), originalCompleteCallback;
5428 + originalCompleteCallback = extendedParams.completeCallback;
5429 + extendedParams.completeCallback = function() {
5430 + if ( originalCompleteCallback ) {
5431 + originalCompleteCallback();
5432 + }
5433 + if ( control.editor ) {
5434 + control.editor.codemirror.focus();
5435 + }
5436 + };
5437 + api.Control.prototype.focus.call( control, extendedParams );
5438 + },
5439 +
5440 + /**
5441 + * Initialize syntax-highlighting editor.
5442 + *
5443 + * @since 4.9.0
5444 + * @param {Object} codeEditorSettings - Code editor settings.
5445 + * @return {void}
5446 + */
5447 + initSyntaxHighlightingEditor: function( codeEditorSettings ) {
5448 + var control = this, $textarea = control.container.find( 'textarea' ), settings, suspendEditorUpdate = false;
5449 +
5450 + settings = _.extend( {}, codeEditorSettings, {
5451 + onTabNext: _.bind( control.onTabNext, control ),
5452 + onTabPrevious: _.bind( control.onTabPrevious, control ),
5453 + onUpdateErrorNotice: _.bind( control.onUpdateErrorNotice, control )
5454 + });
5455 +
5456 + control.editor = wp.codeEditor.initialize( $textarea, settings );
5457 +
5458 + // Improve the editor accessibility.
5459 + $( control.editor.codemirror.display.lineDiv )
5460 + .attr({
5461 + role: 'textbox',
5462 + 'aria-multiline': 'true',
5463 + 'aria-label': control.params.label,
5464 + 'aria-describedby': 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4'
5465 + });
5466 +
5467 + // Focus the editor when clicking on its label.
5468 + control.container.find( 'label' ).on( 'click', function() {
5469 + control.editor.codemirror.focus();
5470 + });
5471 +
5472 + /*
5473 + * When the CodeMirror instance changes, mirror to the textarea,
5474 + * where we have our "true" change event handler bound.
5475 + */
5476 + control.editor.codemirror.on( 'change', function( codemirror ) {
5477 + suspendEditorUpdate = true;
5478 + $textarea.val( codemirror.getValue() ).trigger( 'change' );
5479 + suspendEditorUpdate = false;
5480 + });
5481 +
5482 + // Update CodeMirror when the setting is changed by another plugin.
5483 + control.setting.bind( function( value ) {
5484 + if ( ! suspendEditorUpdate ) {
5485 + control.editor.codemirror.setValue( value );
5486 + }
5487 + });
5488 +
5489 + // Prevent collapsing section when hitting Esc to tab out of editor.
5490 + control.editor.codemirror.on( 'keydown', function onKeydown( codemirror, event ) {
5491 + var escKeyCode = 27;
5492 + if ( escKeyCode === event.keyCode ) {
5493 + event.stopPropagation();
5494 + }
5495 + });
5496 +
5497 + control.deferred.codemirror.resolveWith( control, [ control.editor.codemirror ] );
5498 + },
5499 +
5500 + /**
5501 + * Handle tabbing to the field after the editor.
5502 + *
5503 + * @since 4.9.0
5504 + * @return {void}
5505 + */
5506 + onTabNext: function onTabNext() {
5507 + var control = this, controls, controlIndex, section;
5508 + section = api.section( control.section() );
5509 + controls = section.controls();
5510 + controlIndex = controls.indexOf( control );
5511 + if ( controls.length === controlIndex + 1 ) {
5512 + $( '#customize-footer-actions .collapse-sidebar' ).trigger( 'focus' );
5513 + } else {
5514 + controls[ controlIndex + 1 ].container.find( ':focusable:first' ).focus();
5515 + }
5516 + },
5517 +
5518 + /**
5519 + * Handle tabbing to the field before the editor.
5520 + *
5521 + * @since 4.9.0
5522 + * @return {void}
5523 + */
5524 + onTabPrevious: function onTabPrevious() {
5525 + var control = this, controls, controlIndex, section;
5526 + section = api.section( control.section() );
5527 + controls = section.controls();
5528 + controlIndex = controls.indexOf( control );
5529 + if ( 0 === controlIndex ) {
5530 + section.contentContainer.find( '.customize-section-title .customize-help-toggle, .customize-section-title .customize-section-description.open .section-description-close' ).last().focus();
5531 + } else {
5532 + controls[ controlIndex - 1 ].contentContainer.find( ':focusable:first' ).focus();
5533 + }
5534 + },
5535 +
5536 + /**
5537 + * Update error notice.
5538 + *
5539 + * @since 4.9.0
5540 + * @param {Array} errorAnnotations - Error annotations.
5541 + * @return {void}
5542 + */
5543 + onUpdateErrorNotice: function onUpdateErrorNotice( errorAnnotations ) {
5544 + var control = this, message;
5545 + control.setting.notifications.remove( 'csslint_error' );
5546 +
5547 + if ( 0 !== errorAnnotations.length ) {
5548 + if ( 1 === errorAnnotations.length ) {
5549 + message = api.l10n.customCssError.singular.replace( '%d', '1' );
5550 + } else {
5551 + message = api.l10n.customCssError.plural.replace( '%d', String( errorAnnotations.length ) );
5552 + }
5553 + control.setting.notifications.add( new api.Notification( 'csslint_error', {
5554 + message: message,
5555 + type: 'error'
5556 + } ) );
5557 + }
5558 + },
5559 +
5560 + /**
5561 + * Initialize plain-textarea editor when syntax highlighting is disabled.
5562 + *
5563 + * @since 4.9.0
5564 + * @return {void}
5565 + */
5566 + initPlainTextareaEditor: function() {
5567 + var control = this, $textarea = control.container.find( 'textarea' ), textarea = $textarea[0];
5568 +
5569 + $textarea.on( 'blur', function onBlur() {
5570 + $textarea.data( 'next-tab-blurs', false );
5571 + } );
5572 +
5573 + $textarea.on( 'keydown', function onKeydown( event ) {
5574 + var selectionStart, selectionEnd, value, tabKeyCode = 9, escKeyCode = 27;
5575 +
5576 + if ( escKeyCode === event.keyCode ) {
5577 + if ( ! $textarea.data( 'next-tab-blurs' ) ) {
5578 + $textarea.data( 'next-tab-blurs', true );
5579 + event.stopPropagation(); // Prevent collapsing the section.
5580 + }
5581 + return;
5582 + }
5583 +
5584 + // Short-circuit if tab key is not being pressed or if a modifier key *is* being pressed.
5585 + if ( tabKeyCode !== event.keyCode || event.ctrlKey || event.altKey || event.shiftKey ) {
5586 + return;
5587 + }
5588 +
5589 + // Prevent capturing Tab characters if Esc was pressed.
5590 + if ( $textarea.data( 'next-tab-blurs' ) ) {
5591 + return;
5592 + }
5593 +
5594 + selectionStart = textarea.selectionStart;
5595 + selectionEnd = textarea.selectionEnd;
5596 + value = textarea.value;
5597 +
5598 + if ( selectionStart >= 0 ) {
5599 + textarea.value = value.substring( 0, selectionStart ).concat( '\t', value.substring( selectionEnd ) );
5600 + $textarea.selectionStart = textarea.selectionEnd = selectionStart + 1;
5601 + }
5602 +
5603 + event.stopPropagation();
5604 + event.preventDefault();
5605 + });
5606 +
5607 + control.deferred.codemirror.rejectWith( control );
5608 + }
5609 + });
5610 +
5611 + /**
5612 + * Class wp.customize.DateTimeControl.
5613 + *
5614 + * @since 4.9.0
5615 + * @class wp.customize.DateTimeControl
5616 + * @augments wp.customize.Control
5617 + */
5618 + api.DateTimeControl = api.Control.extend(/** @lends wp.customize.DateTimeControl.prototype */{
5619 +
5620 + /**
5621 + * Initialize behaviors.
5622 + *
5623 + * @since 4.9.0
5624 + * @return {void}
5625 + */
5626 + ready: function ready() {
5627 + var control = this;
5628 +
5629 + control.inputElements = {};
5630 + control.invalidDate = false;
5631 +
5632 + _.bindAll( control, 'populateSetting', 'updateDaysForMonth', 'populateDateInputs' );
5633 +
5634 + if ( ! control.setting ) {
5635 + throw new Error( 'Missing setting' );
5636 + }
5637 +
5638 + control.container.find( '.date-input' ).each( function() {
5639 + var input = $( this ), component, element;
5640 + component = input.data( 'component' );
5641 + element = new api.Element( input );
5642 + control.inputElements[ component ] = element;
5643 + control.elements.push( element );
5644 +
5645 + // Add invalid date error once user changes (and has blurred the input).
5646 + input.on( 'change', function() {
5647 + if ( control.invalidDate ) {
5648 + control.notifications.add( new api.Notification( 'invalid_date', {
5649 + message: api.l10n.invalidDate
5650 + } ) );
5651 + }
5652 + } );
5653 +
5654 + // Remove the error immediately after validity change.
5655 + input.on( 'input', _.debounce( function() {
5656 + if ( ! control.invalidDate ) {
5657 + control.notifications.remove( 'invalid_date' );
5658 + }
5659 + } ) );
5660 +
5661 + // Add zero-padding when blurring field.
5662 + input.on( 'blur', _.debounce( function() {
5663 + if ( ! control.invalidDate ) {
5664 + control.populateDateInputs();
5665 + }
5666 + } ) );
5667 + } );
5668 +
5669 + control.inputElements.month.bind( control.updateDaysForMonth );
5670 + control.inputElements.year.bind( control.updateDaysForMonth );
5671 + control.populateDateInputs();
5672 + control.setting.bind( control.populateDateInputs );
5673 +
5674 + // Start populating setting after inputs have been populated.
5675 + _.each( control.inputElements, function( element ) {
5676 + element.bind( control.populateSetting );
5677 + } );
5678 + },
5679 +
5680 + /**
5681 + * Parse datetime string.
5682 + *
5683 + * @since 4.9.0
5684 + *
5685 + * @param {string} datetime - Date/Time string. Accepts Y-m-d[ H:i[:s]] format.
5686 + * @return {Object|null} Returns object containing date components or null if parse error.
5687 + */
5688 + parseDateTime: function parseDateTime( datetime ) {
5689 + var control = this, matches, date, midDayHour = 12;
5690 +
5691 + if ( datetime ) {
5692 + matches = datetime.match( /^(\d\d\d\d)-(\d\d)-(\d\d)(?: (\d\d):(\d\d)(?::(\d\d))?)?$/ );
5693 + }
5694 +
5695 + if ( ! matches ) {
5696 + return null;
5697 + }
5698 +
5699 + matches.shift();
5700 +
5701 + date = {
5702 + year: matches.shift(),
5703 + month: matches.shift(),
5704 + day: matches.shift(),
5705 + hour: matches.shift() || '00',
5706 + minute: matches.shift() || '00',
5707 + second: matches.shift() || '00'
5708 + };
5709 +
5710 + if ( control.params.includeTime && control.params.twelveHourFormat ) {
5711 + date.hour = parseInt( date.hour, 10 );
5712 + date.meridian = date.hour >= midDayHour ? 'pm' : 'am';
5713 + date.hour = date.hour % midDayHour ? String( date.hour % midDayHour ) : String( midDayHour );
5714 + delete date.second; // @todo Why only if twelveHourFormat?
5715 + }
5716 +
5717 + return date;
5718 + },
5719 +
5720 + /**
5721 + * Validates if input components have valid date and time.
5722 + *
5723 + * @since 4.9.0
5724 + * @return {boolean} If date input fields has error.
5725 + */
5726 + validateInputs: function validateInputs() {
5727 + var control = this, components, validityInput;
5728 +
5729 + control.invalidDate = false;
5730 +
5731 + components = [ 'year', 'day' ];
5732 + if ( control.params.includeTime ) {
5733 + components.push( 'hour', 'minute' );
5734 + }
5735 +
5736 + _.find( components, function( component ) {
5737 + var element, max, min, value;
5738 +
5739 + element = control.inputElements[ component ];
5740 + validityInput = element.element.get( 0 );
5741 + max = parseInt( element.element.attr( 'max' ), 10 );
5742 + min = parseInt( element.element.attr( 'min' ), 10 );
5743 + value = parseInt( element(), 10 );
5744 + control.invalidDate = isNaN( value ) || value > max || value < min;
5745 +
5746 + if ( ! control.invalidDate ) {
5747 + validityInput.setCustomValidity( '' );
5748 + }
5749 +
5750 + return control.invalidDate;
5751 + } );
5752 +
5753 + if ( control.inputElements.meridian && ! control.invalidDate ) {
5754 + validityInput = control.inputElements.meridian.element.get( 0 );
5755 + if ( 'am' !== control.inputElements.meridian.get() && 'pm' !== control.inputElements.meridian.get() ) {
5756 + control.invalidDate = true;
5757 + } else {
5758 + validityInput.setCustomValidity( '' );
5759 + }
5760 + }
5761 +
5762 + if ( control.invalidDate ) {
5763 + validityInput.setCustomValidity( api.l10n.invalidValue );
5764 + } else {
5765 + validityInput.setCustomValidity( '' );
5766 + }
5767 + if ( ! control.section() || api.section.has( control.section() ) && api.section( control.section() ).expanded() ) {
5768 + _.result( validityInput, 'reportValidity' );
5769 + }
5770 +
5771 + return control.invalidDate;
5772 + },
5773 +
5774 + /**
5775 + * Updates number of days according to the month and year selected.
5776 + *
5777 + * @since 4.9.0
5778 + * @return {void}
5779 + */
5780 + updateDaysForMonth: function updateDaysForMonth() {
5781 + var control = this, daysInMonth, year, month, day;
5782 +
5783 + month = parseInt( control.inputElements.month(), 10 );
5784 + year = parseInt( control.inputElements.year(), 10 );
5785 + day = parseInt( control.inputElements.day(), 10 );
5786 +
5787 + if ( month && year ) {
5788 + daysInMonth = new Date( year, month, 0 ).getDate();
5789 + control.inputElements.day.element.attr( 'max', daysInMonth );
5790 +
5791 + if ( day > daysInMonth ) {
5792 + control.inputElements.day( String( daysInMonth ) );
5793 + }
5794 + }
5795 + },
5796 +
5797 + /**
5798 + * Populate setting value from the inputs.
5799 + *
5800 + * @since 4.9.0
5801 + * @return {boolean} If setting updated.
5802 + */
5803 + populateSetting: function populateSetting() {
5804 + var control = this, date;
5805 +
5806 + if ( control.validateInputs() || ! control.params.allowPastDate && ! control.isFutureDate() ) {
5807 + return false;
5808 + }
5809 +
5810 + date = control.convertInputDateToString();
5811 + control.setting.set( date );
5812 + return true;
5813 + },
5814 +
5815 + /**
5816 + * Converts input values to string in Y-m-d H:i:s format.
5817 + *
5818 + * @since 4.9.0
5819 + * @return {string} Date string.
5820 + */
5821 + convertInputDateToString: function convertInputDateToString() {
5822 + var control = this, date = '', dateFormat, hourInTwentyFourHourFormat,
5823 + getElementValue, pad;
5824 +
5825 + pad = function( number, padding ) {
5826 + var zeros;
5827 + if ( String( number ).length < padding ) {
5828 + zeros = padding - String( number ).length;
5829 + number = Math.pow( 10, zeros ).toString().substr( 1 ) + String( number );
5830 + }
5831 + return number;
5832 + };
5833 +
5834 + getElementValue = function( component ) {
5835 + var value = parseInt( control.inputElements[ component ].get(), 10 );
5836 +
5837 + if ( _.contains( [ 'month', 'day', 'hour', 'minute' ], component ) ) {
5838 + value = pad( value, 2 );
5839 + } else if ( 'year' === component ) {
5840 + value = pad( value, 4 );
5841 + }
5842 + return value;
5843 + };
5844 +
5845 + dateFormat = [ 'year', '-', 'month', '-', 'day' ];
5846 + if ( control.params.includeTime ) {
5847 + hourInTwentyFourHourFormat = control.inputElements.meridian ? control.convertHourToTwentyFourHourFormat( control.inputElements.hour(), control.inputElements.meridian() ) : control.inputElements.hour();
5848 + dateFormat = dateFormat.concat( [ ' ', pad( hourInTwentyFourHourFormat, 2 ), ':', 'minute', ':', '00' ] );
5849 + }
5850 +
5851 + _.each( dateFormat, function( component ) {
5852 + date += control.inputElements[ component ] ? getElementValue( component ) : component;
5853 + } );
5854 +
5855 + return date;
5856 + },
5857 +
5858 + /**
5859 + * Check if the date is in the future.
5860 + *
5861 + * @since 4.9.0
5862 + * @return {boolean} True if future date.
5863 + */
5864 + isFutureDate: function isFutureDate() {
5865 + var control = this;
5866 + return 0 < api.utils.getRemainingTime( control.convertInputDateToString() );
5867 + },
5868 +
5869 + /**
5870 + * Convert hour in twelve hour format to twenty four hour format.
5871 + *
5872 + * @since 4.9.0
5873 + * @param {string} hourInTwelveHourFormat - Hour in twelve hour format.
5874 + * @param {string} meridian - Either 'am' or 'pm'.
5875 + * @return {string} Hour in twenty four hour format.
5876 + */
5877 + convertHourToTwentyFourHourFormat: function convertHour( hourInTwelveHourFormat, meridian ) {
5878 + var hourInTwentyFourHourFormat, hour, midDayHour = 12;
5879 +
5880 + hour = parseInt( hourInTwelveHourFormat, 10 );
5881 + if ( isNaN( hour ) ) {
5882 + return '';
5883 + }
5884 +
5885 + if ( 'pm' === meridian && hour < midDayHour ) {
5886 + hourInTwentyFourHourFormat = hour + midDayHour;
5887 + } else if ( 'am' === meridian && midDayHour === hour ) {
5888 + hourInTwentyFourHourFormat = hour - midDayHour;
5889 + } else {
5890 + hourInTwentyFourHourFormat = hour;
5891 + }
5892 +
5893 + return String( hourInTwentyFourHourFormat );
5894 + },
5895 +
5896 + /**
5897 + * Populates date inputs in date fields.
5898 + *
5899 + * @since 4.9.0
5900 + * @return {boolean} Whether the inputs were populated.
5901 + */
5902 + populateDateInputs: function populateDateInputs() {
5903 + var control = this, parsed;
5904 +
5905 + parsed = control.parseDateTime( control.setting.get() );
5906 +
5907 + if ( ! parsed ) {
5908 + return false;
5909 + }
5910 +
5911 + _.each( control.inputElements, function( element, component ) {
5912 + var value = parsed[ component ]; // This will be zero-padded string.
5913 +
5914 + // Set month and meridian regardless of focused state since they are dropdowns.
5915 + if ( 'month' === component || 'meridian' === component ) {
5916 +
5917 + // Options in dropdowns are not zero-padded.
5918 + value = value.replace( /^0/, '' );
5919 +
5920 + element.set( value );
5921 + } else {
5922 +
5923 + value = parseInt( value, 10 );
5924 + if ( ! element.element.is( document.activeElement ) ) {
5925 +
5926 + // Populate element with zero-padded value if not focused.
5927 + element.set( parsed[ component ] );
5928 + } else if ( value !== parseInt( element(), 10 ) ) {
5929 +
5930 + // Forcibly update the value if its underlying value changed, regardless of zero-padding.
5931 + element.set( String( value ) );
5932 + }
5933 + }
5934 + } );
5935 +
5936 + return true;
5937 + },
5938 +
5939 + /**
5940 + * Toggle future date notification for date control.
5941 + *
5942 + * @since 4.9.0
5943 + * @param {boolean} notify Add or remove the notification.
5944 + * @return {wp.customize.DateTimeControl}
5945 + */
5946 + toggleFutureDateNotification: function toggleFutureDateNotification( notify ) {
5947 + var control = this, notificationCode, notification;
5948 +
5949 + notificationCode = 'not_future_date';
5950 +
5951 + if ( notify ) {
5952 + notification = new api.Notification( notificationCode, {
5953 + type: 'error',
5954 + message: api.l10n.futureDateError
5955 + } );
5956 + control.notifications.add( notification );
5957 + } else {
5958 + control.notifications.remove( notificationCode );
5959 + }
5960 +
5961 + return control;
5962 + }
5963 + });
5964 +
5965 + /**
5966 + * Class PreviewLinkControl.
5967 + *
5968 + * @since 4.9.0
5969 + * @class wp.customize.PreviewLinkControl
5970 + * @augments wp.customize.Control
5971 + */
5972 + api.PreviewLinkControl = api.Control.extend(/** @lends wp.customize.PreviewLinkControl.prototype */{
5973 +
5974 + defaults: _.extend( {}, api.Control.prototype.defaults, {
5975 + templateId: 'customize-preview-link-control'
5976 + } ),
5977 +
5978 + /**
5979 + * Initialize behaviors.
5980 + *
5981 + * @since 4.9.0
5982 + * @return {void}
5983 + */
5984 + ready: function ready() {
5985 + var control = this, element, component, node, url, input, button;
5986 +
5987 + _.bindAll( control, 'updatePreviewLink' );
5988 +
5989 + if ( ! control.setting ) {
5990 + control.setting = new api.Value();
5991 + }
5992 +
5993 + control.previewElements = {};
5994 +
5995 + control.container.find( '.preview-control-element' ).each( function() {
5996 + node = $( this );
5997 + component = node.data( 'component' );
5998 + element = new api.Element( node );
5999 + control.previewElements[ component ] = element;
6000 + control.elements.push( element );
6001 + } );
6002 +
6003 + url = control.previewElements.url;
6004 + input = control.previewElements.input;
6005 + button = control.previewElements.button;
6006 +
6007 + input.link( control.setting );
6008 + url.link( control.setting );
6009 +
6010 + url.bind( function( value ) {
6011 + url.element.parent().attr( {
6012 + href: value,
6013 + target: api.settings.changeset.uuid
6014 + } );
6015 + } );
6016 +
6017 + api.bind( 'ready', control.updatePreviewLink );
6018 + api.state( 'saved' ).bind( control.updatePreviewLink );
6019 + api.state( 'changesetStatus' ).bind( control.updatePreviewLink );
6020 + api.state( 'activated' ).bind( control.updatePreviewLink );
6021 + api.previewer.previewUrl.bind( control.updatePreviewLink );
6022 +
6023 + button.element.on( 'click', function( event ) {
6024 + event.preventDefault();
6025 + if ( control.setting() ) {
6026 + input.element.select();
6027 + document.execCommand( 'copy' );
6028 + button( button.element.data( 'copied-text' ) );
6029 + }
6030 + } );
6031 +
6032 + url.element.parent().on( 'click', function( event ) {
6033 + if ( $( this ).hasClass( 'disabled' ) ) {
6034 + event.preventDefault();
6035 + }
6036 + } );
6037 +
6038 + button.element.on( 'mouseenter', function() {
6039 + if ( control.setting() ) {
6040 + button( button.element.data( 'copy-text' ) );
6041 + }
6042 + } );
6043 + },
6044 +
6045 + /**
6046 + * Updates Preview Link
6047 + *
6048 + * @since 4.9.0
6049 + * @return {void}
6050 + */
6051 + updatePreviewLink: function updatePreviewLink() {
6052 + var control = this, unsavedDirtyValues;
6053 +
6054 + unsavedDirtyValues = ! api.state( 'saved' ).get() || '' === api.state( 'changesetStatus' ).get() || 'auto-draft' === api.state( 'changesetStatus' ).get();
6055 +
6056 + control.toggleSaveNotification( unsavedDirtyValues );
6057 + control.previewElements.url.element.parent().toggleClass( 'disabled', unsavedDirtyValues );
6058 + control.previewElements.button.element.prop( 'disabled', unsavedDirtyValues );
6059 + control.setting.set( api.previewer.getFrontendPreviewUrl() );
6060 + },
6061 +
6062 + /**
6063 + * Toggles save notification.
6064 + *
6065 + * @since 4.9.0
6066 + * @param {boolean} notify Add or remove notification.
6067 + * @return {void}
6068 + */
6069 + toggleSaveNotification: function toggleSaveNotification( notify ) {
6070 + var control = this, notificationCode, notification;
6071 +
6072 + notificationCode = 'changes_not_saved';
6073 +
6074 + if ( notify ) {
6075 + notification = new api.Notification( notificationCode, {
6076 + type: 'info',
6077 + message: api.l10n.saveBeforeShare
6078 + } );
6079 + control.notifications.add( notification );
6080 + } else {
6081 + control.notifications.remove( notificationCode );
6082 + }
6083 + }
6084 + });
6085 +
6086 + /**
6087 + * Change objects contained within the main customize object to Settings.
6088 + *
6089 + * @alias wp.customize.defaultConstructor
6090 + */
6091 + api.defaultConstructor = api.Setting;
6092 +
6093 + /**
6094 + * Callback for resolved controls.
6095 + *
6096 + * @callback wp.customize.deferredControlsCallback
6097 + * @param {wp.customize.Control[]} controls Resolved controls.
6098 + */
6099 +
6100 + /**
6101 + * Collection of all registered controls.
6102 + *
6103 + * @alias wp.customize.control
6104 + *
6105 + * @since 3.4.0
6106 + *
6107 + * @type {Function}
6108 + * @param {...string} ids - One or more ids for controls to obtain.
6109 + * @param {deferredControlsCallback} [callback] - Function called when all supplied controls exist.
6110 + * @return {wp.customize.Control|undefined|jQuery.promise} Control instance or undefined (if function called with one id param),
6111 + * or promise resolving to requested controls.
6112 + *
6113 + * @example <caption>Loop over all registered controls.</caption>
6114 + * wp.customize.control.each( function( control ) { ... } );
6115 + *
6116 + * @example <caption>Getting `background_color` control instance.</caption>
6117 + * control = wp.customize.control( 'background_color' );
6118 + *
6119 + * @example <caption>Check if control exists.</caption>
6120 + * hasControl = wp.customize.control.has( 'background_color' );
6121 + *
6122 + * @example <caption>Deferred getting of `background_color` control until it exists, using callback.</caption>
6123 + * wp.customize.control( 'background_color', function( control ) { ... } );
6124 + *
6125 + * @example <caption>Get title and tagline controls when they both exist, using promise (only available when multiple IDs are present).</caption>
6126 + * promise = wp.customize.control( 'blogname', 'blogdescription' );
6127 + * promise.done( function( titleControl, taglineControl ) { ... } );
6128 + *
6129 + * @example <caption>Get title and tagline controls when they both exist, using callback.</caption>
6130 + * wp.customize.control( 'blogname', 'blogdescription', function( titleControl, taglineControl ) { ... } );
6131 + *
6132 + * @example <caption>Getting setting value for `background_color` control.</caption>
6133 + * value = wp.customize.control( 'background_color ').setting.get();
6134 + * value = wp.customize( 'background_color' ).get(); // Same as above, since setting ID and control ID are the same.
6135 + *
6136 + * @example <caption>Add new control for site title.</caption>
6137 + * wp.customize.control.add( new wp.customize.Control( 'other_blogname', {
6138 + * setting: 'blogname',
6139 + * type: 'text',
6140 + * label: 'Site title',
6141 + * section: 'other_site_identify'
6142 + * } ) );
6143 + *
6144 + * @example <caption>Remove control.</caption>
6145 + * wp.customize.control.remove( 'other_blogname' );
6146 + *
6147 + * @example <caption>Listen for control being added.</caption>
6148 + * wp.customize.control.bind( 'add', function( addedControl ) { ... } )
6149 + *
6150 + * @example <caption>Listen for control being removed.</caption>
6151 + * wp.customize.control.bind( 'removed', function( removedControl ) { ... } )
6152 + */
6153 + api.control = new api.Values({ defaultConstructor: api.Control });
6154 +
6155 + /**
6156 + * Callback for resolved sections.
6157 + *
6158 + * @callback wp.customize.deferredSectionsCallback
6159 + * @param {wp.customize.Section[]} sections Resolved sections.
6160 + */
6161 +
6162 + /**
6163 + * Collection of all registered sections.
6164 + *
6165 + * @alias wp.customize.section
6166 + *
6167 + * @since 3.4.0
6168 + *
6169 + * @type {Function}
6170 + * @param {...string} ids - One or more ids for sections to obtain.
6171 + * @param {deferredSectionsCallback} [callback] - Function called when all supplied sections exist.
6172 + * @return {wp.customize.Section|undefined|jQuery.promise} Section instance or undefined (if function called with one id param),
6173 + * or promise resolving to requested sections.
6174 + *
6175 + * @example <caption>Loop over all registered sections.</caption>
6176 + * wp.customize.section.each( function( section ) { ... } )
6177 + *
6178 + * @example <caption>Getting `title_tagline` section instance.</caption>
6179 + * section = wp.customize.section( 'title_tagline' )
6180 + *
6181 + * @example <caption>Expand dynamically-created section when it exists.</caption>
6182 + * wp.customize.section( 'dynamically_created', function( section ) {
6183 + * section.expand();
6184 + * } );
6185 + *
6186 + * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances.
6187 + */
6188 + api.section = new api.Values({ defaultConstructor: api.Section });
6189 +
6190 + /**
6191 + * Callback for resolved panels.
6192 + *
6193 + * @callback wp.customize.deferredPanelsCallback
6194 + * @param {wp.customize.Panel[]} panels Resolved panels.
6195 + */
6196 +
6197 + /**
6198 + * Collection of all registered panels.
6199 + *
6200 + * @alias wp.customize.panel
6201 + *
6202 + * @since 4.0.0
6203 + *
6204 + * @type {Function}
6205 + * @param {...string} ids - One or more ids for panels to obtain.
6206 + * @param {deferredPanelsCallback} [callback] - Function called when all supplied panels exist.
6207 + * @return {wp.customize.Panel|undefined|jQuery.promise} Panel instance or undefined (if function called with one id param),
6208 + * or promise resolving to requested panels.
6209 + *
6210 + * @example <caption>Loop over all registered panels.</caption>
6211 + * wp.customize.panel.each( function( panel ) { ... } )
6212 + *
6213 + * @example <caption>Getting nav_menus panel instance.</caption>
6214 + * panel = wp.customize.panel( 'nav_menus' );
6215 + *
6216 + * @example <caption>Expand dynamically-created panel when it exists.</caption>
6217 + * wp.customize.panel( 'dynamically_created', function( panel ) {
6218 + * panel.expand();
6219 + * } );
6220 + *
6221 + * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances.
6222 + */
6223 + api.panel = new api.Values({ defaultConstructor: api.Panel });
6224 +
6225 + /**
6226 + * Callback for resolved notifications.
6227 + *
6228 + * @callback wp.customize.deferredNotificationsCallback
6229 + * @param {wp.customize.Notification[]} notifications Resolved notifications.
6230 + */
6231 +
6232 + /**
6233 + * Collection of all global notifications.
6234 + *
6235 + * @alias wp.customize.notifications
6236 + *
6237 + * @since 4.9.0
6238 + *
6239 + * @type {Function}
6240 + * @param {...string} codes - One or more codes for notifications to obtain.
6241 + * @param {deferredNotificationsCallback} [callback] - Function called when all supplied notifications exist.
6242 + * @return {wp.customize.Notification|undefined|jQuery.promise} Notification instance or undefined (if function called with one code param),
6243 + * or promise resolving to requested notifications.
6244 + *
6245 + * @example <caption>Check if existing notification</caption>
6246 + * exists = wp.customize.notifications.has( 'a_new_day_arrived' );
6247 + *
6248 + * @example <caption>Obtain existing notification</caption>
6249 + * notification = wp.customize.notifications( 'a_new_day_arrived' );
6250 + *
6251 + * @example <caption>Obtain notification that may not exist yet.</caption>
6252 + * wp.customize.notifications( 'a_new_day_arrived', function( notification ) { ... } );
6253 + *
6254 + * @example <caption>Add a warning notification.</caption>
6255 + * wp.customize.notifications.add( new wp.customize.Notification( 'midnight_almost_here', {
6256 + * type: 'warning',
6257 + * message: 'Midnight has almost arrived!',
6258 + * dismissible: true
6259 + * } ) );
6260 + *
6261 + * @example <caption>Remove a notification.</caption>
6262 + * wp.customize.notifications.remove( 'a_new_day_arrived' );
6263 + *
6264 + * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances.
6265 + */
6266 + api.notifications = new api.Notifications();
6267 +
6268 + api.PreviewFrame = api.Messenger.extend(/** @lends wp.customize.PreviewFrame.prototype */{
6269 + sensitivity: null, // Will get set to api.settings.timeouts.previewFrameSensitivity.
6270 +
6271 + /**
6272 + * An object that fetches a preview in the background of the document, which
6273 + * allows for seamless replacement of an existing preview.
6274 + *
6275 + * @constructs wp.customize.PreviewFrame
6276 + * @augments wp.customize.Messenger
6277 + *
6278 + * @param {Object} params.container
6279 + * @param {Object} params.previewUrl
6280 + * @param {Object} params.query
6281 + * @param {Object} options
6282 + */
6283 + initialize: function( params, options ) {
6284 + var deferred = $.Deferred();
6285 +
6286 + /*
6287 + * Make the instance of the PreviewFrame the promise object
6288 + * so other objects can easily interact with it.
6289 + */
6290 + deferred.promise( this );
6291 +
6292 + this.container = params.container;
6293 +
6294 + $.extend( params, { channel: api.PreviewFrame.uuid() });
6295 +
6296 + api.Messenger.prototype.initialize.call( this, params, options );
6297 +
6298 + this.add( 'previewUrl', params.previewUrl );
6299 +
6300 + this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
6301 +
6302 + this.run( deferred );
6303 + },
6304 +
6305 + /**
6306 + * Run the preview request.
6307 + *
6308 + * @param {Object} deferred jQuery Deferred object to be resolved with
6309 + * the request.
6310 + */
6311 + run: function( deferred ) {
6312 + var previewFrame = this,
6313 + loaded = false,
6314 + ready = false,
6315 + readyData = null,
6316 + hasPendingChangesetUpdate = '{}' !== previewFrame.query.customized,
6317 + urlParser,
6318 + params,
6319 + form;
6320 +
6321 + if ( previewFrame._ready ) {
6322 + previewFrame.unbind( 'ready', previewFrame._ready );
6323 + }
6324 +
6325 + previewFrame._ready = function( data ) {
6326 + ready = true;
6327 + readyData = data;
6328 + previewFrame.container.addClass( 'iframe-ready' );
6329 + if ( ! data ) {
6330 + return;
6331 + }
6332 +
6333 + if ( loaded ) {
6334 + deferred.resolveWith( previewFrame, [ data ] );
6335 + }
6336 + };
6337 +
6338 + previewFrame.bind( 'ready', previewFrame._ready );
6339 +
6340 + urlParser = document.createElement( 'a' );
6341 + urlParser.href = previewFrame.previewUrl();
6342 +
6343 + params = _.extend(
6344 + api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
6345 + {
6346 + customize_changeset_uuid: previewFrame.query.customize_changeset_uuid,
6347 + customize_theme: previewFrame.query.customize_theme,
6348 + customize_messenger_channel: previewFrame.query.customize_messenger_channel
6349 + }
6350 + );
6351 + if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) {
6352 + params.customize_autosaved = 'on';
6353 + }
6354 +
6355 + urlParser.search = $.param( params );
6356 + previewFrame.iframe = $( '<iframe />', {
6357 + title: api.l10n.previewIframeTitle,
6358 + name: 'customize-' + previewFrame.channel()
6359 + } );
6360 + previewFrame.iframe.attr( 'onmousewheel', '' ); // Workaround for Safari bug. See WP Trac #38149.
6361 + previewFrame.iframe.attr( 'sandbox', 'allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts' );
6362 +
6363 + if ( ! hasPendingChangesetUpdate ) {
6364 + previewFrame.iframe.attr( 'src', urlParser.href );
6365 + } else {
6366 + previewFrame.iframe.attr( 'data-src', urlParser.href ); // For debugging purposes.
6367 + }
6368 +
6369 + previewFrame.iframe.appendTo( previewFrame.container );
6370 + previewFrame.targetWindow( previewFrame.iframe[0].contentWindow );
6371 +
6372 + /*
6373 + * Submit customized data in POST request to preview frame window since
6374 + * there are setting value changes not yet written to changeset.
6375 + */
6376 + if ( hasPendingChangesetUpdate ) {
6377 + form = $( '<form>', {
6378 + action: urlParser.href,
6379 + target: previewFrame.iframe.attr( 'name' ),
6380 + method: 'post',
6381 + hidden: 'hidden'
6382 + } );
6383 + form.append( $( '<input>', {
6384 + type: 'hidden',
6385 + name: '_method',
6386 + value: 'GET'
6387 + } ) );
6388 + _.each( previewFrame.query, function( value, key ) {
6389 + form.append( $( '<input>', {
6390 + type: 'hidden',
6391 + name: key,
6392 + value: value
6393 + } ) );
6394 + } );
6395 + previewFrame.container.append( form );
6396 + form.trigger( 'submit' );
6397 + form.remove(); // No need to keep the form around after submitted.
6398 + }
6399 +
6400 + previewFrame.bind( 'iframe-loading-error', function( error ) {
6401 + previewFrame.iframe.remove();
6402 +
6403 + // Check if the user is not logged in.
6404 + if ( 0 === error ) {
6405 + previewFrame.login( deferred );
6406 + return;
6407 + }
6408 +
6409 + // Check for cheaters.
6410 + if ( -1 === error ) {
6411 + deferred.rejectWith( previewFrame, [ 'cheatin' ] );
6412 + return;
6413 + }
6414 +
6415 + deferred.rejectWith( previewFrame, [ 'request failure' ] );
6416 + } );
6417 +
6418 + previewFrame.iframe.one( 'load', function() {
6419 + loaded = true;
6420 +
6421 + if ( ready ) {
6422 + deferred.resolveWith( previewFrame, [ readyData ] );
6423 + } else {
6424 + setTimeout( function() {
6425 + deferred.rejectWith( previewFrame, [ 'ready timeout' ] );
6426 + }, previewFrame.sensitivity );
6427 + }
6428 + });
6429 + },
6430 +
6431 + login: function( deferred ) {
6432 + var self = this,
6433 + reject;
6434 +
6435 + reject = function() {
6436 + deferred.rejectWith( self, [ 'logged out' ] );
6437 + };
6438 +
6439 + if ( this.triedLogin ) {
6440 + return reject();
6441 + }
6442 +
6443 + // Check if we have an admin cookie.
6444 + $.get( api.settings.url.ajax, {
6445 + action: 'logged-in'
6446 + }).fail( reject ).done( function( response ) {
6447 + var iframe;
6448 +
6449 + if ( '1' !== response ) {
6450 + reject();
6451 + }
6452 +
6453 + iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
6454 + iframe.appendTo( self.container );
6455 + iframe.on( 'load', function() {
6456 + self.triedLogin = true;
6457 +
6458 + iframe.remove();
6459 + self.run( deferred );
6460 + });
6461 + });
6462 + },
6463 +
6464 + destroy: function() {
6465 + api.Messenger.prototype.destroy.call( this );
6466 +
6467 + if ( this.iframe ) {
6468 + this.iframe.remove();
6469 + }
6470 +
6471 + delete this.iframe;
6472 + delete this.targetWindow;
6473 + }
6474 + });
6475 +
6476 + (function(){
6477 + var id = 0;
6478 + /**
6479 + * Return an incremented ID for a preview messenger channel.
6480 + *
6481 + * This function is named "uuid" for historical reasons, but it is a
6482 + * misnomer as it is not an actual UUID, and it is not universally unique.
6483 + * This is not to be confused with `api.settings.changeset.uuid`.
6484 + *
6485 + * @return {string}
6486 + */
6487 + api.PreviewFrame.uuid = function() {
6488 + return 'preview-' + String( id++ );
6489 + };
6490 + }());
6491 +
6492 + /**
6493 + * Set the document title of the customizer.
6494 + *
6495 + * @alias wp.customize.setDocumentTitle
6496 + *
6497 + * @since 4.1.0
6498 + *
6499 + * @param {string} documentTitle
6500 + */
6501 + api.setDocumentTitle = function ( documentTitle ) {
6502 + var tmpl, title;
6503 + tmpl = api.settings.documentTitleTmpl;
6504 + title = tmpl.replace( '%s', documentTitle );
6505 + document.title = title;
6506 + api.trigger( 'title', title );
6507 + };
6508 +
6509 + api.Previewer = api.Messenger.extend(/** @lends wp.customize.Previewer.prototype */{
6510 + refreshBuffer: null, // Will get set to api.settings.timeouts.windowRefresh.
6511 +
6512 + /**
6513 + * @constructs wp.customize.Previewer
6514 + * @augments wp.customize.Messenger
6515 + *
6516 + * @param {Array} params.allowedUrls
6517 + * @param {string} params.container A selector or jQuery element for the preview
6518 + * frame to be placed.
6519 + * @param {string} params.form
6520 + * @param {string} params.previewUrl The URL to preview.
6521 + * @param {Object} options
6522 + */
6523 + initialize: function( params, options ) {
6524 + var previewer = this,
6525 + urlParser = document.createElement( 'a' );
6526 +
6527 + $.extend( previewer, options || {} );
6528 + previewer.deferred = {
6529 + active: $.Deferred()
6530 + };
6531 +
6532 + // Debounce to prevent hammering server and then wait for any pending update requests.
6533 + previewer.refresh = _.debounce(
6534 + ( function( originalRefresh ) {
6535 + return function() {
6536 + var isProcessingComplete, refreshOnceProcessingComplete;
6537 + isProcessingComplete = function() {
6538 + return 0 === api.state( 'processing' ).get();
6539 + };
6540 + if ( isProcessingComplete() ) {
6541 + originalRefresh.call( previewer );
6542 + } else {
6543 + refreshOnceProcessingComplete = function() {
6544 + if ( isProcessingComplete() ) {
6545 + originalRefresh.call( previewer );
6546 + api.state( 'processing' ).unbind( refreshOnceProcessingComplete );
6547 + }
6548 + };
6549 + api.state( 'processing' ).bind( refreshOnceProcessingComplete );
6550 + }
6551 + };
6552 + }( previewer.refresh ) ),
6553 + previewer.refreshBuffer
6554 + );
6555 +
6556 + previewer.container = api.ensure( params.container );
6557 + previewer.allowedUrls = params.allowedUrls;
6558 +
6559 + params.url = window.location.href;
6560 +
6561 + api.Messenger.prototype.initialize.call( previewer, params );
6562 +
6563 + urlParser.href = previewer.origin();
6564 + previewer.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) );
6565 +
6566 + /*
6567 + * Limit the URL to internal, front-end links.
6568 + *
6569 + * If the front end and the admin are served from the same domain, load the
6570 + * preview over ssl if the Customizer is being loaded over ssl. This avoids
6571 + * insecure content warnings. This is not attempted if the admin and front end
6572 + * are on different domains to avoid the case where the front end doesn't have
6573 + * ssl certs.
6574 + */
6575 +
6576 + previewer.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
6577 + var result = null, urlParser, queryParams, parsedAllowedUrl, parsedCandidateUrls = [];
6578 + urlParser = document.createElement( 'a' );
6579 + urlParser.href = to;
6580 +
6581 + // Abort if URL is for admin or (static) files in wp-includes or wp-content.
6582 + if ( /\/wp-(admin|includes|content)(\/|$)/.test( urlParser.pathname ) ) {
6583 + return null;
6584 + }
6585 +
6586 + // Remove state query params.
6587 + if ( urlParser.search.length > 1 ) {
6588 + queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
6589 + delete queryParams.customize_changeset_uuid;
6590 + delete queryParams.customize_theme;
6591 + delete queryParams.customize_messenger_channel;
6592 + delete queryParams.customize_autosaved;
6593 + if ( _.isEmpty( queryParams ) ) {
6594 + urlParser.search = '';
6595 + } else {
6596 + urlParser.search = $.param( queryParams );
6597 + }
6598 + }
6599 +
6600 + parsedCandidateUrls.push( urlParser );
6601 +
6602 + // Prepend list with URL that matches the scheme/protocol of the iframe.
6603 + if ( previewer.scheme.get() + ':' !== urlParser.protocol ) {
6604 + urlParser = document.createElement( 'a' );
6605 + urlParser.href = parsedCandidateUrls[0].href;
6606 + urlParser.protocol = previewer.scheme.get() + ':';
6607 + parsedCandidateUrls.unshift( urlParser );
6608 + }
6609 +
6610 + // Attempt to match the URL to the control frame's scheme and check if it's allowed. If not, try the original URL.
6611 + parsedAllowedUrl = document.createElement( 'a' );
6612 + _.find( parsedCandidateUrls, function( parsedCandidateUrl ) {
6613 + return ! _.isUndefined( _.find( previewer.allowedUrls, function( allowedUrl ) {
6614 + parsedAllowedUrl.href = allowedUrl;
6615 + if ( urlParser.protocol === parsedAllowedUrl.protocol && urlParser.host === parsedAllowedUrl.host && 0 === urlParser.pathname.indexOf( parsedAllowedUrl.pathname.replace( /\/$/, '' ) ) ) {
6616 + result = parsedCandidateUrl.href;
6617 + return true;
6618 + }
6619 + } ) );
6620 + } );
6621 +
6622 + return result;
6623 + });
6624 +
6625 + previewer.bind( 'ready', previewer.ready );
6626 +
6627 + // Start listening for keep-alive messages when iframe first loads.
6628 + previewer.deferred.active.done( _.bind( previewer.keepPreviewAlive, previewer ) );
6629 +
6630 + previewer.bind( 'synced', function() {
6631 + previewer.send( 'active' );
6632 + } );
6633 +
6634 + // Refresh the preview when the URL is changed (but not yet).
6635 + previewer.previewUrl.bind( previewer.refresh );
6636 +
6637 + previewer.scroll = 0;
6638 + previewer.bind( 'scroll', function( distance ) {
6639 + previewer.scroll = distance;
6640 + });
6641 +
6642 + // Update the URL when the iframe sends a URL message, resetting scroll position. If URL is unchanged, then refresh.
6643 + previewer.bind( 'url', function( url ) {
6644 + var onUrlChange, urlChanged = false;
6645 + previewer.scroll = 0;
6646 + onUrlChange = function() {
6647 + urlChanged = true;
6648 + };
6649 + previewer.previewUrl.bind( onUrlChange );
6650 + previewer.previewUrl.set( url );
6651 + previewer.previewUrl.unbind( onUrlChange );
6652 + if ( ! urlChanged ) {
6653 + previewer.refresh();
6654 + }
6655 + } );
6656 +
6657 + // Update the document title when the preview changes.
6658 + previewer.bind( 'documentTitle', function ( title ) {
6659 + api.setDocumentTitle( title );
6660 + } );
6661 + },
6662 +
6663 + /**
6664 + * Handle the preview receiving the ready message.
6665 + *
6666 + * @since 4.7.0
6667 + * @access public
6668 + *
6669 + * @param {Object} data - Data from preview.
6670 + * @param {string} data.currentUrl - Current URL.
6671 + * @param {Object} data.activePanels - Active panels.
6672 + * @param {Object} data.activeSections Active sections.
6673 + * @param {Object} data.activeControls Active controls.
6674 + * @return {void}
6675 + */
6676 + ready: function( data ) {
6677 + var previewer = this, synced = {}, constructs;
6678 +
6679 + synced.settings = api.get();
6680 + synced['settings-modified-while-loading'] = previewer.settingsModifiedWhileLoading;
6681 + if ( 'resolved' !== previewer.deferred.active.state() || previewer.loading ) {
6682 + synced.scroll = previewer.scroll;
6683 + }
6684 + synced['edit-shortcut-visibility'] = api.state( 'editShortcutVisibility' ).get();
6685 + previewer.send( 'sync', synced );
6686 +
6687 + // Set the previewUrl without causing the url to set the iframe.
6688 + if ( data.currentUrl ) {
6689 + previewer.previewUrl.unbind( previewer.refresh );
6690 + previewer.previewUrl.set( data.currentUrl );
6691 + previewer.previewUrl.bind( previewer.refresh );
6692 + }
6693 +
6694 + /*
6695 + * Walk over all panels, sections, and controls and set their
6696 + * respective active states to true if the preview explicitly
6697 + * indicates as such.
6698 + */
6699 + constructs = {
6700 + panel: data.activePanels,
6701 + section: data.activeSections,
6702 + control: data.activeControls
6703 + };
6704 + _( constructs ).each( function ( activeConstructs, type ) {
6705 + api[ type ].each( function ( construct, id ) {
6706 + var isDynamicallyCreated = _.isUndefined( api.settings[ type + 's' ][ id ] );
6707 +
6708 + /*
6709 + * If the construct was created statically in PHP (not dynamically in JS)
6710 + * then consider a missing (undefined) value in the activeConstructs to
6711 + * mean it should be deactivated (since it is gone). But if it is
6712 + * dynamically created then only toggle activation if the value is defined,
6713 + * as this means that the construct was also then correspondingly
6714 + * created statically in PHP and the active callback is available.
6715 + * Otherwise, dynamically-created constructs should normally have
6716 + * their active states toggled in JS rather than from PHP.
6717 + */
6718 + if ( ! isDynamicallyCreated || ! _.isUndefined( activeConstructs[ id ] ) ) {
6719 + if ( activeConstructs[ id ] ) {
6720 + construct.activate();
6721 + } else {
6722 + construct.deactivate();
6723 + }
6724 + }
6725 + } );
6726 + } );
6727 +
6728 + if ( data.settingValidities ) {
6729 + api._handleSettingValidities( {
6730 + settingValidities: data.settingValidities,
6731 + focusInvalidControl: false
6732 + } );
6733 + }
6734 + },
6735 +
6736 + /**
6737 + * Keep the preview alive by listening for ready and keep-alive messages.
6738 + *
6739 + * If a message is not received in the allotted time then the iframe will be set back to the last known valid URL.
6740 + *
6741 + * @since 4.7.0
6742 + * @access public
6743 + *
6744 + * @return {void}
6745 + */
6746 + keepPreviewAlive: function keepPreviewAlive() {
6747 + var previewer = this, keepAliveTick, timeoutId, handleMissingKeepAlive, scheduleKeepAliveCheck;
6748 +
6749 + /**
6750 + * Schedule a preview keep-alive check.
6751 + *
6752 + * Note that if a page load takes longer than keepAliveCheck milliseconds,
6753 + * the keep-alive messages will still be getting sent from the previous
6754 + * URL.
6755 + */
6756 + scheduleKeepAliveCheck = function() {
6757 + timeoutId = setTimeout( handleMissingKeepAlive, api.settings.timeouts.keepAliveCheck );
6758 + };
6759 +
6760 + /**
6761 + * Set the previewerAlive state to true when receiving a message from the preview.
6762 + */
6763 + keepAliveTick = function() {
6764 + api.state( 'previewerAlive' ).set( true );
6765 + clearTimeout( timeoutId );
6766 + scheduleKeepAliveCheck();
6767 + };
6768 +
6769 + /**
6770 + * Set the previewerAlive state to false if keepAliveCheck milliseconds have transpired without a message.
6771 + *
6772 + * This is most likely to happen in the case of a connectivity error, or if the theme causes the browser
6773 + * to navigate to a non-allowed URL. Setting this state to false will force settings with a postMessage
6774 + * transport to use refresh instead, causing the preview frame also to be replaced with the current
6775 + * allowed preview URL.
6776 + */
6777 + handleMissingKeepAlive = function() {
6778 + api.state( 'previewerAlive' ).set( false );
6779 + };
6780 + scheduleKeepAliveCheck();
6781 +
6782 + previewer.bind( 'ready', keepAliveTick );
6783 + previewer.bind( 'keep-alive', keepAliveTick );
6784 + },
6785 +
6786 + /**
6787 + * Query string data sent with each preview request.
6788 + *
6789 + * @abstract
6790 + */
6791 + query: function() {},
6792 +
6793 + abort: function() {
6794 + if ( this.loading ) {
6795 + this.loading.destroy();
6796 + delete this.loading;
6797 + }
6798 + },
6799 +
6800 + /**
6801 + * Refresh the preview seamlessly.
6802 + *
6803 + * @since 3.4.0
6804 + * @access public
6805 + *
6806 + * @return {void}
6807 + */
6808 + refresh: function() {
6809 + var previewer = this, onSettingChange;
6810 +
6811 + // Display loading indicator.
6812 + previewer.send( 'loading-initiated' );
6813 +
6814 + previewer.abort();
6815 +
6816 + previewer.loading = new api.PreviewFrame({
6817 + url: previewer.url(),
6818 + previewUrl: previewer.previewUrl(),
6819 + query: previewer.query( { excludeCustomizedSaved: true } ) || {},
6820 + container: previewer.container
6821 + });
6822 +
6823 + previewer.settingsModifiedWhileLoading = {};
6824 + onSettingChange = function( setting ) {
6825 + previewer.settingsModifiedWhileLoading[ setting.id ] = true;
6826 + };
6827 + api.bind( 'change', onSettingChange );
6828 + previewer.loading.always( function() {
6829 + api.unbind( 'change', onSettingChange );
6830 + } );
6831 +
6832 + previewer.loading.done( function( readyData ) {
6833 + var loadingFrame = this, onceSynced;
6834 +
6835 + previewer.preview = loadingFrame;
6836 + previewer.targetWindow( loadingFrame.targetWindow() );
6837 + previewer.channel( loadingFrame.channel() );
6838 +
6839 + onceSynced = function() {
6840 + loadingFrame.unbind( 'synced', onceSynced );
6841 + if ( previewer._previousPreview ) {
6842 + previewer._previousPreview.destroy();
6843 + }
6844 + previewer._previousPreview = previewer.preview;
6845 + previewer.deferred.active.resolve();
6846 + delete previewer.loading;
6847 + };
6848 + loadingFrame.bind( 'synced', onceSynced );
6849 +
6850 + // This event will be received directly by the previewer in normal navigation; this is only needed for seamless refresh.
6851 + previewer.trigger( 'ready', readyData );
6852 + });
6853 +
6854 + previewer.loading.fail( function( reason ) {
6855 + previewer.send( 'loading-failed' );
6856 +
6857 + if ( 'logged out' === reason ) {
6858 + if ( previewer.preview ) {
6859 + previewer.preview.destroy();
6860 + delete previewer.preview;
6861 + }
6862 +
6863 + previewer.login().done( previewer.refresh );
6864 + }
6865 +
6866 + if ( 'cheatin' === reason ) {
6867 + previewer.cheatin();
6868 + }
6869 + });
6870 + },
6871 +
6872 + login: function() {
6873 + var previewer = this,
6874 + deferred, messenger, iframe;
6875 +
6876 + if ( this._login ) {
6877 + return this._login;
6878 + }
6879 +
6880 + deferred = $.Deferred();
6881 + this._login = deferred.promise();
6882 +
6883 + messenger = new api.Messenger({
6884 + channel: 'login',
6885 + url: api.settings.url.login
6886 + });
6887 +
6888 + iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
6889 +
6890 + messenger.targetWindow( iframe[0].contentWindow );
6891 +
6892 + messenger.bind( 'login', function () {
6893 + var refreshNonces = previewer.refreshNonces();
6894 +
6895 + refreshNonces.always( function() {
6896 + iframe.remove();
6897 + messenger.destroy();
6898 + delete previewer._login;
6899 + });
6900 +
6901 + refreshNonces.done( function() {
6902 + deferred.resolve();
6903 + });
6904 +
6905 + refreshNonces.fail( function() {
6906 + previewer.cheatin();
6907 + deferred.reject();
6908 + });
6909 + });
6910 +
6911 + return this._login;
6912 + },
6913 +
6914 + cheatin: function() {
6915 + $( document.body ).empty().addClass( 'cheatin' ).append(
6916 + '<h1>' + api.l10n.notAllowedHeading + '</h1>' +
6917 + '<p>' + api.l10n.notAllowed + '</p>'
6918 + );
6919 + },
6920 +
6921 + refreshNonces: function() {
6922 + var request, deferred = $.Deferred();
6923 +
6924 + deferred.promise();
6925 +
6926 + request = wp.ajax.post( 'customize_refresh_nonces', {
6927 + wp_customize: 'on',
6928 + customize_theme: api.settings.theme.stylesheet
6929 + });
6930 +
6931 + request.done( function( response ) {
6932 + api.trigger( 'nonce-refresh', response );
6933 + deferred.resolve();
6934 + });
6935 +
6936 + request.fail( function() {
6937 + deferred.reject();
6938 + });
6939 +
6940 + return deferred;
6941 + }
6942 + });
6943 +
6944 + api.settingConstructor = {};
6945 + api.controlConstructor = {
6946 + color: api.ColorControl,
6947 + media: api.MediaControl,
6948 + upload: api.UploadControl,
6949 + image: api.ImageControl,
6950 + cropped_image: api.CroppedImageControl,
6951 + site_icon: api.SiteIconControl,
6952 + header: api.HeaderControl,
6953 + background: api.BackgroundControl,
6954 + background_position: api.BackgroundPositionControl,
6955 + theme: api.ThemeControl,
6956 + date_time: api.DateTimeControl,
6957 + code_editor: api.CodeEditorControl
6958 + };
6959 + api.panelConstructor = {
6960 + themes: api.ThemesPanel
6961 + };
6962 + api.sectionConstructor = {
6963 + themes: api.ThemesSection,
6964 + outer: api.OuterSection
6965 + };
6966 +
6967 + /**
6968 + * Handle setting_validities in an error response for the customize-save request.
6969 + *
6970 + * Add notifications to the settings and focus on the first control that has an invalid setting.
6971 + *
6972 + * @alias wp.customize._handleSettingValidities
6973 + *
6974 + * @since 4.6.0
6975 + * @private
6976 + *
6977 + * @param {Object} args
6978 + * @param {Object} args.settingValidities
6979 + * @param {boolean} [args.focusInvalidControl=false]
6980 + * @return {void}
6981 + */
6982 + api._handleSettingValidities = function handleSettingValidities( args ) {
6983 + var invalidSettingControls, invalidSettings = [], wasFocused = false;
6984 +
6985 + // Find the controls that correspond to each invalid setting.
6986 + _.each( args.settingValidities, function( validity, settingId ) {
6987 + var setting = api( settingId );
6988 + if ( setting ) {
6989 +
6990 + // Add notifications for invalidities.
6991 + if ( _.isObject( validity ) ) {
6992 + _.each( validity, function( params, code ) {
6993 + var notification, existingNotification, needsReplacement = false;
6994 + notification = new api.Notification( code, _.extend( { fromServer: true }, params ) );
6995 +
6996 + // Remove existing notification if already exists for code but differs in parameters.
6997 + existingNotification = setting.notifications( notification.code );
6998 + if ( existingNotification ) {
6999 + needsReplacement = notification.type !== existingNotification.type || notification.message !== existingNotification.message || ! _.isEqual( notification.data, existingNotification.data );
7000 + }
7001 + if ( needsReplacement ) {
7002 + setting.notifications.remove( code );
7003 + }
7004 +
7005 + if ( ! setting.notifications.has( notification.code ) ) {
7006 + setting.notifications.add( notification );
7007 + }
7008 + invalidSettings.push( setting.id );
7009 + } );
7010 + }
7011 +
7012 + // Remove notification errors that are no longer valid.
7013 + setting.notifications.each( function( notification ) {
7014 + if ( notification.fromServer && 'error' === notification.type && ( true === validity || ! validity[ notification.code ] ) ) {
7015 + setting.notifications.remove( notification.code );
7016 + }
7017 + } );
7018 + }
7019 + } );
7020 +
7021 + if ( args.focusInvalidControl ) {
7022 + invalidSettingControls = api.findControlsForSettings( invalidSettings );
7023 +
7024 + // Focus on the first control that is inside of an expanded section (one that is visible).
7025 + _( _.values( invalidSettingControls ) ).find( function( controls ) {
7026 + return _( controls ).find( function( control ) {
7027 + var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded();
7028 + if ( isExpanded && control.expanded ) {
7029 + isExpanded = control.expanded();
7030 + }
7031 + if ( isExpanded ) {
7032 + control.focus();
7033 + wasFocused = true;
7034 + }
7035 + return wasFocused;
7036 + } );
7037 + } );
7038 +
7039 + // Focus on the first invalid control.
7040 + if ( ! wasFocused && ! _.isEmpty( invalidSettingControls ) ) {
7041 + _.values( invalidSettingControls )[0][0].focus();
7042 + }
7043 + }
7044 + };
7045 +
7046 + /**
7047 + * Find all controls associated with the given settings.
7048 + *
7049 + * @alias wp.customize.findControlsForSettings
7050 + *
7051 + * @since 4.6.0
7052 + * @param {string[]} settingIds Setting IDs.
7053 + * @return {Object<string, wp.customize.Control>} Mapping setting ids to arrays of controls.
7054 + */
7055 + api.findControlsForSettings = function findControlsForSettings( settingIds ) {
7056 + var controls = {}, settingControls;
7057 + _.each( _.unique( settingIds ), function( settingId ) {
7058 + var setting = api( settingId );
7059 + if ( setting ) {
7060 + settingControls = setting.findControls();
7061 + if ( settingControls && settingControls.length > 0 ) {
7062 + controls[ settingId ] = settingControls;
7063 + }
7064 + }
7065 + } );
7066 + return controls;
7067 + };
7068 +
7069 + /**
7070 + * Sort panels, sections, controls by priorities. Hide empty sections and panels.
7071 + *
7072 + * @alias wp.customize.reflowPaneContents
7073 + *
7074 + * @since 4.1.0
7075 + */
7076 + api.reflowPaneContents = _.bind( function () {
7077 +
7078 + var appendContainer, activeElement, rootHeadContainers, rootNodes = [], wasReflowed = false;
7079 +
7080 + if ( document.activeElement ) {
7081 + activeElement = $( document.activeElement );
7082 + }
7083 +
7084 + // Sort the sections within each panel.
7085 + api.panel.each( function ( panel ) {
7086 + if ( 'themes' === panel.id ) {
7087 + return; // Don't reflow theme sections, as doing so moves them after the themes container.
7088 + }
7089 +
7090 + var sections = panel.sections(),
7091 + sectionHeadContainers = _.pluck( sections, 'headContainer' );
7092 + rootNodes.push( panel );
7093 + appendContainer = ( panel.contentContainer.is( 'ul' ) ) ? panel.contentContainer : panel.contentContainer.find( 'ul:first' );
7094 + if ( ! api.utils.areElementListsEqual( sectionHeadContainers, appendContainer.children( '[id]' ) ) ) {
7095 + _( sections ).each( function ( section ) {
7096 + appendContainer.append( section.headContainer );
7097 + } );
7098 + wasReflowed = true;
7099 + }
7100 + } );
7101 +
7102 + // Sort the controls within each section.
7103 + api.section.each( function ( section ) {
7104 + var controls = section.controls(),
7105 + controlContainers = _.pluck( controls, 'container' );
7106 + if ( ! section.panel() ) {
7107 + rootNodes.push( section );
7108 + }
7109 + appendContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' );
7110 + if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
7111 + _( controls ).each( function ( control ) {
7112 + appendContainer.append( control.container );
7113 + } );
7114 + wasReflowed = true;
7115 + }
7116 + } );
7117 +
7118 + // Sort the root panels and sections.
7119 + rootNodes.sort( api.utils.prioritySort );
7120 + rootHeadContainers = _.pluck( rootNodes, 'headContainer' );
7121 + appendContainer = $( '#customize-theme-controls .customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable.
7122 + if ( ! api.utils.areElementListsEqual( rootHeadContainers, appendContainer.children() ) ) {
7123 + _( rootNodes ).each( function ( rootNode ) {
7124 + appendContainer.append( rootNode.headContainer );
7125 + } );
7126 + wasReflowed = true;
7127 + }
7128 +
7129 + // Now re-trigger the active Value callbacks so that the panels and sections can decide whether they can be rendered.
7130 + api.panel.each( function ( panel ) {
7131 + var value = panel.active();
7132 + panel.active.callbacks.fireWith( panel.active, [ value, value ] );
7133 + } );
7134 + api.section.each( function ( section ) {
7135 + var value = section.active();
7136 + section.active.callbacks.fireWith( section.active, [ value, value ] );
7137 + } );
7138 +
7139 + // Restore focus if there was a reflow and there was an active (focused) element.
7140 + if ( wasReflowed && activeElement ) {
7141 + activeElement.trigger( 'focus' );
7142 + }
7143 + api.trigger( 'pane-contents-reflowed' );
7144 + }, api );
7145 +
7146 + // Define state values.
7147 + api.state = new api.Values();
7148 + _.each( [
7149 + 'saved',
7150 + 'saving',
7151 + 'trashing',
7152 + 'activated',
7153 + 'processing',
7154 + 'paneVisible',
7155 + 'expandedPanel',
7156 + 'expandedSection',
7157 + 'changesetDate',
7158 + 'selectedChangesetDate',
7159 + 'changesetStatus',
7160 + 'selectedChangesetStatus',
7161 + 'remainingTimeToPublish',
7162 + 'previewerAlive',
7163 + 'editShortcutVisibility',
7164 + 'changesetLocked',
7165 + 'previewedDevice'
7166 + ], function( name ) {
7167 + api.state.create( name );
7168 + });
7169 +
7170 + $( function() {
7171 + api.settings = window._wpCustomizeSettings;
7172 + api.l10n = window._wpCustomizeControlsL10n;
7173 +
7174 + // Check if we can run the Customizer.
7175 + if ( ! api.settings ) {
7176 + return;
7177 + }
7178 +
7179 + // Bail if any incompatibilities are found.
7180 + if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) {
7181 + return;
7182 + }
7183 +
7184 + if ( null === api.PreviewFrame.prototype.sensitivity ) {
7185 + api.PreviewFrame.prototype.sensitivity = api.settings.timeouts.previewFrameSensitivity;
7186 + }
7187 + if ( null === api.Previewer.prototype.refreshBuffer ) {
7188 + api.Previewer.prototype.refreshBuffer = api.settings.timeouts.windowRefresh;
7189 + }
7190 +
7191 + var parent,
7192 + body = $( document.body ),
7193 + overlay = body.children( '.wp-full-overlay' ),
7194 + title = $( '#customize-info .panel-title.site-title' ),
7195 + closeBtn = $( '.customize-controls-close' ),
7196 + saveBtn = $( '#save' ),
7197 + btnWrapper = $( '#customize-save-button-wrapper' ),
7198 + publishSettingsBtn = $( '#publish-settings' ),
7199 + footerActions = $( '#customize-footer-actions' );
7200 +
7201 + // Add publish settings section in JS instead of PHP since the Customizer depends on it to function.
7202 + api.bind( 'ready', function() {
7203 + api.section.add( new api.OuterSection( 'publish_settings', {
7204 + title: api.l10n.publishSettings,
7205 + priority: 0,
7206 + active: api.settings.theme.active
7207 + } ) );
7208 + } );
7209 +
7210 + // Set up publish settings section and its controls.
7211 + api.section( 'publish_settings', function( section ) {
7212 + var updateButtonsState, trashControl, updateSectionActive, isSectionActive, statusControl, dateControl, toggleDateControl, publishWhenTime, pollInterval, updateTimeArrivedPoller, cancelScheduleButtonReminder, timeArrivedPollingInterval = 1000;
7213 +
7214 + trashControl = new api.Control( 'trash_changeset', {
7215 + type: 'button',
7216 + section: section.id,
7217 + priority: 30,
7218 + input_attrs: {
7219 + 'class': 'button-link button-link-delete',
7220 + value: api.l10n.discardChanges
7221 + }
7222 + } );
7223 + api.control.add( trashControl );
7224 + trashControl.deferred.embedded.done( function() {
7225 + trashControl.container.find( '.button-link' ).on( 'click', function() {
7226 + if ( confirm( api.l10n.trashConfirm ) ) {
7227 + wp.customize.previewer.trash();
7228 + }
7229 + } );
7230 + } );
7231 +
7232 + api.control.add( new api.PreviewLinkControl( 'changeset_preview_link', {
7233 + section: section.id,
7234 + priority: 100
7235 + } ) );
7236 +
7237 + /**
7238 + * Return whether the publish settings section should be active.
7239 + *
7240 + * @return {boolean} Is section active.
7241 + */
7242 + isSectionActive = function() {
7243 + if ( ! api.state( 'activated' ).get() ) {
7244 + return false;
7245 + }
7246 + if ( api.state( 'trashing' ).get() || 'trash' === api.state( 'changesetStatus' ).get() ) {
7247 + return false;
7248 + }
7249 + if ( '' === api.state( 'changesetStatus' ).get() && api.state( 'saved' ).get() ) {
7250 + return false;
7251 + }
7252 + return true;
7253 + };
7254 +
7255 + // Make sure publish settings are not available while the theme is not active and the customizer is in a published state.
7256 + section.active.validate = isSectionActive;
7257 + updateSectionActive = function() {
7258 + section.active.set( isSectionActive() );
7259 + };
7260 + api.state( 'activated' ).bind( updateSectionActive );
7261 + api.state( 'trashing' ).bind( updateSectionActive );
7262 + api.state( 'saved' ).bind( updateSectionActive );
7263 + api.state( 'changesetStatus' ).bind( updateSectionActive );
7264 + updateSectionActive();
7265 +
7266 + // Bind visibility of the publish settings button to whether the section is active.
7267 + updateButtonsState = function() {
7268 + publishSettingsBtn.toggle( section.active.get() );
7269 + saveBtn.toggleClass( 'has-next-sibling', section.active.get() );
7270 + };
7271 + updateButtonsState();
7272 + section.active.bind( updateButtonsState );
7273 +
7274 + function highlightScheduleButton() {
7275 + if ( ! cancelScheduleButtonReminder ) {
7276 + cancelScheduleButtonReminder = api.utils.highlightButton( btnWrapper, {
7277 + delay: 1000,
7278 +
7279 + /*
7280 + * Only abort the reminder when the save button is focused.
7281 + * If the user clicks the settings button to toggle the
7282 + * settings closed, we'll still remind them.
7283 + */
7284 + focusTarget: saveBtn
7285 + } );
7286 + }
7287 + }
7288 + function cancelHighlightScheduleButton() {
7289 + if ( cancelScheduleButtonReminder ) {
7290 + cancelScheduleButtonReminder();
7291 + cancelScheduleButtonReminder = null;
7292 + }
7293 + }
7294 + api.state( 'selectedChangesetStatus' ).bind( cancelHighlightScheduleButton );
7295 +
7296 + section.contentContainer.find( '.customize-action' ).text( api.l10n.updating );
7297 + section.contentContainer.find( '.customize-section-back' ).removeAttr( 'tabindex' );
7298 + publishSettingsBtn.prop( 'disabled', false );
7299 +
7300 + publishSettingsBtn.on( 'click', function( event ) {
7301 + event.preventDefault();
7302 + section.expanded.set( ! section.expanded.get() );
7303 + } );
7304 +
7305 + section.expanded.bind( function( isExpanded ) {
7306 + var defaultChangesetStatus;
7307 + publishSettingsBtn.attr( 'aria-expanded', String( isExpanded ) );
7308 + publishSettingsBtn.toggleClass( 'active', isExpanded );
7309 +
7310 + if ( isExpanded ) {
7311 + cancelHighlightScheduleButton();
7312 + return;
7313 + }
7314 +
7315 + defaultChangesetStatus = api.state( 'changesetStatus' ).get();
7316 + if ( '' === defaultChangesetStatus || 'auto-draft' === defaultChangesetStatus ) {
7317 + defaultChangesetStatus = 'publish';
7318 + }
7319 +
7320 + if ( api.state( 'selectedChangesetStatus' ).get() !== defaultChangesetStatus ) {
7321 + highlightScheduleButton();
7322 + } else if ( 'future' === api.state( 'selectedChangesetStatus' ).get() && api.state( 'selectedChangesetDate' ).get() !== api.state( 'changesetDate' ).get() ) {
7323 + highlightScheduleButton();
7324 + }
7325 + } );
7326 +
7327 + statusControl = new api.Control( 'changeset_status', {
7328 + priority: 10,
7329 + type: 'radio',
7330 + section: 'publish_settings',
7331 + setting: api.state( 'selectedChangesetStatus' ),
7332 + templateId: 'customize-selected-changeset-status-control',
7333 + label: api.l10n.action,
7334 + choices: api.settings.changeset.statusChoices
7335 + } );
7336 + api.control.add( statusControl );
7337 +
7338 + dateControl = new api.DateTimeControl( 'changeset_scheduled_date', {
7339 + priority: 20,
7340 + section: 'publish_settings',
7341 + setting: api.state( 'selectedChangesetDate' ),
7342 + minYear: ( new Date() ).getFullYear(),
7343 + allowPastDate: false,
7344 + includeTime: true,
7345 + twelveHourFormat: /a/i.test( api.settings.timeFormat ),
7346 + description: api.l10n.scheduleDescription
7347 + } );
7348 + dateControl.notifications.alt = true;
7349 + api.control.add( dateControl );
7350 +
7351 + publishWhenTime = function() {
7352 + api.state( 'selectedChangesetStatus' ).set( 'publish' );
7353 + api.previewer.save();
7354 + };
7355 +
7356 + // Start countdown for when the dateTime arrives, or clear interval when it is .
7357 + updateTimeArrivedPoller = function() {
7358 + var shouldPoll = (
7359 + 'future' === api.state( 'changesetStatus' ).get() &&
7360 + 'future' === api.state( 'selectedChangesetStatus' ).get() &&
7361 + api.state( 'changesetDate' ).get() &&
7362 + api.state( 'selectedChangesetDate' ).get() === api.state( 'changesetDate' ).get() &&
7363 + api.utils.getRemainingTime( api.state( 'changesetDate' ).get() ) >= 0
7364 + );
7365 +
7366 + if ( shouldPoll && ! pollInterval ) {
7367 + pollInterval = setInterval( function() {
7368 + var remainingTime = api.utils.getRemainingTime( api.state( 'changesetDate' ).get() );
7369 + api.state( 'remainingTimeToPublish' ).set( remainingTime );
7370 + if ( remainingTime <= 0 ) {
7371 + clearInterval( pollInterval );
7372 + pollInterval = 0;
7373 + publishWhenTime();
7374 + }
7375 + }, timeArrivedPollingInterval );
7376 + } else if ( ! shouldPoll && pollInterval ) {
7377 + clearInterval( pollInterval );
7378 + pollInterval = 0;
7379 + }
7380 + };
7381 +
7382 + api.state( 'changesetDate' ).bind( updateTimeArrivedPoller );
7383 + api.state( 'selectedChangesetDate' ).bind( updateTimeArrivedPoller );
7384 + api.state( 'changesetStatus' ).bind( updateTimeArrivedPoller );
7385 + api.state( 'selectedChangesetStatus' ).bind( updateTimeArrivedPoller );
7386 + updateTimeArrivedPoller();
7387 +
7388 + // Ensure dateControl only appears when selected status is future.
7389 + dateControl.active.validate = function() {
7390 + return 'future' === api.state( 'selectedChangesetStatus' ).get();
7391 + };
7392 + toggleDateControl = function( value ) {
7393 + dateControl.active.set( 'future' === value );
7394 + };
7395 + toggleDateControl( api.state( 'selectedChangesetStatus' ).get() );
7396 + api.state( 'selectedChangesetStatus' ).bind( toggleDateControl );
7397 +
7398 + // Show notification on date control when status is future but it isn't a future date.
7399 + api.state( 'saving' ).bind( function( isSaving ) {
7400 + if ( isSaving && 'future' === api.state( 'selectedChangesetStatus' ).get() ) {
7401 + dateControl.toggleFutureDateNotification( ! dateControl.isFutureDate() );
7402 + }
7403 + } );
7404 + } );
7405 +
7406 + // Prevent the form from saving when enter is pressed on an input or select element.
7407 + $('#customize-controls').on( 'keydown', function( e ) {
7408 + var isEnter = ( 13 === e.which ),
7409 + $el = $( e.target );
7410 +
7411 + if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
7412 + e.preventDefault();
7413 + }
7414 + });
7415 +
7416 + // Expand/Collapse the main customizer customize info.
7417 + $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
7418 + var section = $( this ).closest( '.accordion-section' ),
7419 + content = section.find( '.customize-panel-description:first' );
7420 +
7421 + if ( section.hasClass( 'cannot-expand' ) ) {
7422 + return;
7423 + }
7424 +
7425 + if ( section.hasClass( 'open' ) ) {
7426 + section.toggleClass( 'open' );
7427 + content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration, function() {
7428 + content.trigger( 'toggled' );
7429 + } );
7430 + $( this ).attr( 'aria-expanded', false );
7431 + } else {
7432 + content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration, function() {
7433 + content.trigger( 'toggled' );
7434 + } );
7435 + section.toggleClass( 'open' );
7436 + $( this ).attr( 'aria-expanded', true );
7437 + }
7438 + });
7439 +
7440 + /**
7441 + * Initialize Previewer
7442 + *
7443 + * @alias wp.customize.previewer
7444 + */
7445 + api.previewer = new api.Previewer({
7446 + container: '#customize-preview',
7447 + form: '#customize-controls',
7448 + previewUrl: api.settings.url.preview,
7449 + allowedUrls: api.settings.url.allowed
7450 + },/** @lends wp.customize.previewer */{
7451 +
7452 + nonce: api.settings.nonce,
7453 +
7454 + /**
7455 + * Build the query to send along with the Preview request.
7456 + *
7457 + * @since 3.4.0
7458 + * @since 4.7.0 Added options param.
7459 + * @access public
7460 + *
7461 + * @param {Object} [options] Options.
7462 + * @param {boolean} [options.excludeCustomizedSaved=false] Exclude saved settings in customized response (values pending writing to changeset).
7463 + * @return {Object} Query vars.
7464 + */
7465 + query: function( options ) {
7466 + var queryVars = {
7467 + wp_customize: 'on',
7468 + customize_theme: api.settings.theme.stylesheet,
7469 + nonce: this.nonce.preview,
7470 + customize_changeset_uuid: api.settings.changeset.uuid
7471 + };
7472 + if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) {
7473 + queryVars.customize_autosaved = 'on';
7474 + }
7475 +
7476 + /*
7477 + * Exclude customized data if requested especially for calls to requestChangesetUpdate.
7478 + * Changeset updates are differential and so it is a performance waste to send all of
7479 + * the dirty settings with each update.
7480 + */
7481 + queryVars.customized = JSON.stringify( api.dirtyValues( {
7482 + unsaved: options && options.excludeCustomizedSaved
7483 + } ) );
7484 +
7485 + return queryVars;
7486 + },
7487 +
7488 + /**
7489 + * Save (and publish) the customizer changeset.
7490 + *
7491 + * Updates to the changeset are transactional. If any of the settings
7492 + * are invalid then none of them will be written into the changeset.
7493 + * A revision will be made for the changeset post if revisions support
7494 + * has been added to the post type.
7495 + *
7496 + * @since 3.4.0
7497 + * @since 4.7.0 Added args param and return value.
7498 + *
7499 + * @param {Object} [args] Args.
7500 + * @param {string} [args.status=publish] Status.
7501 + * @param {string} [args.date] Date, in local time in MySQL format.
7502 + * @param {string} [args.title] Title
7503 + * @return {jQuery.promise} Promise.
7504 + */
7505 + save: function( args ) {
7506 + var previewer = this,
7507 + deferred = $.Deferred(),
7508 + changesetStatus = api.state( 'selectedChangesetStatus' ).get(),
7509 + selectedChangesetDate = api.state( 'selectedChangesetDate' ).get(),
7510 + processing = api.state( 'processing' ),
7511 + submitWhenDoneProcessing,
7512 + submit,
7513 + modifiedWhileSaving = {},
7514 + invalidSettings = [],
7515 + invalidControls = [],
7516 + invalidSettingLessControls = [];
7517 +
7518 + if ( args && args.status ) {
7519 + changesetStatus = args.status;
7520 + }
7521 +
7522 + if ( api.state( 'saving' ).get() ) {
7523 + deferred.reject( 'already_saving' );
7524 + deferred.promise();
7525 + }
7526 +
7527 + api.state( 'saving' ).set( true );
7528 +
7529 + function captureSettingModifiedDuringSave( setting ) {
7530 + modifiedWhileSaving[ setting.id ] = true;
7531 + }
7532 +
7533 + submit = function () {
7534 + var request, query, settingInvalidities = {}, latestRevision = api._latestRevision, errorCode = 'client_side_error';
7535 +
7536 + api.bind( 'change', captureSettingModifiedDuringSave );
7537 + api.notifications.remove( errorCode );
7538 +
7539 + /*
7540 + * Block saving if there are any settings that are marked as
7541 + * invalid from the client (not from the server). Focus on
7542 + * the control.
7543 + */
7544 + api.each( function( setting ) {
7545 + setting.notifications.each( function( notification ) {
7546 + if ( 'error' === notification.type && ! notification.fromServer ) {
7547 + invalidSettings.push( setting.id );
7548 + if ( ! settingInvalidities[ setting.id ] ) {
7549 + settingInvalidities[ setting.id ] = {};
7550 + }
7551 + settingInvalidities[ setting.id ][ notification.code ] = notification;
7552 + }
7553 + } );
7554 + } );
7555 +
7556 + // Find all invalid setting less controls with notification type error.
7557 + api.control.each( function( control ) {
7558 + if ( ! control.setting || ! control.setting.id && control.active.get() ) {
7559 + control.notifications.each( function( notification ) {
7560 + if ( 'error' === notification.type ) {
7561 + invalidSettingLessControls.push( [ control ] );
7562 + }
7563 + } );
7564 + }
7565 + } );
7566 +
7567 + invalidControls = _.union( invalidSettingLessControls, _.values( api.findControlsForSettings( invalidSettings ) ) );
7568 + if ( ! _.isEmpty( invalidControls ) ) {
7569 +
7570 + invalidControls[0][0].focus();
7571 + api.unbind( 'change', captureSettingModifiedDuringSave );
7572 +
7573 + if ( invalidSettings.length ) {
7574 + api.notifications.add( new api.Notification( errorCode, {
7575 + message: ( 1 === invalidSettings.length ? api.l10n.saveBlockedError.singular : api.l10n.saveBlockedError.plural ).replace( /%s/g, String( invalidSettings.length ) ),
7576 + type: 'error',
7577 + dismissible: true,
7578 + saveFailure: true
7579 + } ) );
7580 + }
7581 +
7582 + deferred.rejectWith( previewer, [
7583 + { setting_invalidities: settingInvalidities }
7584 + ] );
7585 + api.state( 'saving' ).set( false );
7586 + return deferred.promise();
7587 + }
7588 +
7589 + /*
7590 + * Note that excludeCustomizedSaved is intentionally false so that the entire
7591 + * set of customized data will be included if bypassed changeset update.
7592 + */
7593 + query = $.extend( previewer.query( { excludeCustomizedSaved: false } ), {
7594 + nonce: previewer.nonce.save,
7595 + customize_changeset_status: changesetStatus
7596 + } );
7597 +
7598 + if ( args && args.date ) {
7599 + query.customize_changeset_date = args.date;
7600 + } else if ( 'future' === changesetStatus && selectedChangesetDate ) {
7601 + query.customize_changeset_date = selectedChangesetDate;
7602 + }
7603 +
7604 + if ( args && args.title ) {
7605 + query.customize_changeset_title = args.title;
7606 + }
7607 +
7608 + // Allow plugins to modify the params included with the save request.
7609 + api.trigger( 'save-request-params', query );
7610 +
7611 + /*
7612 + * Note that the dirty customized values will have already been set in the
7613 + * changeset and so technically query.customized could be deleted. However,
7614 + * it is remaining here to make sure that any settings that got updated
7615 + * quietly which may have not triggered an update request will also get
7616 + * included in the values that get saved to the changeset. This will ensure
7617 + * that values that get injected via the saved event will be included in
7618 + * the changeset. This also ensures that setting values that were invalid
7619 + * will get re-validated, perhaps in the case of settings that are invalid
7620 + * due to dependencies on other settings.
7621 + */
7622 + request = wp.ajax.post( 'customize_save', query );
7623 + api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
7624 +
7625 + api.trigger( 'save', request );
7626 +
7627 + request.always( function () {
7628 + api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
7629 + api.state( 'saving' ).set( false );
7630 + api.unbind( 'change', captureSettingModifiedDuringSave );
7631 + } );
7632 +
7633 + // Remove notifications that were added due to save failures.
7634 + api.notifications.each( function( notification ) {
7635 + if ( notification.saveFailure ) {
7636 + api.notifications.remove( notification.code );
7637 + }
7638 + });
7639 +
7640 + request.fail( function ( response ) {
7641 + var notification, notificationArgs;
7642 + notificationArgs = {
7643 + type: 'error',
7644 + dismissible: true,
7645 + fromServer: true,
7646 + saveFailure: true
7647 + };
7648 +
7649 + if ( '0' === response ) {
7650 + response = 'not_logged_in';
7651 + } else if ( '-1' === response ) {
7652 + // Back-compat in case any other check_ajax_referer() call is dying.
7653 + response = 'invalid_nonce';
7654 + }
7655 +
7656 + if ( 'invalid_nonce' === response ) {
7657 + previewer.cheatin();
7658 + } else if ( 'not_logged_in' === response ) {
7659 + previewer.preview.iframe.hide();
7660 + previewer.login().done( function() {
7661 + previewer.save();
7662 + previewer.preview.iframe.show();
7663 + } );
7664 + } else if ( response.code ) {
7665 + if ( 'not_future_date' === response.code && api.section.has( 'publish_settings' ) && api.section( 'publish_settings' ).active.get() && api.control.has( 'changeset_scheduled_date' ) ) {
7666 + api.control( 'changeset_scheduled_date' ).toggleFutureDateNotification( true ).focus();
7667 + } else if ( 'changeset_locked' !== response.code ) {
7668 + notification = new api.Notification( response.code, _.extend( notificationArgs, {
7669 + message: response.message
7670 + } ) );
7671 + }
7672 + } else {
7673 + notification = new api.Notification( 'unknown_error', _.extend( notificationArgs, {
7674 + message: api.l10n.unknownRequestFail
7675 + } ) );
7676 + }
7677 +
7678 + if ( notification ) {
7679 + api.notifications.add( notification );
7680 + }
7681 +
7682 + if ( response.setting_validities ) {
7683 + api._handleSettingValidities( {
7684 + settingValidities: response.setting_validities,
7685 + focusInvalidControl: true
7686 + } );
7687 + }
7688 +
7689 + deferred.rejectWith( previewer, [ response ] );
7690 + api.trigger( 'error', response );
7691 +
7692 + // Start a new changeset if the underlying changeset was published.
7693 + if ( 'changeset_already_published' === response.code && response.next_changeset_uuid ) {
7694 + api.settings.changeset.uuid = response.next_changeset_uuid;
7695 + api.state( 'changesetStatus' ).set( '' );
7696 + if ( api.settings.changeset.branching ) {
7697 + parent.send( 'changeset-uuid', api.settings.changeset.uuid );
7698 + }
7699 + api.previewer.send( 'changeset-uuid', api.settings.changeset.uuid );
7700 + }
7701 + } );
7702 +
7703 + request.done( function( response ) {
7704 +
7705 + previewer.send( 'saved', response );
7706 +
7707 + api.state( 'changesetStatus' ).set( response.changeset_status );
7708 + if ( response.changeset_date ) {
7709 + api.state( 'changesetDate' ).set( response.changeset_date );
7710 + }
7711 +
7712 + if ( 'publish' === response.changeset_status ) {
7713 +
7714 + // Mark all published as clean if they haven't been modified during the request.
7715 + api.each( function( setting ) {
7716 + /*
7717 + * Note that the setting revision will be undefined in the case of setting
7718 + * values that are marked as dirty when the customizer is loaded, such as
7719 + * when applying starter content. All other dirty settings will have an
7720 + * associated revision due to their modification triggering a change event.
7721 + */
7722 + if ( setting._dirty && ( _.isUndefined( api._latestSettingRevisions[ setting.id ] ) || api._latestSettingRevisions[ setting.id ] <= latestRevision ) ) {
7723 + setting._dirty = false;
7724 + }
7725 + } );
7726 +
7727 + api.state( 'changesetStatus' ).set( '' );
7728 + api.settings.changeset.uuid = response.next_changeset_uuid;
7729 + if ( api.settings.changeset.branching ) {
7730 + parent.send( 'changeset-uuid', api.settings.changeset.uuid );
7731 + }
7732 + }
7733 +
7734 + // Prevent subsequent requestChangesetUpdate() calls from including the settings that have been saved.
7735 + api._lastSavedRevision = Math.max( latestRevision, api._lastSavedRevision );
7736 +
7737 + if ( response.setting_validities ) {
7738 + api._handleSettingValidities( {
7739 + settingValidities: response.setting_validities,
7740 + focusInvalidControl: true
7741 + } );
7742 + }
7743 +
7744 + deferred.resolveWith( previewer, [ response ] );
7745 + api.trigger( 'saved', response );
7746 +
7747 + // Restore the global dirty state if any settings were modified during save.
7748 + if ( ! _.isEmpty( modifiedWhileSaving ) ) {
7749 + api.state( 'saved' ).set( false );
7750 + }
7751 + } );
7752 + };
7753 +
7754 + if ( 0 === processing() ) {
7755 + submit();
7756 + } else {
7757 + submitWhenDoneProcessing = function () {
7758 + if ( 0 === processing() ) {
7759 + api.state.unbind( 'change', submitWhenDoneProcessing );
7760 + submit();
7761 + }
7762 + };
7763 + api.state.bind( 'change', submitWhenDoneProcessing );
7764 + }
7765 +
7766 + return deferred.promise();
7767 + },
7768 +
7769 + /**
7770 + * Trash the current changes.
7771 + *
7772 + * Revert the Customizer to its previously-published state.
7773 + *
7774 + * @since 4.9.0
7775 + *
7776 + * @return {jQuery.promise} Promise.
7777 + */
7778 + trash: function trash() {
7779 + var request, success, fail;
7780 +
7781 + api.state( 'trashing' ).set( true );
7782 + api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
7783 +
7784 + request = wp.ajax.post( 'customize_trash', {
7785 + customize_changeset_uuid: api.settings.changeset.uuid,
7786 + nonce: api.settings.nonce.trash
7787 + } );
7788 + api.notifications.add( new api.OverlayNotification( 'changeset_trashing', {
7789 + type: 'info',
7790 + message: api.l10n.revertingChanges,
7791 + loading: true
7792 + } ) );
7793 +
7794 + success = function() {
7795 + var urlParser = document.createElement( 'a' ), queryParams;
7796 +
7797 + api.state( 'changesetStatus' ).set( 'trash' );
7798 + api.each( function( setting ) {
7799 + setting._dirty = false;
7800 + } );
7801 + api.state( 'saved' ).set( true );
7802 +
7803 + // Go back to Customizer without changeset.
7804 + urlParser.href = location.href;
7805 + queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
7806 + delete queryParams.changeset_uuid;
7807 + queryParams['return'] = api.settings.url['return'];
7808 + urlParser.search = $.param( queryParams );
7809 + location.replace( urlParser.href );
7810 + };
7811 +
7812 + fail = function( code, message ) {
7813 + var notificationCode = code || 'unknown_error';
7814 + api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
7815 + api.state( 'trashing' ).set( false );
7816 + api.notifications.remove( 'changeset_trashing' );
7817 + api.notifications.add( new api.Notification( notificationCode, {
7818 + message: message || api.l10n.unknownError,
7819 + dismissible: true,
7820 + type: 'error'
7821 + } ) );
7822 + };
7823 +
7824 + request.done( function( response ) {
7825 + success( response.message );
7826 + } );
7827 +
7828 + request.fail( function( response ) {
7829 + var code = response.code || 'trashing_failed';
7830 + if ( response.success || 'non_existent_changeset' === code || 'changeset_already_trashed' === code ) {
7831 + success( response.message );
7832 + } else {
7833 + fail( code, response.message );
7834 + }
7835 + } );
7836 + },
7837 +
7838 + /**
7839 + * Builds the front preview URL with the current state of customizer.
7840 + *
7841 + * @since 4.9.0
7842 + *
7843 + * @return {string} Preview URL.
7844 + */
7845 + getFrontendPreviewUrl: function() {
7846 + var previewer = this, params, urlParser;
7847 + urlParser = document.createElement( 'a' );
7848 + urlParser.href = previewer.previewUrl.get();
7849 + params = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
7850 +
7851 + if ( api.state( 'changesetStatus' ).get() && 'publish' !== api.state( 'changesetStatus' ).get() ) {
7852 + params.customize_changeset_uuid = api.settings.changeset.uuid;
7853 + }
7854 + if ( ! api.state( 'activated' ).get() ) {
7855 + params.customize_theme = api.settings.theme.stylesheet;
7856 + }
7857 +
7858 + urlParser.search = $.param( params );
7859 + return urlParser.href;
7860 + }
7861 + });
7862 +
7863 + // Ensure preview nonce is included with every customized request, to allow post data to be read.
7864 + $.ajaxPrefilter( function injectPreviewNonce( options ) {
7865 + if ( ! /wp_customize=on/.test( options.data ) ) {
7866 + return;
7867 + }
7868 + options.data += '&' + $.param({
7869 + customize_preview_nonce: api.settings.nonce.preview
7870 + });
7871 + });
7872 +
7873 + // Refresh the nonces if the preview sends updated nonces over.
7874 + api.previewer.bind( 'nonce', function( nonce ) {
7875 + $.extend( this.nonce, nonce );
7876 + });
7877 +
7878 + // Refresh the nonces if login sends updated nonces over.
7879 + api.bind( 'nonce-refresh', function( nonce ) {
7880 + $.extend( api.settings.nonce, nonce );
7881 + $.extend( api.previewer.nonce, nonce );
7882 + api.previewer.send( 'nonce-refresh', nonce );
7883 + });
7884 +
7885 + // Create Settings.
7886 + $.each( api.settings.settings, function( id, data ) {
7887 + var Constructor = api.settingConstructor[ data.type ] || api.Setting;
7888 + api.add( new Constructor( id, data.value, {
7889 + transport: data.transport,
7890 + previewer: api.previewer,
7891 + dirty: !! data.dirty
7892 + } ) );
7893 + });
7894 +
7895 + // Create Panels.
7896 + $.each( api.settings.panels, function ( id, data ) {
7897 + var Constructor = api.panelConstructor[ data.type ] || api.Panel, options;
7898 + // Inclusion of params alias is for back-compat for custom panels that expect to augment this property.
7899 + options = _.extend( { params: data }, data );
7900 + api.panel.add( new Constructor( id, options ) );
7901 + });
7902 +
7903 + // Create Sections.
7904 + $.each( api.settings.sections, function ( id, data ) {
7905 + var Constructor = api.sectionConstructor[ data.type ] || api.Section, options;
7906 + // Inclusion of params alias is for back-compat for custom sections that expect to augment this property.
7907 + options = _.extend( { params: data }, data );
7908 + api.section.add( new Constructor( id, options ) );
7909 + });
7910 +
7911 + // Create Controls.
7912 + $.each( api.settings.controls, function( id, data ) {
7913 + var Constructor = api.controlConstructor[ data.type ] || api.Control, options;
7914 + // Inclusion of params alias is for back-compat for custom controls that expect to augment this property.
7915 + options = _.extend( { params: data }, data );
7916 + api.control.add( new Constructor( id, options ) );
7917 + });
7918 +
7919 + // Focus the autofocused element.
7920 + _.each( [ 'panel', 'section', 'control' ], function( type ) {
7921 + var id = api.settings.autofocus[ type ];
7922 + if ( ! id ) {
7923 + return;
7924 + }
7925 +
7926 + /*
7927 + * Defer focus until:
7928 + * 1. The panel, section, or control exists (especially for dynamically-created ones).
7929 + * 2. The instance is embedded in the document (and so is focusable).
7930 + * 3. The preview has finished loading so that the active states have been set.
7931 + */
7932 + api[ type ]( id, function( instance ) {
7933 + instance.deferred.embedded.done( function() {
7934 + api.previewer.deferred.active.done( function() {
7935 + instance.focus();
7936 + });
7937 + });
7938 + });
7939 + });
7940 +
7941 + api.bind( 'ready', api.reflowPaneContents );
7942 + $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
7943 + var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, api.settings.timeouts.reflowPaneContents );
7944 + values.bind( 'add', debouncedReflowPaneContents );
7945 + values.bind( 'change', debouncedReflowPaneContents );
7946 + values.bind( 'remove', debouncedReflowPaneContents );
7947 + } );
7948 +
7949 + // Set up global notifications area.
7950 + api.bind( 'ready', function setUpGlobalNotificationsArea() {
7951 + var sidebar, containerHeight, containerInitialTop;
7952 + api.notifications.container = $( '#customize-notifications-area' );
7953 +
7954 + api.notifications.bind( 'change', _.debounce( function() {
7955 + api.notifications.render();
7956 + } ) );
7957 +
7958 + sidebar = $( '.wp-full-overlay-sidebar-content' );
7959 + api.notifications.bind( 'rendered', function updateSidebarTop() {
7960 + sidebar.css( 'top', '' );
7961 + if ( 0 !== api.notifications.count() ) {
7962 + containerHeight = api.notifications.container.outerHeight() + 1;
7963 + containerInitialTop = parseInt( sidebar.css( 'top' ), 10 );
7964 + sidebar.css( 'top', containerInitialTop + containerHeight + 'px' );
7965 + }
7966 + api.notifications.trigger( 'sidebarTopUpdated' );
7967 + });
7968 +
7969 + api.notifications.render();
7970 + });
7971 +
7972 + // Save and activated states.
7973 + (function( state ) {
7974 + var saved = state.instance( 'saved' ),
7975 + saving = state.instance( 'saving' ),
7976 + trashing = state.instance( 'trashing' ),
7977 + activated = state.instance( 'activated' ),
7978 + processing = state.instance( 'processing' ),
7979 + paneVisible = state.instance( 'paneVisible' ),
7980 + expandedPanel = state.instance( 'expandedPanel' ),
7981 + expandedSection = state.instance( 'expandedSection' ),
7982 + changesetStatus = state.instance( 'changesetStatus' ),
7983 + selectedChangesetStatus = state.instance( 'selectedChangesetStatus' ),
7984 + changesetDate = state.instance( 'changesetDate' ),
7985 + selectedChangesetDate = state.instance( 'selectedChangesetDate' ),
7986 + previewerAlive = state.instance( 'previewerAlive' ),
7987 + editShortcutVisibility = state.instance( 'editShortcutVisibility' ),
7988 + changesetLocked = state.instance( 'changesetLocked' ),
7989 + populateChangesetUuidParam, defaultSelectedChangesetStatus;
7990 +
7991 + state.bind( 'change', function() {
7992 + var canSave;
7993 +
7994 + if ( ! activated() ) {
7995 + saveBtn.val( api.l10n.activate );
7996 + closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
7997 +
7998 + } else if ( '' === changesetStatus.get() && saved() ) {
7999 + if ( api.settings.changeset.currentUserCanPublish ) {
8000 + saveBtn.val( api.l10n.published );
8001 + } else {
8002 + saveBtn.val( api.l10n.saved );
8003 + }
8004 + closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
8005 +
8006 + } else {
8007 + if ( 'draft' === selectedChangesetStatus() ) {
8008 + if ( saved() && selectedChangesetStatus() === changesetStatus() ) {
8009 + saveBtn.val( api.l10n.draftSaved );
8010 + } else {
8011 + saveBtn.val( api.l10n.saveDraft );
8012 + }
8013 + } else if ( 'future' === selectedChangesetStatus() ) {
8014 + if ( saved() && selectedChangesetStatus() === changesetStatus() ) {
8015 + if ( changesetDate.get() !== selectedChangesetDate.get() ) {
8016 + saveBtn.val( api.l10n.schedule );
8017 + } else {
8018 + saveBtn.val( api.l10n.scheduled );
8019 + }
8020 + } else {
8021 + saveBtn.val( api.l10n.schedule );
8022 + }
8023 + } else if ( api.settings.changeset.currentUserCanPublish ) {
8024 + saveBtn.val( api.l10n.publish );
8025 + }
8026 + closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
8027 + }
8028 +
8029 + /*
8030 + * Save (publish) button should be enabled if saving is not currently happening,
8031 + * and if the theme is not active or the changeset exists but is not published.
8032 + */
8033 + canSave = ! saving() && ! trashing() && ! changesetLocked() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) );
8034 +
8035 + saveBtn.prop( 'disabled', ! canSave );
8036 + });
8037 +
8038 + selectedChangesetStatus.validate = function( status ) {
8039 + if ( '' === status || 'auto-draft' === status ) {
8040 + return null;
8041 + }
8042 + return status;
8043 + };
8044 +
8045 + defaultSelectedChangesetStatus = api.settings.changeset.currentUserCanPublish ? 'publish' : 'draft';
8046 +
8047 + // Set default states.
8048 + changesetStatus( api.settings.changeset.status );
8049 + changesetLocked( Boolean( api.settings.changeset.lockUser ) );
8050 + changesetDate( api.settings.changeset.publishDate );
8051 + selectedChangesetDate( api.settings.changeset.publishDate );
8052 + selectedChangesetStatus( '' === api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ? defaultSelectedChangesetStatus : api.settings.changeset.status );
8053 + selectedChangesetStatus.link( changesetStatus ); // Ensure that direct updates to status on server via wp.customizer.previewer.save() will update selection.
8054 + saved( true );
8055 + if ( '' === changesetStatus() ) { // Handle case for loading starter content.
8056 + api.each( function( setting ) {
8057 + if ( setting._dirty ) {
8058 + saved( false );
8059 + }
8060 + } );
8061 + }
8062 + saving( false );
8063 + activated( api.settings.theme.active );
8064 + processing( 0 );
8065 + paneVisible( true );
8066 + expandedPanel( false );
8067 + expandedSection( false );
8068 + previewerAlive( true );
8069 + editShortcutVisibility( 'visible' );
8070 +
8071 + api.bind( 'change', function() {
8072 + if ( state( 'saved' ).get() ) {
8073 + state( 'saved' ).set( false );
8074 + }
8075 + });
8076 +
8077 + // Populate changeset UUID param when state becomes dirty.
8078 + if ( api.settings.changeset.branching ) {
8079 + saved.bind( function( isSaved ) {
8080 + if ( ! isSaved ) {
8081 + populateChangesetUuidParam( true );
8082 + }
8083 + });
8084 + }
8085 +
8086 + saving.bind( function( isSaving ) {
8087 + body.toggleClass( 'saving', isSaving );
8088 + } );
8089 + trashing.bind( function( isTrashing ) {
8090 + body.toggleClass( 'trashing', isTrashing );
8091 + } );
8092 +
8093 + api.bind( 'saved', function( response ) {
8094 + state('saved').set( true );
8095 + if ( 'publish' === response.changeset_status ) {
8096 + state( 'activated' ).set( true );
8097 + }
8098 + });
8099 +
8100 + activated.bind( function( to ) {
8101 + if ( to ) {
8102 + api.trigger( 'activated' );
8103 + }
8104 + });
8105 +
8106 + /**
8107 + * Populate URL with UUID via `history.replaceState()`.
8108 + *
8109 + * @since 4.7.0
8110 + * @access private
8111 + *
8112 + * @param {boolean} isIncluded Is UUID included.
8113 + * @return {void}
8114 + */
8115 + populateChangesetUuidParam = function( isIncluded ) {
8116 + var urlParser, queryParams;
8117 +
8118 + // Abort on IE9 which doesn't support history management.
8119 + if ( ! history.replaceState ) {
8120 + return;
8121 + }
8122 +
8123 + urlParser = document.createElement( 'a' );
8124 + urlParser.href = location.href;
8125 + queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
8126 + if ( isIncluded ) {
8127 + if ( queryParams.changeset_uuid === api.settings.changeset.uuid ) {
8128 + return;
8129 + }
8130 + queryParams.changeset_uuid = api.settings.changeset.uuid;
8131 + } else {
8132 + if ( ! queryParams.changeset_uuid ) {
8133 + return;
8134 + }
8135 + delete queryParams.changeset_uuid;
8136 + }
8137 + urlParser.search = $.param( queryParams );
8138 + history.replaceState( {}, document.title, urlParser.href );
8139 + };
8140 +
8141 + // Show changeset UUID in URL when in branching mode and there is a saved changeset.
8142 + if ( api.settings.changeset.branching ) {
8143 + changesetStatus.bind( function( newStatus ) {
8144 + populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus && 'trash' !== newStatus );
8145 + } );
8146 + }
8147 + }( api.state ) );
8148 +
8149 + /**
8150 + * Handles lock notice and take over request.
8151 + *
8152 + * @since 4.9.0
8153 + */
8154 + ( function checkAndDisplayLockNotice() {
8155 +
8156 + var LockedNotification = api.OverlayNotification.extend(/** @lends wp.customize~LockedNotification.prototype */{
8157 +
8158 + /**
8159 + * Template ID.
8160 + *
8161 + * @type {string}
8162 + */
8163 + templateId: 'customize-changeset-locked-notification',
8164 +
8165 + /**
8166 + * Lock user.
8167 + *
8168 + * @type {object}
8169 + */
8170 + lockUser: null,
8171 +
8172 + /**
8173 + * A notification that is displayed in a full-screen overlay with information about the locked changeset.
8174 + *
8175 + * @constructs wp.customize~LockedNotification
8176 + * @augments wp.customize.OverlayNotification
8177 + *
8178 + * @since 4.9.0
8179 + *
8180 + * @param {string} [code] - Code.
8181 + * @param {Object} [params] - Params.
8182 + */
8183 + initialize: function( code, params ) {
8184 + var notification = this, _code, _params;
8185 + _code = code || 'changeset_locked';
8186 + _params = _.extend(
8187 + {
8188 + message: '',
8189 + type: 'warning',
8190 + containerClasses: '',
8191 + lockUser: {}
8192 + },
8193 + params
8194 + );
8195 + _params.containerClasses += ' notification-changeset-locked';
8196 + api.OverlayNotification.prototype.initialize.call( notification, _code, _params );
8197 + },
8198 +
8199 + /**
8200 + * Render notification.
8201 + *
8202 + * @since 4.9.0
8203 + *
8204 + * @return {jQuery} Notification container.
8205 + */
8206 + render: function() {
8207 + var notification = this, li, data, takeOverButton, request;
8208 + data = _.extend(
8209 + {
8210 + allowOverride: false,
8211 + returnUrl: api.settings.url['return'],
8212 + previewUrl: api.previewer.previewUrl.get(),
8213 + frontendPreviewUrl: api.previewer.getFrontendPreviewUrl()
8214 + },
8215 + this
8216 + );
8217 +
8218 + li = api.OverlayNotification.prototype.render.call( data );
8219 +
8220 + // Try to autosave the changeset now.
8221 + api.requestChangesetUpdate( {}, { autosave: true } ).fail( function( response ) {
8222 + if ( ! response.autosaved ) {
8223 + li.find( '.notice-error' ).prop( 'hidden', false ).text( response.message || api.l10n.unknownRequestFail );
8224 + }
8225 + } );
8226 +
8227 + takeOverButton = li.find( '.customize-notice-take-over-button' );
8228 + takeOverButton.on( 'click', function( event ) {
8229 + event.preventDefault();
8230 + if ( request ) {
8231 + return;
8232 + }
8233 +
8234 + takeOverButton.addClass( 'disabled' );
8235 + request = wp.ajax.post( 'customize_override_changeset_lock', {
8236 + wp_customize: 'on',
8237 + customize_theme: api.settings.theme.stylesheet,
8238 + customize_changeset_uuid: api.settings.changeset.uuid,
8239 + nonce: api.settings.nonce.override_lock
8240 + } );
8241 +
8242 + request.done( function() {
8243 + api.notifications.remove( notification.code ); // Remove self.
8244 + api.state( 'changesetLocked' ).set( false );
8245 + } );
8246 +
8247 + request.fail( function( response ) {
8248 + var message = response.message || api.l10n.unknownRequestFail;
8249 + li.find( '.notice-error' ).prop( 'hidden', false ).text( message );
8250 +
8251 + request.always( function() {
8252 + takeOverButton.removeClass( 'disabled' );
8253 + } );
8254 + } );
8255 +
8256 + request.always( function() {
8257 + request = null;
8258 + } );
8259 + } );
8260 +
8261 + return li;
8262 + }
8263 + });
8264 +
8265 + /**
8266 + * Start lock.
8267 + *
8268 + * @since 4.9.0
8269 + *
8270 + * @param {Object} [args] - Args.
8271 + * @param {Object} [args.lockUser] - Lock user data.
8272 + * @param {boolean} [args.allowOverride=false] - Whether override is allowed.
8273 + * @return {void}
8274 + */
8275 + function startLock( args ) {
8276 + if ( args && args.lockUser ) {
8277 + api.settings.changeset.lockUser = args.lockUser;
8278 + }
8279 + api.state( 'changesetLocked' ).set( true );
8280 + api.notifications.add( new LockedNotification( 'changeset_locked', {
8281 + lockUser: api.settings.changeset.lockUser,
8282 + allowOverride: Boolean( args && args.allowOverride )
8283 + } ) );
8284 + }
8285 +
8286 + // Show initial notification.
8287 + if ( api.settings.changeset.lockUser ) {
8288 + startLock( { allowOverride: true } );
8289 + }
8290 +
8291 + // Check for lock when sending heartbeat requests.
8292 + $( document ).on( 'heartbeat-send.update_lock_notice', function( event, data ) {
8293 + data.check_changeset_lock = true;
8294 + data.changeset_uuid = api.settings.changeset.uuid;
8295 + } );
8296 +
8297 + // Handle heartbeat ticks.
8298 + $( document ).on( 'heartbeat-tick.update_lock_notice', function( event, data ) {
8299 + var notification, code = 'changeset_locked';
8300 + if ( ! data.customize_changeset_lock_user ) {
8301 + return;
8302 + }
8303 +
8304 + // Update notification when a different user takes over.
8305 + notification = api.notifications( code );
8306 + if ( notification && notification.lockUser.id !== api.settings.changeset.lockUser.id ) {
8307 + api.notifications.remove( code );
8308 + }
8309 +
8310 + startLock( {
8311 + lockUser: data.customize_changeset_lock_user
8312 + } );
8313 + } );
8314 +
8315 + // Handle locking in response to changeset save errors.
8316 + api.bind( 'error', function( response ) {
8317 + if ( 'changeset_locked' === response.code && response.lock_user ) {
8318 + startLock( {
8319 + lockUser: response.lock_user
8320 + } );
8321 + }
8322 + } );
8323 + } )();
8324 +
8325 + // Set up initial notifications.
8326 + (function() {
8327 + var removedQueryParams = [], autosaveDismissed = false;
8328 +
8329 + /**
8330 + * Obtain the URL to restore the autosave.
8331 + *
8332 + * @return {string} Customizer URL.
8333 + */
8334 + function getAutosaveRestorationUrl() {
8335 + var urlParser, queryParams;
8336 + urlParser = document.createElement( 'a' );
8337 + urlParser.href = location.href;
8338 + queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
8339 + if ( api.settings.changeset.latestAutoDraftUuid ) {
8340 + queryParams.changeset_uuid = api.settings.changeset.latestAutoDraftUuid;
8341 + } else {
8342 + queryParams.customize_autosaved = 'on';
8343 + }
8344 + queryParams['return'] = api.settings.url['return'];
8345 + urlParser.search = $.param( queryParams );
8346 + return urlParser.href;
8347 + }
8348 +
8349 + /**
8350 + * Remove parameter from the URL.
8351 + *
8352 + * @param {Array} params - Parameter names to remove.
8353 + * @return {void}
8354 + */
8355 + function stripParamsFromLocation( params ) {
8356 + var urlParser = document.createElement( 'a' ), queryParams, strippedParams = 0;
8357 + urlParser.href = location.href;
8358 + queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
8359 + _.each( params, function( param ) {
8360 + if ( 'undefined' !== typeof queryParams[ param ] ) {
8361 + strippedParams += 1;
8362 + delete queryParams[ param ];
8363 + }
8364 + } );
8365 + if ( 0 === strippedParams ) {
8366 + return;
8367 + }
8368 +
8369 + urlParser.search = $.param( queryParams );
8370 + history.replaceState( {}, document.title, urlParser.href );
8371 + }
8372 +
8373 + /**
8374 + * Displays a Site Editor notification when a block theme is activated.
8375 + *
8376 + * @since 4.9.0
8377 + *
8378 + * @param {string} [notification] - A notification to display.
8379 + * @return {void}
8380 + */
8381 + function addSiteEditorNotification( notification ) {
8382 + api.notifications.add( new api.Notification( 'site_editor_block_theme_notice', {
8383 + message: notification,
8384 + type: 'info',
8385 + dismissible: false,
8386 + render: function() {
8387 + var notification = api.Notification.prototype.render.call( this ),
8388 + button = notification.find( 'button.switch-to-editor' );
8389 +
8390 + button.on( 'click', function( event ) {
8391 + event.preventDefault();
8392 + location.assign( button.data( 'action' ) );
8393 + } );
8394 +
8395 + return notification;
8396 + }
8397 + } ) );
8398 + }
8399 +
8400 + /**
8401 + * Dismiss autosave.
8402 + *
8403 + * @return {void}
8404 + */
8405 + function dismissAutosave() {
8406 + if ( autosaveDismissed ) {
8407 + return;
8408 + }
8409 + wp.ajax.post( 'customize_dismiss_autosave_or_lock', {
8410 + wp_customize: 'on',
8411 + customize_theme: api.settings.theme.stylesheet,
8412 + customize_changeset_uuid: api.settings.changeset.uuid,
8413 + nonce: api.settings.nonce.dismiss_autosave_or_lock,
8414 + dismiss_autosave: true
8415 + } );
8416 + autosaveDismissed = true;
8417 + }
8418 +
8419 + /**
8420 + * Add notification regarding the availability of an autosave to restore.
8421 + *
8422 + * @return {void}
8423 + */
8424 + function addAutosaveRestoreNotification() {
8425 + var code = 'autosave_available', onStateChange;
8426 +
8427 + // Since there is an autosave revision and the user hasn't loaded with autosaved, add notification to prompt to load autosaved version.
8428 + api.notifications.add( new api.Notification( code, {
8429 + message: api.l10n.autosaveNotice,
8430 + type: 'warning',
8431 + dismissible: true,
8432 + render: function() {
8433 + var li = api.Notification.prototype.render.call( this ), link;
8434 +
8435 + // Handle clicking on restoration link.
8436 + link = li.find( 'a' );
8437 + link.prop( 'href', getAutosaveRestorationUrl() );
8438 + link.on( 'click', function( event ) {
8439 + event.preventDefault();
8440 + location.replace( getAutosaveRestorationUrl() );
8441 + } );
8442 +
8443 + // Handle dismissal of notice.
8444 + li.find( '.notice-dismiss' ).on( 'click', dismissAutosave );
8445 +
8446 + return li;
8447 + }
8448 + } ) );
8449 +
8450 + // Remove the notification once the user starts making changes.
8451 + onStateChange = function() {
8452 + dismissAutosave();
8453 + api.notifications.remove( code );
8454 + api.unbind( 'change', onStateChange );
8455 + api.state( 'changesetStatus' ).unbind( onStateChange );
8456 + };
8457 + api.bind( 'change', onStateChange );
8458 + api.state( 'changesetStatus' ).bind( onStateChange );
8459 + }
8460 +
8461 + if ( api.settings.changeset.autosaved ) {
8462 + api.state( 'saved' ).set( false );
8463 + removedQueryParams.push( 'customize_autosaved' );
8464 + }
8465 + if ( ! api.settings.changeset.branching && ( ! api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ) ) {
8466 + removedQueryParams.push( 'changeset_uuid' ); // Remove UUID when restoring autosave auto-draft.
8467 + }
8468 + if ( removedQueryParams.length > 0 ) {
8469 + stripParamsFromLocation( removedQueryParams );
8470 + }
8471 + if ( api.settings.changeset.latestAutoDraftUuid || api.settings.changeset.hasAutosaveRevision ) {
8472 + addAutosaveRestoreNotification();
8473 + }
8474 + var shouldDisplayBlockThemeNotification = !! parseInt( $( '#customize-info' ).data( 'block-theme' ), 10 );
8475 + if (shouldDisplayBlockThemeNotification) {
8476 + addSiteEditorNotification( api.l10n.blockThemeNotification );
8477 + }
8478 + })();
8479 +
8480 + // Check if preview url is valid and load the preview frame.
8481 + if ( api.previewer.previewUrl() ) {
8482 + api.previewer.refresh();
8483 + } else {
8484 + api.previewer.previewUrl( api.settings.url.home );
8485 + }
8486 +
8487 + // Button bindings.
8488 + saveBtn.on( 'click', function( event ) {
8489 + api.previewer.save();
8490 + event.preventDefault();
8491 + }).on( 'keydown', function( event ) {
8492 + if ( 9 === event.which ) { // Tab.
8493 + return;
8494 + }
8495 + if ( 13 === event.which ) { // Enter.
8496 + api.previewer.save();
8497 + }
8498 + event.preventDefault();
8499 + });
8500 +
8501 + closeBtn.on( 'keydown', function( event ) {
8502 + if ( 9 === event.which ) { // Tab.
8503 + return;
8504 + }
8505 + if ( 13 === event.which ) { // Enter.
8506 + this.click();
8507 + }
8508 + event.preventDefault();
8509 + });
8510 +
8511 + $( '.collapse-sidebar' ).on( 'click', function() {
8512 + api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() );
8513 + });
8514 +
8515 + api.state( 'paneVisible' ).bind( function( paneVisible ) {
8516 + overlay.toggleClass( 'preview-only', ! paneVisible );
8517 + overlay.toggleClass( 'expanded', paneVisible );
8518 + overlay.toggleClass( 'collapsed', ! paneVisible );
8519 +
8520 + if ( ! paneVisible ) {
8521 + $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar });
8522 + } else {
8523 + $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar });
8524 + }
8525 + });
8526 +
8527 + // Keyboard shortcuts - esc to exit section/panel.
8528 + body.on( 'keydown', function( event ) {
8529 + var collapsedObject, expandedControls = [], expandedSections = [], expandedPanels = [];
8530 +
8531 + if ( 27 !== event.which ) { // Esc.
8532 + return;
8533 + }
8534 +
8535 + /*
8536 + * Abort if the event target is not the body (the default) and not inside of #customize-controls.
8537 + * This ensures that ESC meant to collapse a modal dialog or a TinyMCE toolbar won't collapse something else.
8538 + */
8539 + if ( ! $( event.target ).is( 'body' ) && ! $.contains( $( '#customize-controls' )[0], event.target ) ) {
8540 + return;
8541 + }
8542 +
8543 + // Abort if we're inside of a block editor instance.
8544 + if ( event.target.closest( '.block-editor-writing-flow' ) !== null ||
8545 + event.target.closest( '.block-editor-block-list__block-popover' ) !== null
8546 + ) {
8547 + return;
8548 + }
8549 +
8550 + // Check for expanded expandable controls (e.g. widgets and nav menus items), sections, and panels.
8551 + api.control.each( function( control ) {
8552 + if ( control.expanded && control.expanded() && _.isFunction( control.collapse ) ) {
8553 + expandedControls.push( control );
8554 + }
8555 + });
8556 + api.section.each( function( section ) {
8557 + if ( section.expanded() ) {
8558 + expandedSections.push( section );
8559 + }
8560 + });
8561 + api.panel.each( function( panel ) {
8562 + if ( panel.expanded() ) {
8563 + expandedPanels.push( panel );
8564 + }
8565 + });
8566 +
8567 + // Skip collapsing expanded controls if there are no expanded sections.
8568 + if ( expandedControls.length > 0 && 0 === expandedSections.length ) {
8569 + expandedControls.length = 0;
8570 + }
8571 +
8572 + // Collapse the most granular expanded object.
8573 + collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
8574 + if ( collapsedObject ) {
8575 + if ( 'themes' === collapsedObject.params.type ) {
8576 +
8577 + // Themes panel or section.
8578 + if ( body.hasClass( 'modal-open' ) ) {
8579 + collapsedObject.closeDetails();
8580 + } else if ( api.panel.has( 'themes' ) ) {
8581 +
8582 + // If we're collapsing a section, collapse the panel also.
8583 + api.panel( 'themes' ).collapse();
8584 + }
8585 + return;
8586 + }
8587 + collapsedObject.collapse();
8588 + event.preventDefault();
8589 + }
8590 + });
8591 +
8592 + $( '.customize-controls-preview-toggle' ).on( 'click', function() {
8593 + api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() );
8594 + });
8595 +
8596 + /*
8597 + * Sticky header feature.
8598 + */
8599 + (function initStickyHeaders() {
8600 + var parentContainer = $( '.wp-full-overlay-sidebar-content' ),
8601 + changeContainer, updateHeaderHeight, releaseStickyHeader, resetStickyHeader, positionStickyHeader,
8602 + activeHeader, lastScrollTop;
8603 +
8604 + /**
8605 + * Determine which panel or section is currently expanded.
8606 + *
8607 + * @since 4.7.0
8608 + * @access private
8609 + *
8610 + * @param {wp.customize.Panel|wp.customize.Section} container Construct.
8611 + * @return {void}
8612 + */
8613 + changeContainer = function( container ) {
8614 + var newInstance = container,
8615 + expandedSection = api.state( 'expandedSection' ).get(),
8616 + expandedPanel = api.state( 'expandedPanel' ).get(),
8617 + headerElement;
8618 +
8619 + if ( activeHeader && activeHeader.element ) {
8620 + // Release previously active header element.
8621 + releaseStickyHeader( activeHeader.element );
8622 +
8623 + // Remove event listener in the previous panel or section.
8624 + activeHeader.element.find( '.description' ).off( 'toggled', updateHeaderHeight );
8625 + }
8626 +
8627 + if ( ! newInstance ) {
8628 + if ( ! expandedSection && expandedPanel && expandedPanel.contentContainer ) {
8629 + newInstance = expandedPanel;
8630 + } else if ( ! expandedPanel && expandedSection && expandedSection.contentContainer ) {
8631 + newInstance = expandedSection;
8632 + } else {
8633 + activeHeader = false;
8634 + return;
8635 + }
8636 + }
8637 +
8638 + headerElement = newInstance.contentContainer.find( '.customize-section-title, .panel-meta' ).first();
8639 + if ( headerElement.length ) {
8640 + activeHeader = {
8641 + instance: newInstance,
8642 + element: headerElement,
8643 + parent: headerElement.closest( '.customize-pane-child' ),
8644 + height: headerElement.outerHeight()
8645 + };
8646 +
8647 + // Update header height whenever help text is expanded or collapsed.
8648 + activeHeader.element.find( '.description' ).on( 'toggled', updateHeaderHeight );
8649 +
8650 + if ( expandedSection ) {
8651 + resetStickyHeader( activeHeader.element, activeHeader.parent );
8652 + }
8653 + } else {
8654 + activeHeader = false;
8655 + }
8656 + };
8657 + api.state( 'expandedSection' ).bind( changeContainer );
8658 + api.state( 'expandedPanel' ).bind( changeContainer );
8659 +
8660 + // Throttled scroll event handler.
8661 + parentContainer.on( 'scroll', _.throttle( function() {
8662 + if ( ! activeHeader ) {
8663 + return;
8664 + }
8665 +
8666 + var scrollTop = parentContainer.scrollTop(),
8667 + scrollDirection;
8668 +
8669 + if ( ! lastScrollTop ) {
8670 + scrollDirection = 1;
8671 + } else {
8672 + if ( scrollTop === lastScrollTop ) {
8673 + scrollDirection = 0;
8674 + } else if ( scrollTop > lastScrollTop ) {
8675 + scrollDirection = 1;
8676 + } else {
8677 + scrollDirection = -1;
8678 + }
8679 + }
8680 + lastScrollTop = scrollTop;
8681 + if ( 0 !== scrollDirection ) {
8682 + positionStickyHeader( activeHeader, scrollTop, scrollDirection );
8683 + }
8684 + }, 8 ) );
8685 +
8686 + // Update header position on sidebar layout change.
8687 + api.notifications.bind( 'sidebarTopUpdated', function() {
8688 + if ( activeHeader && activeHeader.element.hasClass( 'is-sticky' ) ) {
8689 + activeHeader.element.css( 'top', parentContainer.css( 'top' ) );
8690 + }
8691 + });
8692 +
8693 + // Release header element if it is sticky.
8694 + releaseStickyHeader = function( headerElement ) {
8695 + if ( ! headerElement.hasClass( 'is-sticky' ) ) {
8696 + return;
8697 + }
8698 + headerElement
8699 + .removeClass( 'is-sticky' )
8700 + .addClass( 'maybe-sticky is-in-view' )
8701 + .css( 'top', parentContainer.scrollTop() + 'px' );
8702 + };
8703 +
8704 + // Reset position of the sticky header.
8705 + resetStickyHeader = function( headerElement, headerParent ) {
8706 + if ( headerElement.hasClass( 'is-in-view' ) ) {
8707 + headerElement
8708 + .removeClass( 'maybe-sticky is-in-view' )
8709 + .css( {
8710 + width: '',
8711 + top: ''
8712 + } );
8713 + headerParent.css( 'padding-top', '' );
8714 + }
8715 + };
8716 +
8717 + /**
8718 + * Update active header height.
8719 + *
8720 + * @since 4.7.0
8721 + * @access private
8722 + *
8723 + * @return {void}
8724 + */
8725 + updateHeaderHeight = function() {
8726 + activeHeader.height = activeHeader.element.outerHeight();
8727 + };
8728 +
8729 + /**
8730 + * Reposition header on throttled `scroll` event.
8731 + *
8732 + * @since 4.7.0
8733 + * @access private
8734 + *
8735 + * @param {Object} header - Header.
8736 + * @param {number} scrollTop - Scroll top.
8737 + * @param {number} scrollDirection - Scroll direction, negative number being up and positive being down.
8738 + * @return {void}
8739 + */
8740 + positionStickyHeader = function( header, scrollTop, scrollDirection ) {
8741 + var headerElement = header.element,
8742 + headerParent = header.parent,
8743 + headerHeight = header.height,
8744 + headerTop = parseInt( headerElement.css( 'top' ), 10 ),
8745 + maybeSticky = headerElement.hasClass( 'maybe-sticky' ),
8746 + isSticky = headerElement.hasClass( 'is-sticky' ),
8747 + isInView = headerElement.hasClass( 'is-in-view' ),
8748 + isScrollingUp = ( -1 === scrollDirection );
8749 +
8750 + // When scrolling down, gradually hide sticky header.
8751 + if ( ! isScrollingUp ) {
8752 + if ( isSticky ) {
8753 + headerTop = scrollTop;
8754 + headerElement
8755 + .removeClass( 'is-sticky' )
8756 + .css( {
8757 + top: headerTop + 'px',
8758 + width: ''
8759 + } );
8760 + }
8761 + if ( isInView && scrollTop > headerTop + headerHeight ) {
8762 + headerElement.removeClass( 'is-in-view' );
8763 + headerParent.css( 'padding-top', '' );
8764 + }
8765 + return;
8766 + }
8767 +
8768 + // Scrolling up.
8769 + if ( ! maybeSticky && scrollTop >= headerHeight ) {
8770 + maybeSticky = true;
8771 + headerElement.addClass( 'maybe-sticky' );
8772 + } else if ( 0 === scrollTop ) {
8773 + // Reset header in base position.
8774 + headerElement
8775 + .removeClass( 'maybe-sticky is-in-view is-sticky' )
8776 + .css( {
8777 + top: '',
8778 + width: ''
8779 + } );
8780 + headerParent.css( 'padding-top', '' );
8781 + return;
8782 + }
8783 +
8784 + if ( isInView && ! isSticky ) {
8785 + // Header is in the view but is not yet sticky.
8786 + if ( headerTop >= scrollTop ) {
8787 + // Header is fully visible.
8788 + headerElement
8789 + .addClass( 'is-sticky' )
8790 + .css( {
8791 + top: parentContainer.css( 'top' ),
8792 + width: headerParent.outerWidth() + 'px'
8793 + } );
8794 + }
8795 + } else if ( maybeSticky && ! isInView ) {
8796 + // Header is out of the view.
8797 + headerElement
8798 + .addClass( 'is-in-view' )
8799 + .css( 'top', ( scrollTop - headerHeight ) + 'px' );
8800 + headerParent.css( 'padding-top', headerHeight + 'px' );
8801 + }
8802 + };
8803 + }());
8804 +
8805 + // Previewed device bindings. (The api.previewedDevice property
8806 + // is how this Value was first introduced, but since it has moved to api.state.)
8807 + api.previewedDevice = api.state( 'previewedDevice' );
8808 +
8809 + // Set the default device.
8810 + api.bind( 'ready', function() {
8811 + _.find( api.settings.previewableDevices, function( value, key ) {
8812 + if ( true === value['default'] ) {
8813 + api.previewedDevice.set( key );
8814 + return true;
8815 + }
8816 + } );
8817 + } );
8818 +
8819 + // Set the toggled device.
8820 + footerActions.find( '.devices button' ).on( 'click', function( event ) {
8821 + api.previewedDevice.set( $( event.currentTarget ).data( 'device' ) );
8822 + });
8823 +
8824 + // Bind device changes.
8825 + api.previewedDevice.bind( function( newDevice ) {
8826 + var overlay = $( '.wp-full-overlay' ),
8827 + devices = '';
8828 +
8829 + footerActions.find( '.devices button' )
8830 + .removeClass( 'active' )
8831 + .attr( 'aria-pressed', false );
8832 +
8833 + footerActions.find( '.devices .preview-' + newDevice )
8834 + .addClass( 'active' )
8835 + .attr( 'aria-pressed', true );
8836 +
8837 + $.each( api.settings.previewableDevices, function( device ) {
8838 + devices += ' preview-' + device;
8839 + } );
8840 +
8841 + overlay
8842 + .removeClass( devices )
8843 + .addClass( 'preview-' + newDevice );
8844 + } );
8845 +
8846 + // Bind site title display to the corresponding field.
8847 + if ( title.length ) {
8848 + api( 'blogname', function( setting ) {
8849 + var updateTitle = function() {
8850 + var blogTitle = setting() || '';
8851 + title.text( blogTitle.toString().trim() || api.l10n.untitledBlogName );
8852 + };
8853 + setting.bind( updateTitle );
8854 + updateTitle();
8855 + } );
8856 + }
8857 +
8858 + /*
8859 + * Create a postMessage connection with a parent frame,
8860 + * in case the Customizer frame was opened with the Customize loader.
8861 + *
8862 + * @see wp.customize.Loader
8863 + */
8864 + parent = new api.Messenger({
8865 + url: api.settings.url.parent,
8866 + channel: 'loader'
8867 + });
8868 +
8869 + // Handle exiting of Customizer.
8870 + (function() {
8871 + var isInsideIframe = false;
8872 +
8873 + function isCleanState() {
8874 + var defaultChangesetStatus;
8875 +
8876 + /*
8877 + * Handle special case of previewing theme switch since some settings (for nav menus and widgets)
8878 + * are pre-dirty and non-active themes can only ever be auto-drafts.
8879 + */
8880 + if ( ! api.state( 'activated' ).get() ) {
8881 + return 0 === api._latestRevision;
8882 + }
8883 +
8884 + // Dirty if the changeset status has been changed but not saved yet.
8885 + defaultChangesetStatus = api.state( 'changesetStatus' ).get();
8886 + if ( '' === defaultChangesetStatus || 'auto-draft' === defaultChangesetStatus ) {
8887 + defaultChangesetStatus = 'publish';
8888 + }
8889 + if ( api.state( 'selectedChangesetStatus' ).get() !== defaultChangesetStatus ) {
8890 + return false;
8891 + }
8892 +
8893 + // Dirty if scheduled but the changeset date hasn't been saved yet.
8894 + if ( 'future' === api.state( 'selectedChangesetStatus' ).get() && api.state( 'selectedChangesetDate' ).get() !== api.state( 'changesetDate' ).get() ) {
8895 + return false;
8896 + }
8897 +
8898 + return api.state( 'saved' ).get() && 'auto-draft' !== api.state( 'changesetStatus' ).get();
8899 + }
8900 +
8901 + /*
8902 + * If we receive a 'back' event, we're inside an iframe.
8903 + * Send any clicks to the 'Return' link to the parent page.
8904 + */
8905 + parent.bind( 'back', function() {
8906 + isInsideIframe = true;
8907 + });
8908 +
8909 + function startPromptingBeforeUnload() {
8910 + api.unbind( 'change', startPromptingBeforeUnload );
8911 + api.state( 'selectedChangesetStatus' ).unbind( startPromptingBeforeUnload );
8912 + api.state( 'selectedChangesetDate' ).unbind( startPromptingBeforeUnload );
8913 +
8914 + // Prompt user with AYS dialog if leaving the Customizer with unsaved changes.
8915 + $( window ).on( 'beforeunload.customize-confirm', function() {
8916 + if ( ! isCleanState() && ! api.state( 'changesetLocked' ).get() ) {
8917 + setTimeout( function() {
8918 + overlay.removeClass( 'customize-loading' );
8919 + }, 1 );
8920 + return api.l10n.saveAlert;
8921 + }
8922 + });
8923 + }
8924 + api.bind( 'change', startPromptingBeforeUnload );
8925 + api.state( 'selectedChangesetStatus' ).bind( startPromptingBeforeUnload );
8926 + api.state( 'selectedChangesetDate' ).bind( startPromptingBeforeUnload );
8927 +
8928 + function requestClose() {
8929 + var clearedToClose = $.Deferred(), dismissAutoSave = false, dismissLock = false;
8930 +
8931 + if ( isCleanState() ) {
8932 + dismissLock = true;
8933 + } else if ( confirm( api.l10n.saveAlert ) ) {
8934 +
8935 + dismissLock = true;
8936 +
8937 + // Mark all settings as clean to prevent another call to requestChangesetUpdate.
8938 + api.each( function( setting ) {
8939 + setting._dirty = false;
8940 + });
8941 + $( document ).off( 'visibilitychange.wp-customize-changeset-update' );
8942 + $( window ).off( 'beforeunload.wp-customize-changeset-update' );
8943 +
8944 + closeBtn.css( 'cursor', 'progress' );
8945 + if ( '' !== api.state( 'changesetStatus' ).get() ) {
8946 + dismissAutoSave = true;
8947 + }
8948 + } else {
8949 + clearedToClose.reject();
8950 + }
8951 +
8952 + if ( dismissLock || dismissAutoSave ) {
8953 + wp.ajax.send( 'customize_dismiss_autosave_or_lock', {
8954 + timeout: 500, // Don't wait too long.
8955 + data: {
8956 + wp_customize: 'on',
8957 + customize_theme: api.settings.theme.stylesheet,
8958 + customize_changeset_uuid: api.settings.changeset.uuid,
8959 + nonce: api.settings.nonce.dismiss_autosave_or_lock,
8960 + dismiss_autosave: dismissAutoSave,
8961 + dismiss_lock: dismissLock
8962 + }
8963 + } ).always( function() {
8964 + clearedToClose.resolve();
8965 + } );
8966 + }
8967 +
8968 + return clearedToClose.promise();
8969 + }
8970 +
8971 + parent.bind( 'confirm-close', function() {
8972 + requestClose().done( function() {
8973 + parent.send( 'confirmed-close', true );
8974 + } ).fail( function() {
8975 + parent.send( 'confirmed-close', false );
8976 + } );
8977 + } );
8978 +
8979 + closeBtn.on( 'click.customize-controls-close', function( event ) {
8980 + event.preventDefault();
8981 + if ( isInsideIframe ) {
8982 + parent.send( 'close' ); // See confirm-close logic above.
8983 + } else {
8984 + requestClose().done( function() {
8985 + $( window ).off( 'beforeunload.customize-confirm' );
8986 + window.location.href = closeBtn.prop( 'href' );
8987 + } );
8988 + }
8989 + });
8990 + })();
8991 +
8992 + // Pass events through to the parent.
8993 + $.each( [ 'saved', 'change' ], function ( i, event ) {
8994 + api.bind( event, function() {
8995 + parent.send( event );
8996 + });
8997 + } );
8998 +
8999 + // Pass titles to the parent.
9000 + api.bind( 'title', function( newTitle ) {
9001 + parent.send( 'title', newTitle );
9002 + });
9003 +
9004 + if ( api.settings.changeset.branching ) {
9005 + parent.send( 'changeset-uuid', api.settings.changeset.uuid );
9006 + }
9007 +
9008 + // Initialize the connection with the parent frame.
9009 + parent.send( 'ready' );
9010 +
9011 + // Control visibility for default controls.
9012 + $.each({
9013 + 'background_image': {
9014 + controls: [ 'background_preset', 'background_position', 'background_size', 'background_repeat', 'background_attachment' ],
9015 + callback: function( to ) { return !! to; }
9016 + },
9017 + 'show_on_front': {
9018 + controls: [ 'page_on_front', 'page_for_posts' ],
9019 + callback: function( to ) { return 'page' === to; }
9020 + },
9021 + 'header_textcolor': {
9022 + controls: [ 'header_textcolor' ],
9023 + callback: function( to ) { return 'blank' !== to; }
9024 + }
9025 + }, function( settingId, o ) {
9026 + api( settingId, function( setting ) {
9027 + $.each( o.controls, function( i, controlId ) {
9028 + api.control( controlId, function( control ) {
9029 + var visibility = function( to ) {
9030 + control.container.toggle( o.callback( to ) );
9031 + };
9032 +
9033 + visibility( setting.get() );
9034 + setting.bind( visibility );
9035 + });
9036 + });
9037 + });
9038 + });
9039 +
9040 + api.control( 'background_preset', function( control ) {
9041 + var visibility, defaultValues, values, toggleVisibility, updateSettings, preset;
9042 +
9043 + visibility = { // position, size, repeat, attachment.
9044 + 'default': [ false, false, false, false ],
9045 + 'fill': [ true, false, false, false ],
9046 + 'fit': [ true, false, true, false ],
9047 + 'repeat': [ true, false, false, true ],
9048 + 'custom': [ true, true, true, true ]
9049 + };
9050 +
9051 + defaultValues = [
9052 + _wpCustomizeBackground.defaults['default-position-x'],
9053 + _wpCustomizeBackground.defaults['default-position-y'],
9054 + _wpCustomizeBackground.defaults['default-size'],
9055 + _wpCustomizeBackground.defaults['default-repeat'],
9056 + _wpCustomizeBackground.defaults['default-attachment']
9057 + ];
9058 +
9059 + values = { // position_x, position_y, size, repeat, attachment.
9060 + 'default': defaultValues,
9061 + 'fill': [ 'left', 'top', 'cover', 'no-repeat', 'fixed' ],
9062 + 'fit': [ 'left', 'top', 'contain', 'no-repeat', 'fixed' ],
9063 + 'repeat': [ 'left', 'top', 'auto', 'repeat', 'scroll' ]
9064 + };
9065 +
9066 + // @todo These should actually toggle the active state,
9067 + // but without the preview overriding the state in data.activeControls.
9068 + toggleVisibility = function( preset ) {
9069 + _.each( [ 'background_position', 'background_size', 'background_repeat', 'background_attachment' ], function( controlId, i ) {
9070 + var control = api.control( controlId );
9071 + if ( control ) {
9072 + control.container.toggle( visibility[ preset ][ i ] );
9073 + }
9074 + } );
9075 + };
9076 +
9077 + updateSettings = function( preset ) {
9078 + _.each( [ 'background_position_x', 'background_position_y', 'background_size', 'background_repeat', 'background_attachment' ], function( settingId, i ) {
9079 + var setting = api( settingId );
9080 + if ( setting ) {
9081 + setting.set( values[ preset ][ i ] );
9082 + }
9083 + } );
9084 + };
9085 +
9086 + preset = control.setting.get();
9087 + toggleVisibility( preset );
9088 +
9089 + control.setting.bind( 'change', function( preset ) {
9090 + toggleVisibility( preset );
9091 + if ( 'custom' !== preset ) {
9092 + updateSettings( preset );
9093 + }
9094 + } );
9095 + } );
9096 +
9097 + api.control( 'background_repeat', function( control ) {
9098 + control.elements[0].unsync( api( 'background_repeat' ) );
9099 +
9100 + control.element = new api.Element( control.container.find( 'input' ) );
9101 + control.element.set( 'no-repeat' !== control.setting() );
9102 +
9103 + control.element.bind( function( to ) {
9104 + control.setting.set( to ? 'repeat' : 'no-repeat' );
9105 + } );
9106 +
9107 + control.setting.bind( function( to ) {
9108 + control.element.set( 'no-repeat' !== to );
9109 + } );
9110 + } );
9111 +
9112 + api.control( 'background_attachment', function( control ) {
9113 + control.elements[0].unsync( api( 'background_attachment' ) );
9114 +
9115 + control.element = new api.Element( control.container.find( 'input' ) );
9116 + control.element.set( 'fixed' !== control.setting() );
9117 +
9118 + control.element.bind( function( to ) {
9119 + control.setting.set( to ? 'scroll' : 'fixed' );
9120 + } );
9121 +
9122 + control.setting.bind( function( to ) {
9123 + control.element.set( 'fixed' !== to );
9124 + } );
9125 + } );
9126 +
9127 + // Juggle the two controls that use header_textcolor.
9128 + api.control( 'display_header_text', function( control ) {
9129 + var last = '';
9130 +
9131 + control.elements[0].unsync( api( 'header_textcolor' ) );
9132 +
9133 + control.element = new api.Element( control.container.find('input') );
9134 + control.element.set( 'blank' !== control.setting() );
9135 +
9136 + control.element.bind( function( to ) {
9137 + if ( ! to ) {
9138 + last = api( 'header_textcolor' ).get();
9139 + }
9140 +
9141 + control.setting.set( to ? last : 'blank' );
9142 + });
9143 +
9144 + control.setting.bind( function( to ) {
9145 + control.element.set( 'blank' !== to );
9146 + });
9147 + });
9148 +
9149 + // Add behaviors to the static front page controls.
9150 + api( 'show_on_front', 'page_on_front', 'page_for_posts', function( showOnFront, pageOnFront, pageForPosts ) {
9151 + var handleChange = function() {
9152 + var setting = this, pageOnFrontId, pageForPostsId, errorCode = 'show_on_front_page_collision';
9153 + pageOnFrontId = parseInt( pageOnFront(), 10 );
9154 + pageForPostsId = parseInt( pageForPosts(), 10 );
9155 +
9156 + if ( 'page' === showOnFront() ) {
9157 +
9158 + // Change previewed URL to the homepage when changing the page_on_front.
9159 + if ( setting === pageOnFront && pageOnFrontId > 0 ) {
9160 + api.previewer.previewUrl.set( api.settings.url.home );
9161 + }
9162 +
9163 + // Change the previewed URL to the selected page when changing the page_for_posts.
9164 + if ( setting === pageForPosts && pageForPostsId > 0 ) {
9165 + api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageForPostsId );
9166 + }
9167 + }
9168 +
9169 + // Toggle notification when the homepage and posts page are both set and the same.
9170 + if ( 'page' === showOnFront() && pageOnFrontId && pageForPostsId && pageOnFrontId === pageForPostsId ) {
9171 + showOnFront.notifications.add( new api.Notification( errorCode, {
9172 + type: 'error',
9173 + message: api.l10n.pageOnFrontError
9174 + } ) );
9175 + } else {
9176 + showOnFront.notifications.remove( errorCode );
9177 + }
9178 + };
9179 + showOnFront.bind( handleChange );
9180 + pageOnFront.bind( handleChange );
9181 + pageForPosts.bind( handleChange );
9182 + handleChange.call( showOnFront, showOnFront() ); // Make sure initial notification is added after loading existing changeset.
9183 +
9184 + // Move notifications container to the bottom.
9185 + api.control( 'show_on_front', function( showOnFrontControl ) {
9186 + showOnFrontControl.deferred.embedded.done( function() {
9187 + showOnFrontControl.container.append( showOnFrontControl.getNotificationsContainerElement() );
9188 + });
9189 + });
9190 + });
9191 +
9192 + // Add code editor for Custom CSS.
9193 + (function() {
9194 + var sectionReady = $.Deferred();
9195 +
9196 + api.section( 'custom_css', function( section ) {
9197 + section.deferred.embedded.done( function() {
9198 + if ( section.expanded() ) {
9199 + sectionReady.resolve( section );
9200 + } else {
9201 + section.expanded.bind( function( isExpanded ) {
9202 + if ( isExpanded ) {
9203 + sectionReady.resolve( section );
9204 + }
9205 + } );
9206 + }
9207 + });
9208 + });
9209 +
9210 + // Set up the section description behaviors.
9211 + sectionReady.done( function setupSectionDescription( section ) {
9212 + var control = api.control( 'custom_css' );
9213 +
9214 + // Hide redundant label for visual users.
9215 + control.container.find( '.customize-control-title:first' ).addClass( 'screen-reader-text' );
9216 +
9217 + // Close the section description when clicking the close button.
9218 + section.container.find( '.section-description-buttons .section-description-close' ).on( 'click', function() {
9219 + section.container.find( '.section-meta .customize-section-description:first' )
9220 + .removeClass( 'open' )
9221 + .slideUp();
9222 +
9223 + section.container.find( '.customize-help-toggle' )
9224 + .attr( 'aria-expanded', 'false' )
9225 + .focus(); // Avoid focus loss.
9226 + });
9227 +
9228 + // Reveal help text if setting is empty.
9229 + if ( control && ! control.setting.get() ) {
9230 + section.container.find( '.section-meta .customize-section-description:first' )
9231 + .addClass( 'open' )
9232 + .show()
9233 + .trigger( 'toggled' );
9234 +
9235 + section.container.find( '.customize-help-toggle' ).attr( 'aria-expanded', 'true' );
9236 + }
9237 + });
9238 + })();
9239 +
9240 + // Toggle visibility of Header Video notice when active state change.
9241 + api.control( 'header_video', function( headerVideoControl ) {
9242 + headerVideoControl.deferred.embedded.done( function() {
9243 + var toggleNotice = function() {
9244 + var section = api.section( headerVideoControl.section() ), noticeCode = 'video_header_not_available';
9245 + if ( ! section ) {
9246 + return;
9247 + }
9248 + if ( headerVideoControl.active.get() ) {
9249 + section.notifications.remove( noticeCode );
9250 + } else {
9251 + section.notifications.add( new api.Notification( noticeCode, {
9252 + type: 'info',
9253 + message: api.l10n.videoHeaderNotice
9254 + } ) );
9255 + }
9256 + };
9257 + toggleNotice();
9258 + headerVideoControl.active.bind( toggleNotice );
9259 + } );
9260 + } );
9261 +
9262 + // Update the setting validities.
9263 + api.previewer.bind( 'selective-refresh-setting-validities', function handleSelectiveRefreshedSettingValidities( settingValidities ) {
9264 + api._handleSettingValidities( {
9265 + settingValidities: settingValidities,
9266 + focusInvalidControl: false
9267 + } );
9268 + } );
9269 +
9270 + // Focus on the control that is associated with the given setting.
9271 + api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
9272 + var matchedControls = [];
9273 + api.control.each( function( control ) {
9274 + var settingIds = _.pluck( control.settings, 'id' );
9275 + if ( -1 !== _.indexOf( settingIds, settingId ) ) {
9276 + matchedControls.push( control );
9277 + }
9278 + } );
9279 +
9280 + // Focus on the matched control with the lowest priority (appearing higher).
9281 + if ( matchedControls.length ) {
9282 + matchedControls.sort( function( a, b ) {
9283 + return a.priority() - b.priority();
9284 + } );
9285 + matchedControls[0].focus();
9286 + }
9287 + } );
9288 +
9289 + // Refresh the preview when it requests.
9290 + api.previewer.bind( 'refresh', function() {
9291 + api.previewer.refresh();
9292 + });
9293 +
9294 + // Update the edit shortcut visibility state.
9295 + api.state( 'paneVisible' ).bind( function( isPaneVisible ) {
9296 + var isMobileScreen;
9297 + if ( window.matchMedia ) {
9298 + isMobileScreen = window.matchMedia( 'screen and ( max-width: 640px )' ).matches;
9299 + } else {
9300 + isMobileScreen = $( window ).width() <= 640;
9301 + }
9302 + api.state( 'editShortcutVisibility' ).set( isPaneVisible || isMobileScreen ? 'visible' : 'hidden' );
9303 + } );
9304 + if ( window.matchMedia ) {
9305 + window.matchMedia( 'screen and ( max-width: 640px )' ).addListener( function() {
9306 + var state = api.state( 'paneVisible' );
9307 + state.callbacks.fireWith( state, [ state.get(), state.get() ] );
9308 + } );
9309 + }
9310 + api.previewer.bind( 'edit-shortcut-visibility', function( visibility ) {
9311 + api.state( 'editShortcutVisibility' ).set( visibility );
9312 + } );
9313 + api.state( 'editShortcutVisibility' ).bind( function( visibility ) {
9314 + api.previewer.send( 'edit-shortcut-visibility', visibility );
9315 + } );
9316 +
9317 + // Autosave changeset.
9318 + function startAutosaving() {
9319 + var timeoutId, updateChangesetWithReschedule, scheduleChangesetUpdate, updatePending = false;
9320 +
9321 + api.unbind( 'change', startAutosaving ); // Ensure startAutosaving only fires once.
9322 +
9323 + function onChangeSaved( isSaved ) {
9324 + if ( ! isSaved && ! api.settings.changeset.autosaved ) {
9325 + api.settings.changeset.autosaved = true; // Once a change is made then autosaving kicks in.
9326 + api.previewer.send( 'autosaving' );
9327 + }
9328 + }
9329 + api.state( 'saved' ).bind( onChangeSaved );
9330 + onChangeSaved( api.state( 'saved' ).get() );
9331 +
9332 + /**
9333 + * Request changeset update and then re-schedule the next changeset update time.
9334 + *
9335 + * @since 4.7.0
9336 + * @private
9337 + */
9338 + updateChangesetWithReschedule = function() {
9339 + if ( ! updatePending ) {
9340 + updatePending = true;
9341 + api.requestChangesetUpdate( {}, { autosave: true } ).always( function() {
9342 + updatePending = false;
9343 + } );
9344 + }
9345 + scheduleChangesetUpdate();
9346 + };
9347 +
9348 + /**
9349 + * Schedule changeset update.
9350 + *
9351 + * @since 4.7.0
9352 + * @private
9353 + */
9354 + scheduleChangesetUpdate = function() {
9355 + clearTimeout( timeoutId );
9356 + timeoutId = setTimeout( function() {
9357 + updateChangesetWithReschedule();
9358 + }, api.settings.timeouts.changesetAutoSave );
9359 + };
9360 +
9361 + // Start auto-save interval for updating changeset.
9362 + scheduleChangesetUpdate();
9363 +
9364 + // Save changeset when focus removed from window.
9365 + $( document ).on( 'visibilitychange.wp-customize-changeset-update', function() {
9366 + if ( document.hidden ) {
9367 + updateChangesetWithReschedule();
9368 + }
9369 + } );
9370 +
9371 + // Save changeset before unloading window.
9372 + $( window ).on( 'beforeunload.wp-customize-changeset-update', function() {
9373 + updateChangesetWithReschedule();
9374 + } );
9375 + }
9376 + api.bind( 'change', startAutosaving );
9377 +
9378 + // Make sure TinyMCE dialogs appear above Customizer UI.
9379 + $( document ).one( 'tinymce-editor-setup', function() {
9380 + if ( window.tinymce.ui.FloatPanel && ( ! window.tinymce.ui.FloatPanel.zIndex || window.tinymce.ui.FloatPanel.zIndex < 500001 ) ) {
9381 + window.tinymce.ui.FloatPanel.zIndex = 500001;
9382 + }
9383 + } );
9384 +
9385 + body.addClass( 'ready' );
9386 + api.trigger( 'ready' );
9387 + });
9388 +
9389 + })( wp, jQuery );
9390 +