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