1 /* globals _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer */ |
1 /* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer, console, confirm */ |
2 (function( exports, $ ){ |
2 (function( exports, $ ){ |
3 var Container, focus, api = wp.customize; |
3 var Container, focus, normalizedTransitionendEventName, api = wp.customize; |
4 |
4 |
5 /** |
5 /** |
|
6 * A notification that is displayed in a full-screen overlay. |
|
7 * |
|
8 * @since 4.9.0 |
|
9 * @class |
|
10 * @augments wp.customize.Notification |
|
11 */ |
|
12 api.OverlayNotification = api.Notification.extend({ |
|
13 |
|
14 /** |
|
15 * Whether the notification should show a loading spinner. |
|
16 * |
|
17 * @since 4.9.0 |
|
18 * @var {boolean} |
|
19 */ |
|
20 loading: false, |
|
21 |
|
22 /** |
|
23 * Initialize. |
|
24 * |
|
25 * @since 4.9.0 |
|
26 * |
|
27 * @param {string} code - Code. |
|
28 * @param {object} params - Params. |
|
29 */ |
|
30 initialize: function( code, params ) { |
|
31 var notification = this; |
|
32 api.Notification.prototype.initialize.call( notification, code, params ); |
|
33 notification.containerClasses += ' notification-overlay'; |
|
34 if ( notification.loading ) { |
|
35 notification.containerClasses += ' notification-loading'; |
|
36 } |
|
37 }, |
|
38 |
|
39 /** |
|
40 * Render notification. |
|
41 * |
|
42 * @since 4.9.0 |
|
43 * |
|
44 * @return {jQuery} Notification container. |
|
45 */ |
|
46 render: function() { |
|
47 var li = api.Notification.prototype.render.call( this ); |
|
48 li.on( 'keydown', _.bind( this.handleEscape, this ) ); |
|
49 return li; |
|
50 }, |
|
51 |
|
52 /** |
|
53 * Stop propagation on escape key presses, but also dismiss notification if it is dismissible. |
|
54 * |
|
55 * @since 4.9.0 |
|
56 * |
|
57 * @param {jQuery.Event} event - Event. |
|
58 * @returns {void} |
|
59 */ |
|
60 handleEscape: function( event ) { |
|
61 var notification = this; |
|
62 if ( 27 === event.which ) { |
|
63 event.stopPropagation(); |
|
64 if ( notification.dismissible && notification.parent ) { |
|
65 notification.parent.remove( notification.code ); |
|
66 } |
|
67 } |
|
68 } |
|
69 }); |
|
70 |
|
71 /** |
|
72 * A collection of observable notifications. |
|
73 * |
|
74 * @since 4.9.0 |
|
75 * @class |
|
76 * @augments wp.customize.Values |
|
77 */ |
|
78 api.Notifications = api.Values.extend({ |
|
79 |
|
80 /** |
|
81 * Whether the alternative style should be used. |
|
82 * |
|
83 * @since 4.9.0 |
|
84 * @type {boolean} |
|
85 */ |
|
86 alt: false, |
|
87 |
|
88 /** |
|
89 * The default constructor for items of the collection. |
|
90 * |
|
91 * @since 4.9.0 |
|
92 * @type {object} |
|
93 */ |
|
94 defaultConstructor: api.Notification, |
|
95 |
|
96 /** |
|
97 * Initialize notifications area. |
|
98 * |
|
99 * @since 4.9.0 |
|
100 * @constructor |
|
101 * @param {object} options - Options. |
|
102 * @param {jQuery} [options.container] - Container element for notifications. This can be injected later. |
|
103 * @param {boolean} [options.alt] - Whether alternative style should be used when rendering notifications. |
|
104 * @returns {void} |
|
105 * @this {wp.customize.Notifications} |
|
106 */ |
|
107 initialize: function( options ) { |
|
108 var collection = this; |
|
109 |
|
110 api.Values.prototype.initialize.call( collection, options ); |
|
111 |
|
112 _.bindAll( collection, 'constrainFocus' ); |
|
113 |
|
114 // Keep track of the order in which the notifications were added for sorting purposes. |
|
115 collection._addedIncrement = 0; |
|
116 collection._addedOrder = {}; |
|
117 |
|
118 // Trigger change event when notification is added or removed. |
|
119 collection.bind( 'add', function( notification ) { |
|
120 collection.trigger( 'change', notification ); |
|
121 }); |
|
122 collection.bind( 'removed', function( notification ) { |
|
123 collection.trigger( 'change', notification ); |
|
124 }); |
|
125 }, |
|
126 |
|
127 /** |
|
128 * Get the number of notifications added. |
|
129 * |
|
130 * @since 4.9.0 |
|
131 * @return {number} Count of notifications. |
|
132 */ |
|
133 count: function() { |
|
134 return _.size( this._value ); |
|
135 }, |
|
136 |
|
137 /** |
|
138 * Add notification to the collection. |
|
139 * |
|
140 * @since 4.9.0 |
|
141 * |
|
142 * @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. |
|
143 * @param {wp.customize.Notification} [notificationObject] - Notification to add when first argument is the code string. |
|
144 * @returns {wp.customize.Notification} Added notification (or existing instance if it was already added). |
|
145 */ |
|
146 add: function( notification, notificationObject ) { |
|
147 var collection = this, code, instance; |
|
148 if ( 'string' === typeof notification ) { |
|
149 code = notification; |
|
150 instance = notificationObject; |
|
151 } else { |
|
152 code = notification.code; |
|
153 instance = notification; |
|
154 } |
|
155 if ( ! collection.has( code ) ) { |
|
156 collection._addedIncrement += 1; |
|
157 collection._addedOrder[ code ] = collection._addedIncrement; |
|
158 } |
|
159 return api.Values.prototype.add.call( collection, code, instance ); |
|
160 }, |
|
161 |
|
162 /** |
|
163 * Add notification to the collection. |
|
164 * |
|
165 * @since 4.9.0 |
|
166 * @param {string} code - Notification code to remove. |
|
167 * @return {api.Notification} Added instance (or existing instance if it was already added). |
|
168 */ |
|
169 remove: function( code ) { |
|
170 var collection = this; |
|
171 delete collection._addedOrder[ code ]; |
|
172 return api.Values.prototype.remove.call( this, code ); |
|
173 }, |
|
174 |
|
175 /** |
|
176 * Get list of notifications. |
|
177 * |
|
178 * Notifications may be sorted by type followed by added time. |
|
179 * |
|
180 * @since 4.9.0 |
|
181 * @param {object} args - Args. |
|
182 * @param {boolean} [args.sort=false] - Whether to return the notifications sorted. |
|
183 * @return {Array.<wp.customize.Notification>} Notifications. |
|
184 * @this {wp.customize.Notifications} |
|
185 */ |
|
186 get: function( args ) { |
|
187 var collection = this, notifications, errorTypePriorities, params; |
|
188 notifications = _.values( collection._value ); |
|
189 |
|
190 params = _.extend( |
|
191 { sort: false }, |
|
192 args |
|
193 ); |
|
194 |
|
195 if ( params.sort ) { |
|
196 errorTypePriorities = { error: 4, warning: 3, success: 2, info: 1 }; |
|
197 notifications.sort( function( a, b ) { |
|
198 var aPriority = 0, bPriority = 0; |
|
199 if ( ! _.isUndefined( errorTypePriorities[ a.type ] ) ) { |
|
200 aPriority = errorTypePriorities[ a.type ]; |
|
201 } |
|
202 if ( ! _.isUndefined( errorTypePriorities[ b.type ] ) ) { |
|
203 bPriority = errorTypePriorities[ b.type ]; |
|
204 } |
|
205 if ( aPriority !== bPriority ) { |
|
206 return bPriority - aPriority; // Show errors first. |
|
207 } |
|
208 return collection._addedOrder[ b.code ] - collection._addedOrder[ a.code ]; // Show newer notifications higher. |
|
209 }); |
|
210 } |
|
211 |
|
212 return notifications; |
|
213 }, |
|
214 |
|
215 /** |
|
216 * Render notifications area. |
|
217 * |
|
218 * @since 4.9.0 |
|
219 * @returns {void} |
|
220 * @this {wp.customize.Notifications} |
|
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 ).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 * @returns {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 /** |
|
344 * A Customizer Setting. |
|
345 * |
|
346 * A setting is WordPress data (theme mod, option, menu, etc.) that the user can |
|
347 * draft changes to in the Customizer. |
|
348 * |
|
349 * @see PHP class WP_Customize_Setting. |
|
350 * |
|
351 * @since 3.4.0 |
6 * @class |
352 * @class |
7 * @augments wp.customize.Value |
353 * @augments wp.customize.Value |
8 * @augments wp.customize.Class |
354 * @augments wp.customize.Class |
9 * |
|
10 * @param options |
|
11 * - previewer - The Previewer instance to sync with. |
|
12 * - transport - The transport to use for previewing. Supports 'refresh' and 'postMessage'. |
|
13 */ |
355 */ |
14 api.Setting = api.Value.extend({ |
356 api.Setting = api.Value.extend({ |
|
357 |
|
358 /** |
|
359 * Default params. |
|
360 * |
|
361 * @since 4.9.0 |
|
362 * @var {object} |
|
363 */ |
|
364 defaults: { |
|
365 transport: 'refresh', |
|
366 dirty: false |
|
367 }, |
|
368 |
|
369 /** |
|
370 * Initialize. |
|
371 * |
|
372 * @since 3.4.0 |
|
373 * |
|
374 * @param {string} id - The setting ID. |
|
375 * @param {*} value - The initial value of the setting. |
|
376 * @param {object} [options={}] - Options. |
|
377 * @param {string} [options.transport=refresh] - The transport to use for previewing. Supports 'refresh' and 'postMessage'. |
|
378 * @param {boolean} [options.dirty=false] - Whether the setting should be considered initially dirty. |
|
379 * @param {object} [options.previewer] - The Previewer instance to sync with. Defaults to wp.customize.previewer. |
|
380 */ |
15 initialize: function( id, value, options ) { |
381 initialize: function( id, value, options ) { |
16 api.Value.prototype.initialize.call( this, value, options ); |
382 var setting = this, params; |
17 |
383 params = _.extend( |
18 this.id = id; |
384 { previewer: api.previewer }, |
19 this.transport = this.transport || 'refresh'; |
385 setting.defaults, |
20 this._dirty = options.dirty || false; |
386 options || {} |
21 |
387 ); |
22 this.bind( this.preview ); |
388 |
23 }, |
389 api.Value.prototype.initialize.call( setting, value, params ); |
|
390 |
|
391 setting.id = id; |
|
392 setting._dirty = params.dirty; // The _dirty property is what the Customizer reads from. |
|
393 setting.notifications = new api.Notifications(); |
|
394 |
|
395 // Whenever the setting's value changes, refresh the preview. |
|
396 setting.bind( setting.preview ); |
|
397 }, |
|
398 |
|
399 /** |
|
400 * Refresh the preview, respective of the setting's refresh policy. |
|
401 * |
|
402 * If the preview hasn't sent a keep-alive message and is likely |
|
403 * disconnected by having navigated to a non-allowed URL, then the |
|
404 * refresh transport will be forced when postMessage is the transport. |
|
405 * Note that postMessage does not throw an error when the recipient window |
|
406 * fails to match the origin window, so using try/catch around the |
|
407 * previewer.send() call to then fallback to refresh will not work. |
|
408 * |
|
409 * @since 3.4.0 |
|
410 * @access public |
|
411 * |
|
412 * @returns {void} |
|
413 */ |
24 preview: function() { |
414 preview: function() { |
25 switch ( this.transport ) { |
415 var setting = this, transport; |
26 case 'refresh': |
416 transport = setting.transport; |
27 return this.previewer.refresh(); |
417 |
28 case 'postMessage': |
418 if ( 'postMessage' === transport && ! api.state( 'previewerAlive' ).get() ) { |
29 return this.previewer.send( 'setting', [ this.id, this() ] ); |
419 transport = 'refresh'; |
30 } |
420 } |
|
421 |
|
422 if ( 'postMessage' === transport ) { |
|
423 setting.previewer.send( 'setting', [ setting.id, setting() ] ); |
|
424 } else if ( 'refresh' === transport ) { |
|
425 setting.previewer.refresh(); |
|
426 } |
|
427 }, |
|
428 |
|
429 /** |
|
430 * Find controls associated with this setting. |
|
431 * |
|
432 * @since 4.6.0 |
|
433 * @returns {wp.customize.Control[]} Controls associated with setting. |
|
434 */ |
|
435 findControls: function() { |
|
436 var setting = this, controls = []; |
|
437 api.control.each( function( control ) { |
|
438 _.each( control.settings, function( controlSetting ) { |
|
439 if ( controlSetting.id === setting.id ) { |
|
440 controls.push( control ); |
|
441 } |
|
442 } ); |
|
443 } ); |
|
444 return controls; |
31 } |
445 } |
32 }); |
446 }); |
33 |
447 |
34 /** |
448 /** |
35 * Utility function namespace |
449 * Current change count. |
|
450 * |
|
451 * @since 4.7.0 |
|
452 * @type {number} |
|
453 * @protected |
36 */ |
454 */ |
37 api.utils = {}; |
455 api._latestRevision = 0; |
|
456 |
|
457 /** |
|
458 * Last revision that was saved. |
|
459 * |
|
460 * @since 4.7.0 |
|
461 * @type {number} |
|
462 * @protected |
|
463 */ |
|
464 api._lastSavedRevision = 0; |
|
465 |
|
466 /** |
|
467 * Latest revisions associated with the updated setting. |
|
468 * |
|
469 * @since 4.7.0 |
|
470 * @type {object} |
|
471 * @protected |
|
472 */ |
|
473 api._latestSettingRevisions = {}; |
|
474 |
|
475 /* |
|
476 * Keep track of the revision associated with each updated setting so that |
|
477 * requestChangesetUpdate knows which dirty settings to include. Also, once |
|
478 * ready is triggered and all initial settings have been added, increment |
|
479 * revision for each newly-created initially-dirty setting so that it will |
|
480 * also be included in changeset update requests. |
|
481 */ |
|
482 api.bind( 'change', function incrementChangedSettingRevision( setting ) { |
|
483 api._latestRevision += 1; |
|
484 api._latestSettingRevisions[ setting.id ] = api._latestRevision; |
|
485 } ); |
|
486 api.bind( 'ready', function() { |
|
487 api.bind( 'add', function incrementCreatedSettingRevision( setting ) { |
|
488 if ( setting._dirty ) { |
|
489 api._latestRevision += 1; |
|
490 api._latestSettingRevisions[ setting.id ] = api._latestRevision; |
|
491 } |
|
492 } ); |
|
493 } ); |
|
494 |
|
495 /** |
|
496 * Get the dirty setting values. |
|
497 * |
|
498 * @since 4.7.0 |
|
499 * @access public |
|
500 * |
|
501 * @param {object} [options] Options. |
|
502 * @param {boolean} [options.unsaved=false] Whether only values not saved yet into a changeset will be returned (differential changes). |
|
503 * @returns {object} Dirty setting values. |
|
504 */ |
|
505 api.dirtyValues = function dirtyValues( options ) { |
|
506 var values = {}; |
|
507 api.each( function( setting ) { |
|
508 var settingRevision; |
|
509 |
|
510 if ( ! setting._dirty ) { |
|
511 return; |
|
512 } |
|
513 |
|
514 settingRevision = api._latestSettingRevisions[ setting.id ]; |
|
515 |
|
516 // Skip including settings that have already been included in the changeset, if only requesting unsaved. |
|
517 if ( api.state( 'changesetStatus' ).get() && ( options && options.unsaved ) && ( _.isUndefined( settingRevision ) || settingRevision <= api._lastSavedRevision ) ) { |
|
518 return; |
|
519 } |
|
520 |
|
521 values[ setting.id ] = setting.get(); |
|
522 } ); |
|
523 return values; |
|
524 }; |
|
525 |
|
526 /** |
|
527 * Request updates to the changeset. |
|
528 * |
|
529 * @since 4.7.0 |
|
530 * @access public |
|
531 * |
|
532 * @param {object} [changes] - Mapping of setting IDs to setting params each normally including a value property, or mapping to null. |
|
533 * If not provided, then the changes will still be obtained from unsaved dirty settings. |
|
534 * @param {object} [args] - Additional options for the save request. |
|
535 * @param {boolean} [args.autosave=false] - Whether changes will be stored in autosave revision if the changeset has been promoted from an auto-draft. |
|
536 * @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. |
|
537 * @param {string} [args.title] - Title to update in the changeset. Optional. |
|
538 * @param {string} [args.date] - Date to update in the changeset. Optional. |
|
539 * @returns {jQuery.Promise} Promise resolving with the response data. |
|
540 */ |
|
541 api.requestChangesetUpdate = function requestChangesetUpdate( changes, args ) { |
|
542 var deferred, request, submittedChanges = {}, data, submittedArgs; |
|
543 deferred = new $.Deferred(); |
|
544 |
|
545 // Prevent attempting changeset update while request is being made. |
|
546 if ( 0 !== api.state( 'processing' ).get() ) { |
|
547 deferred.reject( 'already_processing' ); |
|
548 return deferred.promise(); |
|
549 } |
|
550 |
|
551 submittedArgs = _.extend( { |
|
552 title: null, |
|
553 date: null, |
|
554 autosave: false, |
|
555 force: false |
|
556 }, args ); |
|
557 |
|
558 if ( changes ) { |
|
559 _.extend( submittedChanges, changes ); |
|
560 } |
|
561 |
|
562 // Ensure all revised settings (changes pending save) are also included, but not if marked for deletion in changes. |
|
563 _.each( api.dirtyValues( { unsaved: true } ), function( dirtyValue, settingId ) { |
|
564 if ( ! changes || null !== changes[ settingId ] ) { |
|
565 submittedChanges[ settingId ] = _.extend( |
|
566 {}, |
|
567 submittedChanges[ settingId ] || {}, |
|
568 { value: dirtyValue } |
|
569 ); |
|
570 } |
|
571 } ); |
|
572 |
|
573 // Allow plugins to attach additional params to the settings. |
|
574 api.trigger( 'changeset-save', submittedChanges, submittedArgs ); |
|
575 |
|
576 // Short-circuit when there are no pending changes. |
|
577 if ( ! submittedArgs.force && _.isEmpty( submittedChanges ) && null === submittedArgs.title && null === submittedArgs.date ) { |
|
578 deferred.resolve( {} ); |
|
579 return deferred.promise(); |
|
580 } |
|
581 |
|
582 // A status would cause a revision to be made, and for this wp.customize.previewer.save() should be used. Status is also disallowed for revisions regardless. |
|
583 if ( submittedArgs.status ) { |
|
584 return deferred.reject( { code: 'illegal_status_in_changeset_update' } ).promise(); |
|
585 } |
|
586 |
|
587 // Dates not beung allowed for revisions are is a technical limitation of post revisions. |
|
588 if ( submittedArgs.date && submittedArgs.autosave ) { |
|
589 return deferred.reject( { code: 'illegal_autosave_with_date_gmt' } ).promise(); |
|
590 } |
|
591 |
|
592 // Make sure that publishing a changeset waits for all changeset update requests to complete. |
|
593 api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 ); |
|
594 deferred.always( function() { |
|
595 api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 ); |
|
596 } ); |
|
597 |
|
598 // Ensure that if any plugins add data to save requests by extending query() that they get included here. |
|
599 data = api.previewer.query( { excludeCustomizedSaved: true } ); |
|
600 delete data.customized; // Being sent in customize_changeset_data instead. |
|
601 _.extend( data, { |
|
602 nonce: api.settings.nonce.save, |
|
603 customize_theme: api.settings.theme.stylesheet, |
|
604 customize_changeset_data: JSON.stringify( submittedChanges ) |
|
605 } ); |
|
606 if ( null !== submittedArgs.title ) { |
|
607 data.customize_changeset_title = submittedArgs.title; |
|
608 } |
|
609 if ( null !== submittedArgs.date ) { |
|
610 data.customize_changeset_date = submittedArgs.date; |
|
611 } |
|
612 if ( false !== submittedArgs.autosave ) { |
|
613 data.customize_changeset_autosave = 'true'; |
|
614 } |
|
615 |
|
616 // Allow plugins to modify the params included with the save request. |
|
617 api.trigger( 'save-request-params', data ); |
|
618 |
|
619 request = wp.ajax.post( 'customize_save', data ); |
|
620 |
|
621 request.done( function requestChangesetUpdateDone( data ) { |
|
622 var savedChangesetValues = {}; |
|
623 |
|
624 // Ensure that all settings updated subsequently will be included in the next changeset update request. |
|
625 api._lastSavedRevision = Math.max( api._latestRevision, api._lastSavedRevision ); |
|
626 |
|
627 api.state( 'changesetStatus' ).set( data.changeset_status ); |
|
628 |
|
629 if ( data.changeset_date ) { |
|
630 api.state( 'changesetDate' ).set( data.changeset_date ); |
|
631 } |
|
632 |
|
633 deferred.resolve( data ); |
|
634 api.trigger( 'changeset-saved', data ); |
|
635 |
|
636 if ( data.setting_validities ) { |
|
637 _.each( data.setting_validities, function( validity, settingId ) { |
|
638 if ( true === validity && _.isObject( submittedChanges[ settingId ] ) && ! _.isUndefined( submittedChanges[ settingId ].value ) ) { |
|
639 savedChangesetValues[ settingId ] = submittedChanges[ settingId ].value; |
|
640 } |
|
641 } ); |
|
642 } |
|
643 |
|
644 api.previewer.send( 'changeset-saved', _.extend( {}, data, { saved_changeset_values: savedChangesetValues } ) ); |
|
645 } ); |
|
646 request.fail( function requestChangesetUpdateFail( data ) { |
|
647 deferred.reject( data ); |
|
648 api.trigger( 'changeset-error', data ); |
|
649 } ); |
|
650 request.always( function( data ) { |
|
651 if ( data.setting_validities ) { |
|
652 api._handleSettingValidities( { |
|
653 settingValidities: data.setting_validities |
|
654 } ); |
|
655 } |
|
656 } ); |
|
657 |
|
658 return deferred.promise(); |
|
659 }; |
38 |
660 |
39 /** |
661 /** |
40 * Watch all changes to Value properties, and bubble changes to parent Values instance |
662 * Watch all changes to Value properties, and bubble changes to parent Values instance |
41 * |
663 * |
42 * @since 4.1.0 |
664 * @since 4.1.0 |
314 onChangeExpanded: function () { |
1179 onChangeExpanded: function () { |
315 throw new Error( 'Must override with subclass.' ); |
1180 throw new Error( 'Must override with subclass.' ); |
316 }, |
1181 }, |
317 |
1182 |
318 /** |
1183 /** |
319 * @param {Boolean} expanded |
1184 * Handle the toggle logic for expand/collapse. |
320 * @param {Object} [params] |
1185 * |
321 * @returns {Boolean} false if state already applied |
1186 * @param {Boolean} expanded - The new state to apply. |
322 */ |
1187 * @param {Object} [params] - Object containing options for expand/collapse. |
323 _toggleExpanded: function ( expanded, params ) { |
1188 * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete. |
324 var self = this; |
1189 * @returns {Boolean} false if state already applied or active state is false |
|
1190 */ |
|
1191 _toggleExpanded: function( expanded, params ) { |
|
1192 var instance = this, previousCompleteCallback; |
325 params = params || {}; |
1193 params = params || {}; |
326 var section = this, previousCompleteCallback = params.completeCallback; |
1194 previousCompleteCallback = params.completeCallback; |
327 params.completeCallback = function () { |
1195 |
|
1196 // Short-circuit expand() if the instance is not active. |
|
1197 if ( expanded && ! instance.active() ) { |
|
1198 return false; |
|
1199 } |
|
1200 |
|
1201 api.state( 'paneVisible' ).set( true ); |
|
1202 params.completeCallback = function() { |
328 if ( previousCompleteCallback ) { |
1203 if ( previousCompleteCallback ) { |
329 previousCompleteCallback.apply( section, arguments ); |
1204 previousCompleteCallback.apply( instance, arguments ); |
330 } |
1205 } |
331 if ( expanded ) { |
1206 if ( expanded ) { |
332 section.container.trigger( 'expanded' ); |
1207 instance.container.trigger( 'expanded' ); |
333 } else { |
1208 } else { |
334 section.container.trigger( 'collapsed' ); |
1209 instance.container.trigger( 'collapsed' ); |
335 } |
1210 } |
336 }; |
1211 }; |
337 if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) { |
1212 if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) { |
338 params.unchanged = true; |
1213 params.unchanged = true; |
339 self.onChangeExpanded( self.expanded.get(), params ); |
1214 instance.onChangeExpanded( instance.expanded.get(), params ); |
340 return false; |
1215 return false; |
341 } else { |
1216 } else { |
342 params.unchanged = false; |
1217 params.unchanged = false; |
343 this.expandedArgumentsQueue.push( params ); |
1218 instance.expandedArgumentsQueue.push( params ); |
344 this.expanded.set( expanded ); |
1219 instance.expanded.set( expanded ); |
345 return true; |
1220 return true; |
346 } |
1221 } |
347 }, |
1222 }, |
348 |
1223 |
349 /** |
1224 /** |
350 * @param {Object} [params] |
1225 * @param {Object} [params] |
351 * @returns {Boolean} false if already expanded |
1226 * @returns {Boolean} false if already expanded or if inactive. |
352 */ |
1227 */ |
353 expand: function ( params ) { |
1228 expand: function ( params ) { |
354 return this._toggleExpanded( true, params ); |
1229 return this._toggleExpanded( true, params ); |
355 }, |
1230 }, |
356 |
1231 |
357 /** |
1232 /** |
358 * @param {Object} [params] |
1233 * @param {Object} [params] |
359 * @returns {Boolean} false if already collapsed |
1234 * @returns {Boolean} false if already collapsed. |
360 */ |
1235 */ |
361 collapse: function ( params ) { |
1236 collapse: function ( params ) { |
362 return this._toggleExpanded( false, params ); |
1237 return this._toggleExpanded( false, params ); |
363 }, |
1238 }, |
364 |
1239 |
365 /** |
1240 /** |
|
1241 * Animate container state change if transitions are supported by the browser. |
|
1242 * |
|
1243 * @since 4.7.0 |
|
1244 * @private |
|
1245 * |
|
1246 * @param {function} completeCallback Function to be called after transition is completed. |
|
1247 * @returns {void} |
|
1248 */ |
|
1249 _animateChangeExpanded: function( completeCallback ) { |
|
1250 // Return if CSS transitions are not supported. |
|
1251 if ( ! normalizedTransitionendEventName ) { |
|
1252 if ( completeCallback ) { |
|
1253 completeCallback(); |
|
1254 } |
|
1255 return; |
|
1256 } |
|
1257 |
|
1258 var construct = this, |
|
1259 content = construct.contentContainer, |
|
1260 overlay = content.closest( '.wp-full-overlay' ), |
|
1261 elements, transitionEndCallback, transitionParentPane; |
|
1262 |
|
1263 // Determine set of elements that are affected by the animation. |
|
1264 elements = overlay.add( content ); |
|
1265 |
|
1266 if ( ! construct.panel || '' === construct.panel() ) { |
|
1267 transitionParentPane = true; |
|
1268 } else if ( api.panel( construct.panel() ).contentContainer.hasClass( 'skip-transition' ) ) { |
|
1269 transitionParentPane = true; |
|
1270 } else { |
|
1271 transitionParentPane = false; |
|
1272 } |
|
1273 if ( transitionParentPane ) { |
|
1274 elements = elements.add( '#customize-info, .customize-pane-parent' ); |
|
1275 } |
|
1276 |
|
1277 // Handle `transitionEnd` event. |
|
1278 transitionEndCallback = function( e ) { |
|
1279 if ( 2 !== e.eventPhase || ! $( e.target ).is( content ) ) { |
|
1280 return; |
|
1281 } |
|
1282 content.off( normalizedTransitionendEventName, transitionEndCallback ); |
|
1283 elements.removeClass( 'busy' ); |
|
1284 if ( completeCallback ) { |
|
1285 completeCallback(); |
|
1286 } |
|
1287 }; |
|
1288 content.on( normalizedTransitionendEventName, transitionEndCallback ); |
|
1289 elements.addClass( 'busy' ); |
|
1290 |
|
1291 // Prevent screen flicker when pane has been scrolled before expanding. |
|
1292 _.defer( function() { |
|
1293 var container = content.closest( '.wp-full-overlay-sidebar-content' ), |
|
1294 currentScrollTop = container.scrollTop(), |
|
1295 previousScrollTop = content.data( 'previous-scrollTop' ) || 0, |
|
1296 expanded = construct.expanded(); |
|
1297 |
|
1298 if ( expanded && 0 < currentScrollTop ) { |
|
1299 content.css( 'top', currentScrollTop + 'px' ); |
|
1300 content.data( 'previous-scrollTop', currentScrollTop ); |
|
1301 } else if ( ! expanded && 0 < currentScrollTop + previousScrollTop ) { |
|
1302 content.css( 'top', previousScrollTop - currentScrollTop + 'px' ); |
|
1303 container.scrollTop( previousScrollTop ); |
|
1304 } |
|
1305 } ); |
|
1306 }, |
|
1307 |
|
1308 /** |
366 * Bring the container into view and then expand this and bring it into view |
1309 * Bring the container into view and then expand this and bring it into view |
367 * @param {Object} [params] |
1310 * @param {Object} [params] |
368 */ |
1311 */ |
369 focus: focus |
1312 focus: focus, |
|
1313 |
|
1314 /** |
|
1315 * Return the container html, generated from its JS template, if it exists. |
|
1316 * |
|
1317 * @since 4.3.0 |
|
1318 */ |
|
1319 getContainer: function () { |
|
1320 var template, |
|
1321 container = this; |
|
1322 |
|
1323 if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) { |
|
1324 template = wp.template( container.templateSelector ); |
|
1325 } else { |
|
1326 template = wp.template( 'customize-' + container.containerType + '-default' ); |
|
1327 } |
|
1328 if ( template && container.container ) { |
|
1329 return $.trim( template( _.extend( |
|
1330 { id: container.id }, |
|
1331 container.params |
|
1332 ) ) ); |
|
1333 } |
|
1334 |
|
1335 return '<li></li>'; |
|
1336 }, |
|
1337 |
|
1338 /** |
|
1339 * Find content element which is displayed when the section is expanded. |
|
1340 * |
|
1341 * After a construct is initialized, the return value will be available via the `contentContainer` property. |
|
1342 * By default the element will be related it to the parent container with `aria-owns` and detached. |
|
1343 * Custom panels and sections (such as the `NewMenuSection`) that do not have a sliding pane should |
|
1344 * just return the content element without needing to add the `aria-owns` element or detach it from |
|
1345 * the container. Such non-sliding pane custom sections also need to override the `onChangeExpanded` |
|
1346 * method to handle animating the panel/section into and out of view. |
|
1347 * |
|
1348 * @since 4.7.0 |
|
1349 * @access public |
|
1350 * |
|
1351 * @returns {jQuery} Detached content element. |
|
1352 */ |
|
1353 getContent: function() { |
|
1354 var construct = this, |
|
1355 container = construct.container, |
|
1356 content = container.find( '.accordion-section-content, .control-panel-content' ).first(), |
|
1357 contentId = 'sub-' + container.attr( 'id' ), |
|
1358 ownedElements = contentId, |
|
1359 alreadyOwnedElements = container.attr( 'aria-owns' ); |
|
1360 |
|
1361 if ( alreadyOwnedElements ) { |
|
1362 ownedElements = ownedElements + ' ' + alreadyOwnedElements; |
|
1363 } |
|
1364 container.attr( 'aria-owns', ownedElements ); |
|
1365 |
|
1366 return content.detach().attr( { |
|
1367 'id': contentId, |
|
1368 'class': 'customize-pane-child ' + content.attr( 'class' ) + ' ' + container.attr( 'class' ) |
|
1369 } ); |
|
1370 } |
370 }); |
1371 }); |
371 |
1372 |
372 /** |
1373 /** |
373 * @since 4.1.0 |
1374 * @since 4.1.0 |
374 * |
1375 * |
375 * @class |
1376 * @class |
376 * @augments wp.customize.Class |
1377 * @augments wp.customize.Class |
377 */ |
1378 */ |
378 api.Section = Container.extend({ |
1379 api.Section = Container.extend({ |
|
1380 containerType: 'section', |
|
1381 containerParent: '#customize-theme-controls', |
|
1382 containerPaneParent: '.customize-pane-parent', |
|
1383 defaults: { |
|
1384 title: '', |
|
1385 description: '', |
|
1386 priority: 100, |
|
1387 type: 'default', |
|
1388 content: null, |
|
1389 active: true, |
|
1390 instanceNumber: null, |
|
1391 panel: null, |
|
1392 customizeAction: '' |
|
1393 }, |
379 |
1394 |
380 /** |
1395 /** |
381 * @since 4.1.0 |
1396 * @since 4.1.0 |
382 * |
1397 * |
383 * @param {String} id |
1398 * @param {string} id - The ID for the section. |
384 * @param {Array} options |
1399 * @param {object} options - Options. |
|
1400 * @param {string} options.title - Title shown when section is collapsed and expanded. |
|
1401 * @param {string=} [options.description] - Description shown at the top of the section. |
|
1402 * @param {number=100} [options.priority] - The sort priority for the section. |
|
1403 * @param {string=default} [options.type] - The type of the section. See wp.customize.sectionConstructor. |
|
1404 * @param {string=} [options.content] - The markup to be used for the section container. If empty, a JS template is used. |
|
1405 * @param {boolean=true} [options.active] - Whether the section is active or not. |
|
1406 * @param {string} options.panel - The ID for the panel this section is associated with. |
|
1407 * @param {string=} [options.customizeAction] - Additional context information shown before the section title when expanded. |
|
1408 * @param {object} [options.params] - Deprecated wrapper for the above properties. |
385 */ |
1409 */ |
386 initialize: function ( id, options ) { |
1410 initialize: function ( id, options ) { |
387 var section = this; |
1411 var section = this, params; |
388 Container.prototype.initialize.call( section, id, options ); |
1412 params = options.params || options; |
|
1413 |
|
1414 // Look up the type if one was not supplied. |
|
1415 if ( ! params.type ) { |
|
1416 _.find( api.sectionConstructor, function( Constructor, type ) { |
|
1417 if ( Constructor === section.constructor ) { |
|
1418 params.type = type; |
|
1419 return true; |
|
1420 } |
|
1421 return false; |
|
1422 } ); |
|
1423 } |
|
1424 |
|
1425 Container.prototype.initialize.call( section, id, params ); |
389 |
1426 |
390 section.id = id; |
1427 section.id = id; |
391 section.panel = new api.Value(); |
1428 section.panel = new api.Value(); |
392 section.panel.bind( function ( id ) { |
1429 section.panel.bind( function ( id ) { |
393 $( section.container ).toggleClass( 'control-subsection', !! id ); |
1430 $( section.headContainer ).toggleClass( 'control-subsection', !! id ); |
394 }); |
1431 }); |
395 section.panel.set( section.params.panel || '' ); |
1432 section.panel.set( section.params.panel || '' ); |
396 api.utils.bubbleChildValueChanges( section, [ 'panel' ] ); |
1433 api.utils.bubbleChildValueChanges( section, [ 'panel' ] ); |
397 |
1434 |
398 section.embed(); |
1435 section.embed(); |
697 * @since 4.2.0 |
1943 * @since 4.2.0 |
698 * |
1944 * |
699 * @param {Boolean} expanded |
1945 * @param {Boolean} expanded |
700 * @param {Object} args |
1946 * @param {Object} args |
701 * @param {Boolean} args.unchanged |
1947 * @param {Boolean} args.unchanged |
702 * @param {Callback} args.completeCallback |
1948 * @param {Function} args.completeCallback |
|
1949 * @returns {void} |
703 */ |
1950 */ |
704 onChangeExpanded: function ( expanded, args ) { |
1951 onChangeExpanded: function ( expanded, args ) { |
|
1952 |
|
1953 // Note: there is a second argument 'args' passed |
|
1954 var section = this, |
|
1955 container = section.contentContainer.closest( '.customize-themes-full-container' ); |
705 |
1956 |
706 // Immediately call the complete callback if there were no changes |
1957 // Immediately call the complete callback if there were no changes |
707 if ( args.unchanged ) { |
1958 if ( args.unchanged ) { |
708 if ( args.completeCallback ) { |
1959 if ( args.completeCallback ) { |
709 args.completeCallback(); |
1960 args.completeCallback(); |
710 } |
1961 } |
711 return; |
1962 return; |
712 } |
1963 } |
713 |
1964 |
714 // Note: there is a second argument 'args' passed |
1965 function expand() { |
715 var position, scroll, |
1966 |
716 panel = this, |
1967 // Try to load controls if none are loaded yet. |
717 section = panel.container.closest( '.accordion-section' ), |
1968 if ( 0 === section.loaded ) { |
718 overlay = section.closest( '.wp-full-overlay' ), |
1969 section.loadThemes(); |
719 container = section.closest( '.wp-full-overlay-sidebar-content' ), |
1970 } |
720 siblings = container.find( '.open' ), |
|
721 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ), |
|
722 customizeBtn = section.find( '.customize-theme' ), |
|
723 changeBtn = section.find( '.change-theme' ), |
|
724 content = section.find( '.control-panel-content' ); |
|
725 |
|
726 if ( expanded ) { |
|
727 |
1971 |
728 // Collapse any sibling sections/panels |
1972 // Collapse any sibling sections/panels |
729 api.section.each( function ( otherSection ) { |
1973 api.section.each( function ( otherSection ) { |
730 if ( otherSection !== panel ) { |
1974 var searchTerm; |
731 otherSection.collapse( { duration: args.duration } ); |
1975 |
|
1976 if ( otherSection !== section ) { |
|
1977 |
|
1978 // Try to sync the current search term to the new section. |
|
1979 if ( 'themes' === otherSection.params.type ) { |
|
1980 searchTerm = otherSection.contentContainer.find( '.wp-filter-search' ).val(); |
|
1981 section.contentContainer.find( '.wp-filter-search' ).val( searchTerm ); |
|
1982 |
|
1983 // Directly initialize an empty remote search to avoid a race condition. |
|
1984 if ( '' === searchTerm && '' !== section.term && 'local' !== section.params.filter_type ) { |
|
1985 section.term = ''; |
|
1986 section.initializeNewQuery( section.term, section.tags ); |
|
1987 } else { |
|
1988 if ( 'remote' === section.params.filter_type ) { |
|
1989 section.checkTerm( section ); |
|
1990 } else if ( 'local' === section.params.filter_type ) { |
|
1991 section.filterSearch( searchTerm ); |
|
1992 } |
|
1993 } |
|
1994 otherSection.collapse( { duration: args.duration } ); |
|
1995 } |
732 } |
1996 } |
733 }); |
1997 }); |
734 api.panel.each( function ( otherPanel ) { |
1998 |
735 otherPanel.collapse( { duration: 0 } ); |
1999 section.contentContainer.addClass( 'current-section' ); |
|
2000 container.scrollTop(); |
|
2001 |
|
2002 container.on( 'scroll', _.throttle( section.renderScreenshots, 300 ) ); |
|
2003 container.on( 'scroll', _.throttle( section.loadMore, 300 ) ); |
|
2004 |
|
2005 if ( args.completeCallback ) { |
|
2006 args.completeCallback(); |
|
2007 } |
|
2008 section.updateCount(); // Show this section's count. |
|
2009 } |
|
2010 |
|
2011 if ( expanded ) { |
|
2012 if ( section.panel() && api.panel.has( section.panel() ) ) { |
|
2013 api.panel( section.panel() ).expand({ |
|
2014 duration: args.duration, |
|
2015 completeCallback: expand |
|
2016 }); |
|
2017 } else { |
|
2018 expand(); |
|
2019 } |
|
2020 } else { |
|
2021 section.contentContainer.removeClass( 'current-section' ); |
|
2022 |
|
2023 // Always hide, even if they don't exist or are already hidden. |
|
2024 section.headerContainer.find( '.filter-details' ).slideUp( 180 ); |
|
2025 |
|
2026 container.off( 'scroll' ); |
|
2027 |
|
2028 if ( args.completeCallback ) { |
|
2029 args.completeCallback(); |
|
2030 } |
|
2031 } |
|
2032 }, |
|
2033 |
|
2034 /** |
|
2035 * Return the section's content element without detaching from the parent. |
|
2036 * |
|
2037 * @since 4.9.0 |
|
2038 * |
|
2039 * @returns {jQuery} |
|
2040 */ |
|
2041 getContent: function() { |
|
2042 return this.container.find( '.control-section-content' ); |
|
2043 }, |
|
2044 |
|
2045 /** |
|
2046 * Load theme data via Ajax and add themes to the section as controls. |
|
2047 * |
|
2048 * @since 4.9.0 |
|
2049 * |
|
2050 * @returns {void} |
|
2051 */ |
|
2052 loadThemes: function() { |
|
2053 var section = this, params, page, request; |
|
2054 |
|
2055 if ( section.loading ) { |
|
2056 return; // We're already loading a batch of themes. |
|
2057 } |
|
2058 |
|
2059 // Parameters for every API query. Additional params are set in PHP. |
|
2060 page = Math.ceil( section.loaded / 100 ) + 1; |
|
2061 params = { |
|
2062 'nonce': api.settings.nonce.switch_themes, |
|
2063 'wp_customize': 'on', |
|
2064 'theme_action': section.params.action, |
|
2065 'customized_theme': api.settings.theme.stylesheet, |
|
2066 'page': page |
|
2067 }; |
|
2068 |
|
2069 // Add fields for remote filtering. |
|
2070 if ( 'remote' === section.params.filter_type ) { |
|
2071 params.search = section.term; |
|
2072 params.tags = section.tags; |
|
2073 } |
|
2074 |
|
2075 // Load themes. |
|
2076 section.headContainer.closest( '.wp-full-overlay' ).addClass( 'loading' ); |
|
2077 section.loading = true; |
|
2078 section.container.find( '.no-themes' ).hide(); |
|
2079 request = wp.ajax.post( 'customize_load_themes', params ); |
|
2080 request.done(function( data ) { |
|
2081 var themes = data.themes; |
|
2082 |
|
2083 // Stop and try again if the term changed while loading. |
|
2084 if ( '' !== section.nextTerm || '' !== section.nextTags ) { |
|
2085 if ( section.nextTerm ) { |
|
2086 section.term = section.nextTerm; |
|
2087 } |
|
2088 if ( section.nextTags ) { |
|
2089 section.tags = section.nextTags; |
|
2090 } |
|
2091 section.nextTerm = ''; |
|
2092 section.nextTags = ''; |
|
2093 section.loading = false; |
|
2094 section.loadThemes(); |
|
2095 return; |
|
2096 } |
|
2097 |
|
2098 if ( 0 !== themes.length ) { |
|
2099 |
|
2100 section.loadControls( themes, page ); |
|
2101 |
|
2102 if ( 1 === page ) { |
|
2103 |
|
2104 // Pre-load the first 3 theme screenshots. |
|
2105 _.each( section.controls().slice( 0, 3 ), function( control ) { |
|
2106 var img, src = control.params.theme.screenshot[0]; |
|
2107 if ( src ) { |
|
2108 img = new Image(); |
|
2109 img.src = src; |
|
2110 } |
|
2111 }); |
|
2112 if ( 'local' !== section.params.filter_type ) { |
|
2113 wp.a11y.speak( api.settings.l10n.themeSearchResults.replace( '%d', data.info.results ) ); |
|
2114 } |
|
2115 } |
|
2116 |
|
2117 _.delay( section.renderScreenshots, 100 ); // Wait for the controls to become visible. |
|
2118 |
|
2119 if ( 'local' === section.params.filter_type || 100 > themes.length ) { // If we have less than the requested 100 themes, it's the end of the list. |
|
2120 section.fullyLoaded = true; |
|
2121 } |
|
2122 } else { |
|
2123 if ( 0 === section.loaded ) { |
|
2124 section.container.find( '.no-themes' ).show(); |
|
2125 wp.a11y.speak( section.container.find( '.no-themes' ).text() ); |
|
2126 } else { |
|
2127 section.fullyLoaded = true; |
|
2128 } |
|
2129 } |
|
2130 if ( 'local' === section.params.filter_type ) { |
|
2131 section.updateCount(); // Count of visible theme controls. |
|
2132 } else { |
|
2133 section.updateCount( data.info.results ); // Total number of results including pages not yet loaded. |
|
2134 } |
|
2135 section.container.find( '.unexpected-error' ).hide(); // Hide error notice in case it was previously shown. |
|
2136 |
|
2137 // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case. |
|
2138 section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' ); |
|
2139 section.loading = false; |
|
2140 }); |
|
2141 request.fail(function( data ) { |
|
2142 if ( 'undefined' === typeof data ) { |
|
2143 section.container.find( '.unexpected-error' ).show(); |
|
2144 wp.a11y.speak( section.container.find( '.unexpected-error' ).text() ); |
|
2145 } else if ( 'undefined' !== typeof console && console.error ) { |
|
2146 console.error( data ); |
|
2147 } |
|
2148 |
|
2149 // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case. |
|
2150 section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' ); |
|
2151 section.loading = false; |
|
2152 }); |
|
2153 }, |
|
2154 |
|
2155 /** |
|
2156 * Loads controls into the section from data received from loadThemes(). |
|
2157 * |
|
2158 * @since 4.9.0 |
|
2159 * @param {Array} themes - Array of theme data to create controls with. |
|
2160 * @param {integer} page - Page of results being loaded. |
|
2161 * @returns {void} |
|
2162 */ |
|
2163 loadControls: function( themes, page ) { |
|
2164 var newThemeControls = [], |
|
2165 section = this; |
|
2166 |
|
2167 // Add controls for each theme. |
|
2168 _.each( themes, function( theme ) { |
|
2169 var themeControl = new api.controlConstructor.theme( section.params.action + '_theme_' + theme.id, { |
|
2170 type: 'theme', |
|
2171 section: section.params.id, |
|
2172 theme: theme, |
|
2173 priority: section.loaded + 1 |
|
2174 } ); |
|
2175 |
|
2176 api.control.add( themeControl ); |
|
2177 newThemeControls.push( themeControl ); |
|
2178 section.loaded = section.loaded + 1; |
|
2179 }); |
|
2180 |
|
2181 if ( 1 !== page ) { |
|
2182 Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue. |
|
2183 } |
|
2184 }, |
|
2185 |
|
2186 /** |
|
2187 * Determines whether more themes should be loaded, and loads them. |
|
2188 * |
|
2189 * @since 4.9.0 |
|
2190 * @returns {void} |
|
2191 */ |
|
2192 loadMore: function() { |
|
2193 var section = this, container, bottom, threshold; |
|
2194 if ( ! section.fullyLoaded && ! section.loading ) { |
|
2195 container = section.container.closest( '.customize-themes-full-container' ); |
|
2196 |
|
2197 bottom = container.scrollTop() + container.height(); |
|
2198 threshold = container.prop( 'scrollHeight' ) - 3000; // Use a fixed distance to the bottom of loaded results to avoid unnecessarily loading results sooner when using a percentage of scroll distance. |
|
2199 |
|
2200 if ( bottom > threshold ) { |
|
2201 section.loadThemes(); |
|
2202 } |
|
2203 } |
|
2204 }, |
|
2205 |
|
2206 /** |
|
2207 * Event handler for search input that filters visible controls. |
|
2208 * |
|
2209 * @since 4.9.0 |
|
2210 * |
|
2211 * @param {string} term - The raw search input value. |
|
2212 * @returns {void} |
|
2213 */ |
|
2214 filterSearch: function( term ) { |
|
2215 var count = 0, |
|
2216 visible = false, |
|
2217 section = this, |
|
2218 noFilter = ( api.section.has( 'wporg_themes' ) && 'remote' !== section.params.filter_type ) ? '.no-themes-local' : '.no-themes', |
|
2219 controls = section.controls(), |
|
2220 terms; |
|
2221 |
|
2222 if ( section.loading ) { |
|
2223 return; |
|
2224 } |
|
2225 |
|
2226 // Standardize search term format and split into an array of individual words. |
|
2227 terms = term.toLowerCase().trim().replace( /-/g, ' ' ).split( ' ' ); |
|
2228 |
|
2229 _.each( controls, function( control ) { |
|
2230 visible = control.filter( terms ); // Shows/hides and sorts control based on the applicability of the search term. |
|
2231 if ( visible ) { |
|
2232 count = count + 1; |
|
2233 } |
|
2234 }); |
|
2235 |
|
2236 if ( 0 === count ) { |
|
2237 section.container.find( noFilter ).show(); |
|
2238 wp.a11y.speak( section.container.find( noFilter ).text() ); |
|
2239 } else { |
|
2240 section.container.find( noFilter ).hide(); |
|
2241 } |
|
2242 |
|
2243 section.renderScreenshots(); |
|
2244 api.reflowPaneContents(); |
|
2245 |
|
2246 // Update theme count. |
|
2247 section.updateCountDebounced( count ); |
|
2248 }, |
|
2249 |
|
2250 /** |
|
2251 * Event handler for search input that determines if the terms have changed and loads new controls as needed. |
|
2252 * |
|
2253 * @since 4.9.0 |
|
2254 * |
|
2255 * @param {wp.customize.ThemesSection} section - The current theme section, passed through the debouncer. |
|
2256 * @returns {void} |
|
2257 */ |
|
2258 checkTerm: function( section ) { |
|
2259 var newTerm; |
|
2260 if ( 'remote' === section.params.filter_type ) { |
|
2261 newTerm = section.contentContainer.find( '.wp-filter-search' ).val(); |
|
2262 if ( section.term !== newTerm.trim() ) { |
|
2263 section.initializeNewQuery( newTerm, section.tags ); |
|
2264 } |
|
2265 } |
|
2266 }, |
|
2267 |
|
2268 /** |
|
2269 * Check for filters checked in the feature filter list and initialize a new query. |
|
2270 * |
|
2271 * @since 4.9.0 |
|
2272 * |
|
2273 * @returns {void} |
|
2274 */ |
|
2275 filtersChecked: function() { |
|
2276 var section = this, |
|
2277 items = section.container.find( '.filter-group' ).find( ':checkbox' ), |
|
2278 tags = []; |
|
2279 |
|
2280 _.each( items.filter( ':checked' ), function( item ) { |
|
2281 tags.push( $( item ).prop( 'value' ) ); |
|
2282 }); |
|
2283 |
|
2284 // When no filters are checked, restore initial state. Update filter count. |
|
2285 if ( 0 === tags.length ) { |
|
2286 tags = ''; |
|
2287 section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).show(); |
|
2288 section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).hide(); |
|
2289 } else { |
|
2290 section.contentContainer.find( '.feature-filter-toggle .theme-filter-count' ).text( tags.length ); |
|
2291 section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).hide(); |
|
2292 section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).show(); |
|
2293 } |
|
2294 |
|
2295 // Check whether tags have changed, and either load or queue them. |
|
2296 if ( ! _.isEqual( section.tags, tags ) ) { |
|
2297 if ( section.loading ) { |
|
2298 section.nextTags = tags; |
|
2299 } else { |
|
2300 if ( 'remote' === section.params.filter_type ) { |
|
2301 section.initializeNewQuery( section.term, tags ); |
|
2302 } else if ( 'local' === section.params.filter_type ) { |
|
2303 section.filterSearch( tags.join( ' ' ) ); |
|
2304 } |
|
2305 } |
|
2306 } |
|
2307 }, |
|
2308 |
|
2309 /** |
|
2310 * Reset the current query and load new results. |
|
2311 * |
|
2312 * @since 4.9.0 |
|
2313 * |
|
2314 * @param {string} newTerm - New term. |
|
2315 * @param {Array} newTags - New tags. |
|
2316 * @returns {void} |
|
2317 */ |
|
2318 initializeNewQuery: function( newTerm, newTags ) { |
|
2319 var section = this; |
|
2320 |
|
2321 // Clear the controls in the section. |
|
2322 _.each( section.controls(), function( control ) { |
|
2323 control.container.remove(); |
|
2324 api.control.remove( control.id ); |
|
2325 }); |
|
2326 section.loaded = 0; |
|
2327 section.fullyLoaded = false; |
|
2328 section.screenshotQueue = null; |
|
2329 |
|
2330 // Run a new query, with loadThemes handling paging, etc. |
|
2331 if ( ! section.loading ) { |
|
2332 section.term = newTerm; |
|
2333 section.tags = newTags; |
|
2334 section.loadThemes(); |
|
2335 } else { |
|
2336 section.nextTerm = newTerm; // This will reload from loadThemes() with the newest term once the current batch is loaded. |
|
2337 section.nextTags = newTags; // This will reload from loadThemes() with the newest tags once the current batch is loaded. |
|
2338 } |
|
2339 if ( ! section.expanded() ) { |
|
2340 section.expand(); // Expand the section if it isn't expanded. |
|
2341 } |
|
2342 }, |
|
2343 |
|
2344 /** |
|
2345 * Render control's screenshot if the control comes into view. |
|
2346 * |
|
2347 * @since 4.2.0 |
|
2348 * |
|
2349 * @returns {void} |
|
2350 */ |
|
2351 renderScreenshots: function() { |
|
2352 var section = this; |
|
2353 |
|
2354 // Fill queue initially, or check for more if empty. |
|
2355 if ( null === section.screenshotQueue || 0 === section.screenshotQueue.length ) { |
|
2356 |
|
2357 // Add controls that haven't had their screenshots rendered. |
|
2358 section.screenshotQueue = _.filter( section.controls(), function( control ) { |
|
2359 return ! control.screenshotRendered; |
736 }); |
2360 }); |
737 |
2361 } |
738 content.show( 0, function() { |
2362 |
739 position = content.offset().top; |
2363 // Are all screenshots rendered (for now)? |
740 scroll = container.scrollTop(); |
|
741 content.css( 'margin-top', ( $( '#customize-header-actions' ).height() - position - scroll ) ); |
|
742 section.addClass( 'current-panel' ); |
|
743 overlay.addClass( 'in-themes-panel' ); |
|
744 container.scrollTop( 0 ); |
|
745 _.delay( panel.renderScreenshots, 10 ); // Wait for the controls |
|
746 panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) ); |
|
747 if ( args.completeCallback ) { |
|
748 args.completeCallback(); |
|
749 } |
|
750 } ); |
|
751 topPanel.attr( 'tabindex', '-1' ); |
|
752 changeBtn.attr( 'tabindex', '-1' ); |
|
753 customizeBtn.focus(); |
|
754 } else { |
|
755 siblings.removeClass( 'open' ); |
|
756 section.removeClass( 'current-panel' ); |
|
757 overlay.removeClass( 'in-themes-panel' ); |
|
758 panel.$customizeSidebar.off( 'scroll.customize-themes-section' ); |
|
759 content.delay( 180 ).hide( 0, function() { |
|
760 content.css( 'margin-top', 'inherit' ); // Reset |
|
761 if ( args.completeCallback ) { |
|
762 args.completeCallback(); |
|
763 } |
|
764 } ); |
|
765 topPanel.attr( 'tabindex', '0' ); |
|
766 customizeBtn.attr( 'tabindex', '0' ); |
|
767 changeBtn.focus(); |
|
768 container.scrollTop( 0 ); |
|
769 } |
|
770 }, |
|
771 |
|
772 /** |
|
773 * Render control's screenshot if the control comes into view. |
|
774 * |
|
775 * @since 4.2.0 |
|
776 */ |
|
777 renderScreenshots: function( ) { |
|
778 var section = this; |
|
779 |
|
780 // Fill queue initially. |
|
781 if ( section.screenshotQueue === null ) { |
|
782 section.screenshotQueue = section.controls(); |
|
783 } |
|
784 |
|
785 // Are all screenshots rendered? |
|
786 if ( ! section.screenshotQueue.length ) { |
2364 if ( ! section.screenshotQueue.length ) { |
787 return; |
2365 return; |
788 } |
2366 } |
789 |
2367 |
790 section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) { |
2368 section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) { |
1085 } |
2917 } |
1086 return; |
2918 return; |
1087 } |
2919 } |
1088 |
2920 |
1089 // Note: there is a second argument 'args' passed |
2921 // Note: there is a second argument 'args' passed |
1090 var position, scroll, |
2922 var panel = this, |
1091 panel = this, |
2923 accordionSection = panel.contentContainer, |
1092 section = panel.container.closest( '.accordion-section' ), |
2924 overlay = accordionSection.closest( '.wp-full-overlay' ), |
1093 overlay = section.closest( '.wp-full-overlay' ), |
2925 container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ), |
1094 container = section.closest( '.wp-full-overlay-sidebar-content' ), |
2926 topPanel = panel.headContainer.find( '.accordion-section-title' ), |
1095 siblings = container.find( '.open' ), |
2927 backBtn = accordionSection.find( '.customize-panel-back' ), |
1096 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ), |
2928 childSections = panel.sections(), |
1097 backBtn = overlay.find( '.control-panel-back' ), |
2929 skipTransition; |
1098 panelTitle = section.find( '.accordion-section-title' ).first(), |
2930 |
1099 content = section.find( '.control-panel-content' ); |
2931 if ( expanded && ! accordionSection.hasClass( 'current-panel' ) ) { |
1100 |
|
1101 if ( expanded ) { |
|
1102 |
|
1103 // Collapse any sibling sections/panels |
2932 // Collapse any sibling sections/panels |
1104 api.section.each( function ( section ) { |
2933 api.section.each( function ( section ) { |
1105 if ( ! section.panel() ) { |
2934 if ( panel.id !== section.panel() ) { |
1106 section.collapse( { duration: 0 } ); |
2935 section.collapse( { duration: 0 } ); |
1107 } |
2936 } |
1108 }); |
2937 }); |
1109 api.panel.each( function ( otherPanel ) { |
2938 api.panel.each( function ( otherPanel ) { |
1110 if ( panel !== otherPanel ) { |
2939 if ( panel !== otherPanel ) { |
1111 otherPanel.collapse( { duration: 0 } ); |
2940 otherPanel.collapse( { duration: 0 } ); |
1112 } |
2941 } |
1113 }); |
2942 }); |
1114 |
2943 |
1115 content.show( 0, function() { |
2944 if ( panel.params.autoExpandSoleSection && 1 === childSections.length && childSections[0].active.get() ) { |
1116 content.parent().show(); |
2945 accordionSection.addClass( 'current-panel skip-transition' ); |
1117 position = content.offset().top; |
|
1118 scroll = container.scrollTop(); |
|
1119 content.css( 'margin-top', ( $( '#customize-header-actions' ).height() - position - scroll ) ); |
|
1120 section.addClass( 'current-panel' ); |
|
1121 overlay.addClass( 'in-sub-panel' ); |
2946 overlay.addClass( 'in-sub-panel' ); |
1122 container.scrollTop( 0 ); |
2947 |
1123 if ( args.completeCallback ) { |
2948 childSections[0].expand( { |
1124 args.completeCallback(); |
2949 completeCallback: args.completeCallback |
1125 } |
2950 } ); |
1126 } ); |
2951 } else { |
1127 topPanel.attr( 'tabindex', '-1' ); |
2952 panel._animateChangeExpanded( function() { |
1128 backBtn.attr( 'tabindex', '0' ); |
2953 topPanel.attr( 'tabindex', '-1' ); |
1129 backBtn.focus(); |
2954 backBtn.attr( 'tabindex', '0' ); |
|
2955 |
|
2956 backBtn.focus(); |
|
2957 accordionSection.css( 'top', '' ); |
|
2958 container.scrollTop( 0 ); |
|
2959 |
|
2960 if ( args.completeCallback ) { |
|
2961 args.completeCallback(); |
|
2962 } |
|
2963 } ); |
|
2964 |
|
2965 accordionSection.addClass( 'current-panel' ); |
|
2966 overlay.addClass( 'in-sub-panel' ); |
|
2967 } |
|
2968 |
|
2969 api.state( 'expandedPanel' ).set( panel ); |
|
2970 |
|
2971 } else if ( ! expanded && accordionSection.hasClass( 'current-panel' ) ) { |
|
2972 skipTransition = accordionSection.hasClass( 'skip-transition' ); |
|
2973 if ( ! skipTransition ) { |
|
2974 panel._animateChangeExpanded( function() { |
|
2975 topPanel.attr( 'tabindex', '0' ); |
|
2976 backBtn.attr( 'tabindex', '-1' ); |
|
2977 |
|
2978 topPanel.focus(); |
|
2979 accordionSection.css( 'top', '' ); |
|
2980 |
|
2981 if ( args.completeCallback ) { |
|
2982 args.completeCallback(); |
|
2983 } |
|
2984 } ); |
|
2985 } else { |
|
2986 accordionSection.removeClass( 'skip-transition' ); |
|
2987 } |
|
2988 |
|
2989 overlay.removeClass( 'in-sub-panel' ); |
|
2990 accordionSection.removeClass( 'current-panel' ); |
|
2991 if ( panel === api.state( 'expandedPanel' ).get() ) { |
|
2992 api.state( 'expandedPanel' ).set( false ); |
|
2993 } |
|
2994 } |
|
2995 }, |
|
2996 |
|
2997 /** |
|
2998 * Render the panel from its JS template, if it exists. |
|
2999 * |
|
3000 * The panel's container must already exist in the DOM. |
|
3001 * |
|
3002 * @since 4.3.0 |
|
3003 */ |
|
3004 renderContent: function () { |
|
3005 var template, |
|
3006 panel = this; |
|
3007 |
|
3008 // Add the content to the container. |
|
3009 if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) { |
|
3010 template = wp.template( panel.templateSelector + '-content' ); |
1130 } else { |
3011 } else { |
1131 siblings.removeClass( 'open' ); |
3012 template = wp.template( 'customize-panel-default-content' ); |
1132 section.removeClass( 'current-panel' ); |
3013 } |
1133 overlay.removeClass( 'in-sub-panel' ); |
3014 if ( template && panel.headContainer ) { |
1134 content.delay( 180 ).hide( 0, function() { |
3015 panel.contentContainer.html( template( _.extend( |
1135 content.css( 'margin-top', 'inherit' ); // Reset |
3016 { id: panel.id }, |
1136 if ( args.completeCallback ) { |
3017 panel.params |
1137 args.completeCallback(); |
3018 ) ) ); |
1138 } |
|
1139 } ); |
|
1140 topPanel.attr( 'tabindex', '0' ); |
|
1141 backBtn.attr( 'tabindex', '-1' ); |
|
1142 panelTitle.focus(); |
|
1143 container.scrollTop( 0 ); |
|
1144 } |
3019 } |
1145 } |
3020 } |
1146 }); |
3021 }); |
1147 |
3022 |
1148 /** |
3023 /** |
|
3024 * Class wp.customize.ThemesPanel. |
|
3025 * |
|
3026 * Custom section for themes that displays without the customize preview. |
|
3027 * |
|
3028 * @constructor |
|
3029 * @augments wp.customize.Panel |
|
3030 * @augments wp.customize.Container |
|
3031 */ |
|
3032 api.ThemesPanel = api.Panel.extend({ |
|
3033 |
|
3034 /** |
|
3035 * Initialize. |
|
3036 * |
|
3037 * @since 4.9.0 |
|
3038 * |
|
3039 * @param {string} id - The ID for the panel. |
|
3040 * @param {object} options - Options. |
|
3041 * @returns {void} |
|
3042 */ |
|
3043 initialize: function( id, options ) { |
|
3044 var panel = this; |
|
3045 panel.installingThemes = []; |
|
3046 api.Panel.prototype.initialize.call( panel, id, options ); |
|
3047 }, |
|
3048 |
|
3049 /** |
|
3050 * Determine whether a given theme can be switched to, or in general. |
|
3051 * |
|
3052 * @since 4.9.0 |
|
3053 * |
|
3054 * @param {string} [slug] - Theme slug. |
|
3055 * @returns {boolean} Whether the theme can be switched to. |
|
3056 */ |
|
3057 canSwitchTheme: function canSwitchTheme( slug ) { |
|
3058 if ( slug && slug === api.settings.theme.stylesheet ) { |
|
3059 return true; |
|
3060 } |
|
3061 return 'publish' === api.state( 'selectedChangesetStatus' ).get() && ( '' === api.state( 'changesetStatus' ).get() || 'auto-draft' === api.state( 'changesetStatus' ).get() ); |
|
3062 }, |
|
3063 |
|
3064 /** |
|
3065 * Attach events. |
|
3066 * |
|
3067 * @since 4.9.0 |
|
3068 * @returns {void} |
|
3069 */ |
|
3070 attachEvents: function() { |
|
3071 var panel = this; |
|
3072 |
|
3073 // Attach regular panel events. |
|
3074 api.Panel.prototype.attachEvents.apply( panel ); |
|
3075 |
|
3076 // Temporary since supplying SFTP credentials does not work yet. See #42184 |
|
3077 if ( api.settings.theme._canInstall && api.settings.theme._filesystemCredentialsNeeded ) { |
|
3078 panel.notifications.add( new api.Notification( 'theme_install_unavailable', { |
|
3079 message: api.l10n.themeInstallUnavailable, |
|
3080 type: 'info', |
|
3081 dismissible: true |
|
3082 } ) ); |
|
3083 } |
|
3084 |
|
3085 function toggleDisabledNotifications() { |
|
3086 if ( panel.canSwitchTheme() ) { |
|
3087 panel.notifications.remove( 'theme_switch_unavailable' ); |
|
3088 } else { |
|
3089 panel.notifications.add( new api.Notification( 'theme_switch_unavailable', { |
|
3090 message: api.l10n.themePreviewUnavailable, |
|
3091 type: 'warning' |
|
3092 } ) ); |
|
3093 } |
|
3094 } |
|
3095 toggleDisabledNotifications(); |
|
3096 api.state( 'selectedChangesetStatus' ).bind( toggleDisabledNotifications ); |
|
3097 api.state( 'changesetStatus' ).bind( toggleDisabledNotifications ); |
|
3098 |
|
3099 // Collapse panel to customize the current theme. |
|
3100 panel.contentContainer.on( 'click', '.customize-theme', function() { |
|
3101 panel.collapse(); |
|
3102 }); |
|
3103 |
|
3104 // Toggle between filtering and browsing themes on mobile. |
|
3105 panel.contentContainer.on( 'click', '.customize-themes-section-title, .customize-themes-mobile-back', function() { |
|
3106 $( '.wp-full-overlay' ).toggleClass( 'showing-themes' ); |
|
3107 }); |
|
3108 |
|
3109 // Install (and maybe preview) a theme. |
|
3110 panel.contentContainer.on( 'click', '.theme-install', function( event ) { |
|
3111 panel.installTheme( event ); |
|
3112 }); |
|
3113 |
|
3114 // Update a theme. Theme cards have the class, the details modal has the id. |
|
3115 panel.contentContainer.on( 'click', '.update-theme, #update-theme', function( event ) { |
|
3116 |
|
3117 // #update-theme is a link. |
|
3118 event.preventDefault(); |
|
3119 event.stopPropagation(); |
|
3120 |
|
3121 panel.updateTheme( event ); |
|
3122 }); |
|
3123 |
|
3124 // Delete a theme. |
|
3125 panel.contentContainer.on( 'click', '.delete-theme', function( event ) { |
|
3126 panel.deleteTheme( event ); |
|
3127 }); |
|
3128 |
|
3129 _.bindAll( panel, 'installTheme', 'updateTheme' ); |
|
3130 }, |
|
3131 |
|
3132 /** |
|
3133 * Update UI to reflect expanded state |
|
3134 * |
|
3135 * @since 4.9.0 |
|
3136 * |
|
3137 * @param {Boolean} expanded - Expanded state. |
|
3138 * @param {Object} args - Args. |
|
3139 * @param {Boolean} args.unchanged - Whether or not the state changed. |
|
3140 * @param {Function} args.completeCallback - Callback to execute when the animation completes. |
|
3141 * @returns {void} |
|
3142 */ |
|
3143 onChangeExpanded: function( expanded, args ) { |
|
3144 var panel = this, overlay, sections, hasExpandedSection = false; |
|
3145 |
|
3146 // Expand/collapse the panel normally. |
|
3147 api.Panel.prototype.onChangeExpanded.apply( this, [ expanded, args ] ); |
|
3148 |
|
3149 // Immediately call the complete callback if there were no changes |
|
3150 if ( args.unchanged ) { |
|
3151 if ( args.completeCallback ) { |
|
3152 args.completeCallback(); |
|
3153 } |
|
3154 return; |
|
3155 } |
|
3156 |
|
3157 overlay = panel.headContainer.closest( '.wp-full-overlay' ); |
|
3158 |
|
3159 if ( expanded ) { |
|
3160 overlay |
|
3161 .addClass( 'in-themes-panel' ) |
|
3162 .delay( 200 ).find( '.customize-themes-full-container' ).addClass( 'animate' ); |
|
3163 |
|
3164 _.delay( function() { |
|
3165 overlay.addClass( 'themes-panel-expanded' ); |
|
3166 }, 200 ); |
|
3167 |
|
3168 // Automatically open the first section (except on small screens), if one isn't already expanded. |
|
3169 if ( 600 < window.innerWidth ) { |
|
3170 sections = panel.sections(); |
|
3171 _.each( sections, function( section ) { |
|
3172 if ( section.expanded() ) { |
|
3173 hasExpandedSection = true; |
|
3174 } |
|
3175 } ); |
|
3176 if ( ! hasExpandedSection && sections.length > 0 ) { |
|
3177 sections[0].expand(); |
|
3178 } |
|
3179 } |
|
3180 } else { |
|
3181 overlay |
|
3182 .removeClass( 'in-themes-panel themes-panel-expanded' ) |
|
3183 .find( '.customize-themes-full-container' ).removeClass( 'animate' ); |
|
3184 } |
|
3185 }, |
|
3186 |
|
3187 /** |
|
3188 * Install a theme via wp.updates. |
|
3189 * |
|
3190 * @since 4.9.0 |
|
3191 * |
|
3192 * @param {jQuery.Event} event - Event. |
|
3193 * @returns {jQuery.promise} Promise. |
|
3194 */ |
|
3195 installTheme: function( event ) { |
|
3196 var panel = this, preview, onInstallSuccess, slug = $( event.target ).data( 'slug' ), deferred = $.Deferred(), request; |
|
3197 preview = $( event.target ).hasClass( 'preview' ); |
|
3198 |
|
3199 // Temporary since supplying SFTP credentials does not work yet. See #42184. |
|
3200 if ( api.settings.theme._filesystemCredentialsNeeded ) { |
|
3201 deferred.reject({ |
|
3202 errorCode: 'theme_install_unavailable' |
|
3203 }); |
|
3204 return deferred.promise(); |
|
3205 } |
|
3206 |
|
3207 // Prevent loading a non-active theme preview when there is a drafted/scheduled changeset. |
|
3208 if ( ! panel.canSwitchTheme( slug ) ) { |
|
3209 deferred.reject({ |
|
3210 errorCode: 'theme_switch_unavailable' |
|
3211 }); |
|
3212 return deferred.promise(); |
|
3213 } |
|
3214 |
|
3215 // Theme is already being installed. |
|
3216 if ( _.contains( panel.installingThemes, slug ) ) { |
|
3217 deferred.reject({ |
|
3218 errorCode: 'theme_already_installing' |
|
3219 }); |
|
3220 return deferred.promise(); |
|
3221 } |
|
3222 |
|
3223 wp.updates.maybeRequestFilesystemCredentials( event ); |
|
3224 |
|
3225 onInstallSuccess = function( response ) { |
|
3226 var theme = false, themeControl; |
|
3227 if ( preview ) { |
|
3228 api.notifications.remove( 'theme_installing' ); |
|
3229 |
|
3230 panel.loadThemePreview( slug ); |
|
3231 |
|
3232 } else { |
|
3233 api.control.each( function( control ) { |
|
3234 if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) { |
|
3235 theme = control.params.theme; // Used below to add theme control. |
|
3236 control.rerenderAsInstalled( true ); |
|
3237 } |
|
3238 }); |
|
3239 |
|
3240 // Don't add the same theme more than once. |
|
3241 if ( ! theme || api.control.has( 'installed_theme_' + theme.id ) ) { |
|
3242 deferred.resolve( response ); |
|
3243 return; |
|
3244 } |
|
3245 |
|
3246 // Add theme control to installed section. |
|
3247 theme.type = 'installed'; |
|
3248 themeControl = new api.controlConstructor.theme( 'installed_theme_' + theme.id, { |
|
3249 type: 'theme', |
|
3250 section: 'installed_themes', |
|
3251 theme: theme, |
|
3252 priority: 0 // Add all newly-installed themes to the top. |
|
3253 } ); |
|
3254 |
|
3255 api.control.add( themeControl ); |
|
3256 api.control( themeControl.id ).container.trigger( 'render-screenshot' ); |
|
3257 |
|
3258 // Close the details modal if it's open to the installed theme. |
|
3259 api.section.each( function( section ) { |
|
3260 if ( 'themes' === section.params.type ) { |
|
3261 if ( theme.id === section.currentTheme ) { // Don't close the modal if the user has navigated elsewhere. |
|
3262 section.closeDetails(); |
|
3263 } |
|
3264 } |
|
3265 }); |
|
3266 } |
|
3267 deferred.resolve( response ); |
|
3268 }; |
|
3269 |
|
3270 panel.installingThemes.push( slug ); // Note: we don't remove elements from installingThemes, since they shouldn't be installed again. |
|
3271 request = wp.updates.installTheme( { |
|
3272 slug: slug |
|
3273 } ); |
|
3274 |
|
3275 // Also preview the theme as the event is triggered on Install & Preview. |
|
3276 if ( preview ) { |
|
3277 api.notifications.add( new api.OverlayNotification( 'theme_installing', { |
|
3278 message: api.l10n.themeDownloading, |
|
3279 type: 'info', |
|
3280 loading: true |
|
3281 } ) ); |
|
3282 } |
|
3283 |
|
3284 request.done( onInstallSuccess ); |
|
3285 request.fail( function() { |
|
3286 api.notifications.remove( 'theme_installing' ); |
|
3287 } ); |
|
3288 |
|
3289 return deferred.promise(); |
|
3290 }, |
|
3291 |
|
3292 /** |
|
3293 * Load theme preview. |
|
3294 * |
|
3295 * @since 4.9.0 |
|
3296 * |
|
3297 * @param {string} themeId Theme ID. |
|
3298 * @returns {jQuery.promise} Promise. |
|
3299 */ |
|
3300 loadThemePreview: function( themeId ) { |
|
3301 var panel = this, deferred = $.Deferred(), onceProcessingComplete, urlParser, queryParams; |
|
3302 |
|
3303 // Prevent loading a non-active theme preview when there is a drafted/scheduled changeset. |
|
3304 if ( ! panel.canSwitchTheme( themeId ) ) { |
|
3305 deferred.reject({ |
|
3306 errorCode: 'theme_switch_unavailable' |
|
3307 }); |
|
3308 return deferred.promise(); |
|
3309 } |
|
3310 |
|
3311 urlParser = document.createElement( 'a' ); |
|
3312 urlParser.href = location.href; |
|
3313 queryParams = _.extend( |
|
3314 api.utils.parseQueryString( urlParser.search.substr( 1 ) ), |
|
3315 { |
|
3316 theme: themeId, |
|
3317 changeset_uuid: api.settings.changeset.uuid, |
|
3318 'return': api.settings.url['return'] |
|
3319 } |
|
3320 ); |
|
3321 |
|
3322 // Include autosaved param to load autosave revision without prompting user to restore it. |
|
3323 if ( ! api.state( 'saved' ).get() ) { |
|
3324 queryParams.customize_autosaved = 'on'; |
|
3325 } |
|
3326 |
|
3327 urlParser.search = $.param( queryParams ); |
|
3328 |
|
3329 // Update loading message. Everything else is handled by reloading the page. |
|
3330 api.notifications.add( new api.OverlayNotification( 'theme_previewing', { |
|
3331 message: api.l10n.themePreviewWait, |
|
3332 type: 'info', |
|
3333 loading: true |
|
3334 } ) ); |
|
3335 |
|
3336 onceProcessingComplete = function() { |
|
3337 var request; |
|
3338 if ( api.state( 'processing' ).get() > 0 ) { |
|
3339 return; |
|
3340 } |
|
3341 |
|
3342 api.state( 'processing' ).unbind( onceProcessingComplete ); |
|
3343 |
|
3344 request = api.requestChangesetUpdate( {}, { autosave: true } ); |
|
3345 request.done( function() { |
|
3346 deferred.resolve(); |
|
3347 $( window ).off( 'beforeunload.customize-confirm' ); |
|
3348 location.replace( urlParser.href ); |
|
3349 } ); |
|
3350 request.fail( function() { |
|
3351 |
|
3352 // @todo Show notification regarding failure. |
|
3353 api.notifications.remove( 'theme_previewing' ); |
|
3354 |
|
3355 deferred.reject(); |
|
3356 } ); |
|
3357 }; |
|
3358 |
|
3359 if ( 0 === api.state( 'processing' ).get() ) { |
|
3360 onceProcessingComplete(); |
|
3361 } else { |
|
3362 api.state( 'processing' ).bind( onceProcessingComplete ); |
|
3363 } |
|
3364 |
|
3365 return deferred.promise(); |
|
3366 }, |
|
3367 |
|
3368 /** |
|
3369 * Update a theme via wp.updates. |
|
3370 * |
|
3371 * @since 4.9.0 |
|
3372 * |
|
3373 * @param {jQuery.Event} event - Event. |
|
3374 * @returns {void} |
|
3375 */ |
|
3376 updateTheme: function( event ) { |
|
3377 wp.updates.maybeRequestFilesystemCredentials( event ); |
|
3378 |
|
3379 $( document ).one( 'wp-theme-update-success', function( e, response ) { |
|
3380 |
|
3381 // Rerender the control to reflect the update. |
|
3382 api.control.each( function( control ) { |
|
3383 if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) { |
|
3384 control.params.theme.hasUpdate = false; |
|
3385 control.params.theme.version = response.newVersion; |
|
3386 setTimeout( function() { |
|
3387 control.rerenderAsInstalled( true ); |
|
3388 }, 2000 ); |
|
3389 } |
|
3390 }); |
|
3391 } ); |
|
3392 |
|
3393 wp.updates.updateTheme( { |
|
3394 slug: $( event.target ).closest( '.notice' ).data( 'slug' ) |
|
3395 } ); |
|
3396 }, |
|
3397 |
|
3398 /** |
|
3399 * Delete a theme via wp.updates. |
|
3400 * |
|
3401 * @since 4.9.0 |
|
3402 * |
|
3403 * @param {jQuery.Event} event - Event. |
|
3404 * @returns {void} |
|
3405 */ |
|
3406 deleteTheme: function( event ) { |
|
3407 var theme, section; |
|
3408 theme = $( event.target ).data( 'slug' ); |
|
3409 section = api.section( 'installed_themes' ); |
|
3410 |
|
3411 event.preventDefault(); |
|
3412 |
|
3413 // Temporary since supplying SFTP credentials does not work yet. See #42184. |
|
3414 if ( api.settings.theme._filesystemCredentialsNeeded ) { |
|
3415 return; |
|
3416 } |
|
3417 |
|
3418 // Confirmation dialog for deleting a theme. |
|
3419 if ( ! window.confirm( api.settings.l10n.confirmDeleteTheme ) ) { |
|
3420 return; |
|
3421 } |
|
3422 |
|
3423 wp.updates.maybeRequestFilesystemCredentials( event ); |
|
3424 |
|
3425 $( document ).one( 'wp-theme-delete-success', function() { |
|
3426 var control = api.control( 'installed_theme_' + theme ); |
|
3427 |
|
3428 // Remove theme control. |
|
3429 control.container.remove(); |
|
3430 api.control.remove( control.id ); |
|
3431 |
|
3432 // Update installed count. |
|
3433 section.loaded = section.loaded - 1; |
|
3434 section.updateCount(); |
|
3435 |
|
3436 // Rerender any other theme controls as uninstalled. |
|
3437 api.control.each( function( control ) { |
|
3438 if ( 'theme' === control.params.type && control.params.theme.id === theme ) { |
|
3439 control.rerenderAsInstalled( false ); |
|
3440 } |
|
3441 }); |
|
3442 } ); |
|
3443 |
|
3444 wp.updates.deleteTheme( { |
|
3445 slug: theme |
|
3446 } ); |
|
3447 |
|
3448 // Close modal and focus the section. |
|
3449 section.closeDetails(); |
|
3450 section.focus(); |
|
3451 } |
|
3452 }); |
|
3453 |
|
3454 /** |
1149 * A Customizer Control. |
3455 * A Customizer Control. |
1150 * |
3456 * |
1151 * A control provides a UI element that allows a user to modify a Customizer Setting. |
3457 * A control provides a UI element that allows a user to modify a Customizer Setting. |
1152 * |
3458 * |
1153 * @see PHP class WP_Customize_Control. |
3459 * @see PHP class WP_Customize_Control. |
1154 * |
3460 * |
1155 * @class |
3461 * @class |
1156 * @augments wp.customize.Class |
3462 * @augments wp.customize.Class |
1157 * |
|
1158 * @param {string} id Unique identifier for the control instance. |
|
1159 * @param {object} options Options hash for the control instance. |
|
1160 * @param {object} options.params |
|
1161 * @param {object} options.params.type Type of control (e.g. text, radio, dropdown-pages, etc.) |
|
1162 * @param {string} options.params.content The HTML content for the control. |
|
1163 * @param {string} options.params.priority Order of priority to show the control within the section. |
|
1164 * @param {string} options.params.active |
|
1165 * @param {string} options.params.section |
|
1166 * @param {string} options.params.label |
|
1167 * @param {string} options.params.description |
|
1168 * @param {string} options.params.instanceNumber Order in which this instance was created in relation to other instances. |
|
1169 */ |
3463 */ |
1170 api.Control = api.Class.extend({ |
3464 api.Control = api.Class.extend({ |
1171 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop }, |
3465 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop }, |
1172 |
3466 |
|
3467 /** |
|
3468 * Default params. |
|
3469 * |
|
3470 * @since 4.9.0 |
|
3471 * @var {object} |
|
3472 */ |
|
3473 defaults: { |
|
3474 label: '', |
|
3475 description: '', |
|
3476 active: true, |
|
3477 priority: 10 |
|
3478 }, |
|
3479 |
|
3480 /** |
|
3481 * Initialize. |
|
3482 * |
|
3483 * @param {string} id - Unique identifier for the control instance. |
|
3484 * @param {object} options - Options hash for the control instance. |
|
3485 * @param {object} options.type - Type of control (e.g. text, radio, dropdown-pages, etc.) |
|
3486 * @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. |
|
3487 * @param {string} [options.templateId] - Template ID for control's content. |
|
3488 * @param {string} [options.priority=10] - Order of priority to show the control within the section. |
|
3489 * @param {string} [options.active=true] - Whether the control is active. |
|
3490 * @param {string} options.section - The ID of the section the control belongs to. |
|
3491 * @param {mixed} [options.setting] - The ID of the main setting or an instance of this setting. |
|
3492 * @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. |
|
3493 * @param {mixed} options.settings.default - The ID of the setting the control relates to. |
|
3494 * @param {string} options.settings.data - @todo Is this used? |
|
3495 * @param {string} options.label - Label. |
|
3496 * @param {string} options.description - Description. |
|
3497 * @param {number} [options.instanceNumber] - Order in which this instance was created in relation to other instances. |
|
3498 * @param {object} [options.params] - Deprecated wrapper for the above properties. |
|
3499 * @returns {void} |
|
3500 */ |
1173 initialize: function( id, options ) { |
3501 initialize: function( id, options ) { |
1174 var control = this, |
3502 var control = this, deferredSettingIds = [], settings, gatherSettings; |
1175 nodes, radios, settings; |
3503 |
1176 |
3504 control.params = _.extend( |
1177 control.params = {}; |
3505 {}, |
1178 $.extend( control, options || {} ); |
3506 control.defaults, |
|
3507 control.params || {}, // In case sub-class already defines. |
|
3508 options.params || options || {} // The options.params property is deprecated, but it is checked first for back-compat. |
|
3509 ); |
|
3510 |
|
3511 if ( ! api.Control.instanceCounter ) { |
|
3512 api.Control.instanceCounter = 0; |
|
3513 } |
|
3514 api.Control.instanceCounter++; |
|
3515 if ( ! control.params.instanceNumber ) { |
|
3516 control.params.instanceNumber = api.Control.instanceCounter; |
|
3517 } |
|
3518 |
|
3519 // Look up the type if one was not supplied. |
|
3520 if ( ! control.params.type ) { |
|
3521 _.find( api.controlConstructor, function( Constructor, type ) { |
|
3522 if ( Constructor === control.constructor ) { |
|
3523 control.params.type = type; |
|
3524 return true; |
|
3525 } |
|
3526 return false; |
|
3527 } ); |
|
3528 } |
|
3529 |
|
3530 if ( ! control.params.content ) { |
|
3531 control.params.content = $( '<li></li>', { |
|
3532 id: 'customize-control-' + id.replace( /]/g, '' ).replace( /\[/g, '-' ), |
|
3533 'class': 'customize-control customize-control-' + control.params.type |
|
3534 } ); |
|
3535 } |
|
3536 |
1179 control.id = id; |
3537 control.id = id; |
1180 control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); |
3538 control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); // Deprecated, likely dead code from time before #28709. |
1181 control.templateSelector = 'customize-control-' + control.params.type + '-content'; |
3539 if ( control.params.content ) { |
1182 control.container = control.params.content ? $( control.params.content ) : $( control.selector ); |
3540 control.container = $( control.params.content ); |
1183 |
3541 } else { |
1184 control.deferred = { |
3542 control.container = $( control.selector ); // Likely dead, per above. See #28709. |
|
3543 } |
|
3544 |
|
3545 if ( control.params.templateId ) { |
|
3546 control.templateSelector = control.params.templateId; |
|
3547 } else { |
|
3548 control.templateSelector = 'customize-control-' + control.params.type + '-content'; |
|
3549 } |
|
3550 |
|
3551 control.deferred = _.extend( control.deferred || {}, { |
1185 embedded: new $.Deferred() |
3552 embedded: new $.Deferred() |
1186 }; |
3553 } ); |
1187 control.section = new api.Value(); |
3554 control.section = new api.Value(); |
1188 control.priority = new api.Value(); |
3555 control.priority = new api.Value(); |
1189 control.active = new api.Value(); |
3556 control.active = new api.Value(); |
1190 control.activeArgumentsQueue = []; |
3557 control.activeArgumentsQueue = []; |
|
3558 control.notifications = new api.Notifications({ |
|
3559 alt: control.altNotice |
|
3560 }); |
1191 |
3561 |
1192 control.elements = []; |
3562 control.elements = []; |
1193 |
|
1194 nodes = control.container.find('[data-customize-setting-link]'); |
|
1195 radios = {}; |
|
1196 |
|
1197 nodes.each( function() { |
|
1198 var node = $( this ), |
|
1199 name; |
|
1200 |
|
1201 if ( node.is( ':radio' ) ) { |
|
1202 name = node.prop( 'name' ); |
|
1203 if ( radios[ name ] ) { |
|
1204 return; |
|
1205 } |
|
1206 |
|
1207 radios[ name ] = true; |
|
1208 node = nodes.filter( '[name="' + name + '"]' ); |
|
1209 } |
|
1210 |
|
1211 api( node.data( 'customizeSettingLink' ), function( setting ) { |
|
1212 var element = new api.Element( node ); |
|
1213 control.elements.push( element ); |
|
1214 element.sync( setting ); |
|
1215 element.set( setting() ); |
|
1216 }); |
|
1217 }); |
|
1218 |
3563 |
1219 control.active.bind( function ( active ) { |
3564 control.active.bind( function ( active ) { |
1220 var args = control.activeArgumentsQueue.shift(); |
3565 var args = control.activeArgumentsQueue.shift(); |
1221 args = $.extend( {}, control.defaultActiveArguments, args ); |
3566 args = $.extend( {}, control.defaultActiveArguments, args ); |
1222 control.onChangeActive( active, args ); |
3567 control.onChangeActive( active, args ); |
1676 api.UploadControl.prototype.select.apply( this, arguments ); |
4466 api.UploadControl.prototype.select.apply( this, arguments ); |
1677 |
4467 |
1678 wp.ajax.post( 'custom-background-add', { |
4468 wp.ajax.post( 'custom-background-add', { |
1679 nonce: _wpCustomizeBackground.nonces.add, |
4469 nonce: _wpCustomizeBackground.nonces.add, |
1680 wp_customize: 'on', |
4470 wp_customize: 'on', |
1681 theme: api.settings.theme.stylesheet, |
4471 customize_theme: api.settings.theme.stylesheet, |
1682 attachment_id: this.params.attachment.id |
4472 attachment_id: this.params.attachment.id |
1683 } ); |
4473 } ); |
|
4474 } |
|
4475 }); |
|
4476 |
|
4477 /** |
|
4478 * A control for positioning a background image. |
|
4479 * |
|
4480 * @since 4.7.0 |
|
4481 * |
|
4482 * @class |
|
4483 * @augments wp.customize.Control |
|
4484 * @augments wp.customize.Class |
|
4485 */ |
|
4486 api.BackgroundPositionControl = api.Control.extend( { |
|
4487 |
|
4488 /** |
|
4489 * Set up control UI once embedded in DOM and settings are created. |
|
4490 * |
|
4491 * @since 4.7.0 |
|
4492 * @access public |
|
4493 */ |
|
4494 ready: function() { |
|
4495 var control = this, updateRadios; |
|
4496 |
|
4497 control.container.on( 'change', 'input[name="background-position"]', function() { |
|
4498 var position = $( this ).val().split( ' ' ); |
|
4499 control.settings.x( position[0] ); |
|
4500 control.settings.y( position[1] ); |
|
4501 } ); |
|
4502 |
|
4503 updateRadios = _.debounce( function() { |
|
4504 var x, y, radioInput, inputValue; |
|
4505 x = control.settings.x.get(); |
|
4506 y = control.settings.y.get(); |
|
4507 inputValue = String( x ) + ' ' + String( y ); |
|
4508 radioInput = control.container.find( 'input[name="background-position"][value="' + inputValue + '"]' ); |
|
4509 radioInput.click(); |
|
4510 } ); |
|
4511 control.settings.x.bind( updateRadios ); |
|
4512 control.settings.y.bind( updateRadios ); |
|
4513 |
|
4514 updateRadios(); // Set initial UI. |
|
4515 } |
|
4516 } ); |
|
4517 |
|
4518 /** |
|
4519 * A control for selecting and cropping an image. |
|
4520 * |
|
4521 * @class |
|
4522 * @augments wp.customize.MediaControl |
|
4523 * @augments wp.customize.Control |
|
4524 * @augments wp.customize.Class |
|
4525 */ |
|
4526 api.CroppedImageControl = api.MediaControl.extend({ |
|
4527 |
|
4528 /** |
|
4529 * Open the media modal to the library state. |
|
4530 */ |
|
4531 openFrame: function( event ) { |
|
4532 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
|
4533 return; |
|
4534 } |
|
4535 |
|
4536 this.initFrame(); |
|
4537 this.frame.setState( 'library' ).open(); |
|
4538 }, |
|
4539 |
|
4540 /** |
|
4541 * Create a media modal select frame, and store it so the instance can be reused when needed. |
|
4542 */ |
|
4543 initFrame: function() { |
|
4544 var l10n = _wpMediaViewsL10n; |
|
4545 |
|
4546 this.frame = wp.media({ |
|
4547 button: { |
|
4548 text: l10n.select, |
|
4549 close: false |
|
4550 }, |
|
4551 states: [ |
|
4552 new wp.media.controller.Library({ |
|
4553 title: this.params.button_labels.frame_title, |
|
4554 library: wp.media.query({ type: 'image' }), |
|
4555 multiple: false, |
|
4556 date: false, |
|
4557 priority: 20, |
|
4558 suggestedWidth: this.params.width, |
|
4559 suggestedHeight: this.params.height |
|
4560 }), |
|
4561 new wp.media.controller.CustomizeImageCropper({ |
|
4562 imgSelectOptions: this.calculateImageSelectOptions, |
|
4563 control: this |
|
4564 }) |
|
4565 ] |
|
4566 }); |
|
4567 |
|
4568 this.frame.on( 'select', this.onSelect, this ); |
|
4569 this.frame.on( 'cropped', this.onCropped, this ); |
|
4570 this.frame.on( 'skippedcrop', this.onSkippedCrop, this ); |
|
4571 }, |
|
4572 |
|
4573 /** |
|
4574 * After an image is selected in the media modal, switch to the cropper |
|
4575 * state if the image isn't the right size. |
|
4576 */ |
|
4577 onSelect: function() { |
|
4578 var attachment = this.frame.state().get( 'selection' ).first().toJSON(); |
|
4579 |
|
4580 if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) { |
|
4581 this.setImageFromAttachment( attachment ); |
|
4582 this.frame.close(); |
|
4583 } else { |
|
4584 this.frame.setState( 'cropper' ); |
|
4585 } |
|
4586 }, |
|
4587 |
|
4588 /** |
|
4589 * After the image has been cropped, apply the cropped image data to the setting. |
|
4590 * |
|
4591 * @param {object} croppedImage Cropped attachment data. |
|
4592 */ |
|
4593 onCropped: function( croppedImage ) { |
|
4594 this.setImageFromAttachment( croppedImage ); |
|
4595 }, |
|
4596 |
|
4597 /** |
|
4598 * Returns a set of options, computed from the attached image data and |
|
4599 * control-specific data, to be fed to the imgAreaSelect plugin in |
|
4600 * wp.media.view.Cropper. |
|
4601 * |
|
4602 * @param {wp.media.model.Attachment} attachment |
|
4603 * @param {wp.media.controller.Cropper} controller |
|
4604 * @returns {Object} Options |
|
4605 */ |
|
4606 calculateImageSelectOptions: function( attachment, controller ) { |
|
4607 var control = controller.get( 'control' ), |
|
4608 flexWidth = !! parseInt( control.params.flex_width, 10 ), |
|
4609 flexHeight = !! parseInt( control.params.flex_height, 10 ), |
|
4610 realWidth = attachment.get( 'width' ), |
|
4611 realHeight = attachment.get( 'height' ), |
|
4612 xInit = parseInt( control.params.width, 10 ), |
|
4613 yInit = parseInt( control.params.height, 10 ), |
|
4614 ratio = xInit / yInit, |
|
4615 xImg = xInit, |
|
4616 yImg = yInit, |
|
4617 x1, y1, imgSelectOptions; |
|
4618 |
|
4619 controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) ); |
|
4620 |
|
4621 if ( realWidth / realHeight > ratio ) { |
|
4622 yInit = realHeight; |
|
4623 xInit = yInit * ratio; |
|
4624 } else { |
|
4625 xInit = realWidth; |
|
4626 yInit = xInit / ratio; |
|
4627 } |
|
4628 |
|
4629 x1 = ( realWidth - xInit ) / 2; |
|
4630 y1 = ( realHeight - yInit ) / 2; |
|
4631 |
|
4632 imgSelectOptions = { |
|
4633 handles: true, |
|
4634 keys: true, |
|
4635 instance: true, |
|
4636 persistent: true, |
|
4637 imageWidth: realWidth, |
|
4638 imageHeight: realHeight, |
|
4639 minWidth: xImg > xInit ? xInit : xImg, |
|
4640 minHeight: yImg > yInit ? yInit : yImg, |
|
4641 x1: x1, |
|
4642 y1: y1, |
|
4643 x2: xInit + x1, |
|
4644 y2: yInit + y1 |
|
4645 }; |
|
4646 |
|
4647 if ( flexHeight === false && flexWidth === false ) { |
|
4648 imgSelectOptions.aspectRatio = xInit + ':' + yInit; |
|
4649 } |
|
4650 |
|
4651 if ( true === flexHeight ) { |
|
4652 delete imgSelectOptions.minHeight; |
|
4653 imgSelectOptions.maxWidth = realWidth; |
|
4654 } |
|
4655 |
|
4656 if ( true === flexWidth ) { |
|
4657 delete imgSelectOptions.minWidth; |
|
4658 imgSelectOptions.maxHeight = realHeight; |
|
4659 } |
|
4660 |
|
4661 return imgSelectOptions; |
|
4662 }, |
|
4663 |
|
4664 /** |
|
4665 * Return whether the image must be cropped, based on required dimensions. |
|
4666 * |
|
4667 * @param {bool} flexW |
|
4668 * @param {bool} flexH |
|
4669 * @param {int} dstW |
|
4670 * @param {int} dstH |
|
4671 * @param {int} imgW |
|
4672 * @param {int} imgH |
|
4673 * @return {bool} |
|
4674 */ |
|
4675 mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) { |
|
4676 if ( true === flexW && true === flexH ) { |
|
4677 return false; |
|
4678 } |
|
4679 |
|
4680 if ( true === flexW && dstH === imgH ) { |
|
4681 return false; |
|
4682 } |
|
4683 |
|
4684 if ( true === flexH && dstW === imgW ) { |
|
4685 return false; |
|
4686 } |
|
4687 |
|
4688 if ( dstW === imgW && dstH === imgH ) { |
|
4689 return false; |
|
4690 } |
|
4691 |
|
4692 if ( imgW <= dstW ) { |
|
4693 return false; |
|
4694 } |
|
4695 |
|
4696 return true; |
|
4697 }, |
|
4698 |
|
4699 /** |
|
4700 * If cropping was skipped, apply the image data directly to the setting. |
|
4701 */ |
|
4702 onSkippedCrop: function() { |
|
4703 var attachment = this.frame.state().get( 'selection' ).first().toJSON(); |
|
4704 this.setImageFromAttachment( attachment ); |
|
4705 }, |
|
4706 |
|
4707 /** |
|
4708 * Updates the setting and re-renders the control UI. |
|
4709 * |
|
4710 * @param {object} attachment |
|
4711 */ |
|
4712 setImageFromAttachment: function( attachment ) { |
|
4713 this.params.attachment = attachment; |
|
4714 |
|
4715 // Set the Customizer setting; the callback takes care of rendering. |
|
4716 this.setting( attachment.id ); |
|
4717 } |
|
4718 }); |
|
4719 |
|
4720 /** |
|
4721 * A control for selecting and cropping Site Icons. |
|
4722 * |
|
4723 * @class |
|
4724 * @augments wp.customize.CroppedImageControl |
|
4725 * @augments wp.customize.MediaControl |
|
4726 * @augments wp.customize.Control |
|
4727 * @augments wp.customize.Class |
|
4728 */ |
|
4729 api.SiteIconControl = api.CroppedImageControl.extend({ |
|
4730 |
|
4731 /** |
|
4732 * Create a media modal select frame, and store it so the instance can be reused when needed. |
|
4733 */ |
|
4734 initFrame: function() { |
|
4735 var l10n = _wpMediaViewsL10n; |
|
4736 |
|
4737 this.frame = wp.media({ |
|
4738 button: { |
|
4739 text: l10n.select, |
|
4740 close: false |
|
4741 }, |
|
4742 states: [ |
|
4743 new wp.media.controller.Library({ |
|
4744 title: this.params.button_labels.frame_title, |
|
4745 library: wp.media.query({ type: 'image' }), |
|
4746 multiple: false, |
|
4747 date: false, |
|
4748 priority: 20, |
|
4749 suggestedWidth: this.params.width, |
|
4750 suggestedHeight: this.params.height |
|
4751 }), |
|
4752 new wp.media.controller.SiteIconCropper({ |
|
4753 imgSelectOptions: this.calculateImageSelectOptions, |
|
4754 control: this |
|
4755 }) |
|
4756 ] |
|
4757 }); |
|
4758 |
|
4759 this.frame.on( 'select', this.onSelect, this ); |
|
4760 this.frame.on( 'cropped', this.onCropped, this ); |
|
4761 this.frame.on( 'skippedcrop', this.onSkippedCrop, this ); |
|
4762 }, |
|
4763 |
|
4764 /** |
|
4765 * After an image is selected in the media modal, switch to the cropper |
|
4766 * state if the image isn't the right size. |
|
4767 */ |
|
4768 onSelect: function() { |
|
4769 var attachment = this.frame.state().get( 'selection' ).first().toJSON(), |
|
4770 controller = this; |
|
4771 |
|
4772 if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) { |
|
4773 wp.ajax.post( 'crop-image', { |
|
4774 nonce: attachment.nonces.edit, |
|
4775 id: attachment.id, |
|
4776 context: 'site-icon', |
|
4777 cropDetails: { |
|
4778 x1: 0, |
|
4779 y1: 0, |
|
4780 width: this.params.width, |
|
4781 height: this.params.height, |
|
4782 dst_width: this.params.width, |
|
4783 dst_height: this.params.height |
|
4784 } |
|
4785 } ).done( function( croppedImage ) { |
|
4786 controller.setImageFromAttachment( croppedImage ); |
|
4787 controller.frame.close(); |
|
4788 } ).fail( function() { |
|
4789 controller.frame.trigger('content:error:crop'); |
|
4790 } ); |
|
4791 } else { |
|
4792 this.frame.setState( 'cropper' ); |
|
4793 } |
|
4794 }, |
|
4795 |
|
4796 /** |
|
4797 * Updates the setting and re-renders the control UI. |
|
4798 * |
|
4799 * @param {object} attachment |
|
4800 */ |
|
4801 setImageFromAttachment: function( attachment ) { |
|
4802 var sizes = [ 'site_icon-32', 'thumbnail', 'full' ], link, |
|
4803 icon; |
|
4804 |
|
4805 _.each( sizes, function( size ) { |
|
4806 if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) { |
|
4807 icon = attachment.sizes[ size ]; |
|
4808 } |
|
4809 } ); |
|
4810 |
|
4811 this.params.attachment = attachment; |
|
4812 |
|
4813 // Set the Customizer setting; the callback takes care of rendering. |
|
4814 this.setting( attachment.id ); |
|
4815 |
|
4816 if ( ! icon ) { |
|
4817 return; |
|
4818 } |
|
4819 |
|
4820 // Update the icon in-browser. |
|
4821 link = $( 'link[rel="icon"][sizes="32x32"]' ); |
|
4822 link.attr( 'href', icon.url ); |
|
4823 }, |
|
4824 |
|
4825 /** |
|
4826 * Called when the "Remove" link is clicked. Empties the setting. |
|
4827 * |
|
4828 * @param {object} event jQuery Event object |
|
4829 */ |
|
4830 removeFile: function( event ) { |
|
4831 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
|
4832 return; |
|
4833 } |
|
4834 event.preventDefault(); |
|
4835 |
|
4836 this.params.attachment = {}; |
|
4837 this.setting( '' ); |
|
4838 this.renderContent(); // Not bound to setting change when emptying. |
|
4839 $( 'link[rel="icon"][sizes="32x32"]' ).attr( 'href', '/favicon.ico' ); // Set to default. |
1684 } |
4840 } |
1685 }); |
4841 }); |
1686 |
4842 |
1687 /** |
4843 /** |
1688 * @class |
4844 * @class |
1956 * @augments wp.customize.Class |
5116 * @augments wp.customize.Class |
1957 */ |
5117 */ |
1958 api.ThemeControl = api.Control.extend({ |
5118 api.ThemeControl = api.Control.extend({ |
1959 |
5119 |
1960 touchDrag: false, |
5120 touchDrag: false, |
1961 isRendered: false, |
5121 screenshotRendered: false, |
1962 |
5122 |
1963 /** |
5123 /** |
1964 * Defer rendering the theme control until the section is displayed. |
|
1965 * |
|
1966 * @since 4.2.0 |
5124 * @since 4.2.0 |
1967 */ |
5125 */ |
1968 renderContent: function () { |
|
1969 var control = this, |
|
1970 renderContentArgs = arguments; |
|
1971 |
|
1972 api.section( control.section(), function( section ) { |
|
1973 if ( section.expanded() ) { |
|
1974 api.Control.prototype.renderContent.apply( control, renderContentArgs ); |
|
1975 control.isRendered = true; |
|
1976 } else { |
|
1977 section.expanded.bind( function( expanded ) { |
|
1978 if ( expanded && ! control.isRendered ) { |
|
1979 api.Control.prototype.renderContent.apply( control, renderContentArgs ); |
|
1980 control.isRendered = true; |
|
1981 } |
|
1982 } ); |
|
1983 } |
|
1984 } ); |
|
1985 }, |
|
1986 |
|
1987 /** |
|
1988 * @since 4.2.0 |
|
1989 */ |
|
1990 ready: function() { |
5126 ready: function() { |
1991 var control = this; |
5127 var control = this, panel = api.panel( 'themes' ); |
|
5128 |
|
5129 function disableSwitchButtons() { |
|
5130 return ! panel.canSwitchTheme( control.params.theme.id ); |
|
5131 } |
|
5132 |
|
5133 // Temporary special function since supplying SFTP credentials does not work yet. See #42184. |
|
5134 function disableInstallButtons() { |
|
5135 return disableSwitchButtons() || false === api.settings.theme._canInstall || true === api.settings.theme._filesystemCredentialsNeeded; |
|
5136 } |
|
5137 function updateButtons() { |
|
5138 control.container.find( 'button.preview, button.preview-theme' ).toggleClass( 'disabled', disableSwitchButtons() ); |
|
5139 control.container.find( 'button.theme-install' ).toggleClass( 'disabled', disableInstallButtons() ); |
|
5140 } |
|
5141 |
|
5142 api.state( 'selectedChangesetStatus' ).bind( updateButtons ); |
|
5143 api.state( 'changesetStatus' ).bind( updateButtons ); |
|
5144 updateButtons(); |
1992 |
5145 |
1993 control.container.on( 'touchmove', '.theme', function() { |
5146 control.container.on( 'touchmove', '.theme', function() { |
1994 control.touchDrag = true; |
5147 control.touchDrag = true; |
1995 }); |
5148 }); |
1996 |
5149 |
1997 // Bind details view trigger. |
5150 // Bind details view trigger. |
1998 control.container.on( 'click keydown touchend', '.theme', function( event ) { |
5151 control.container.on( 'click keydown touchend', '.theme', function( event ) { |
|
5152 var section; |
1999 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
5153 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
2000 return; |
5154 return; |
2001 } |
5155 } |
2002 |
5156 |
2003 // Bail if the user scrolled on a touch device. |
5157 // Bail if the user scrolled on a touch device. |
2004 if ( control.touchDrag === true ) { |
5158 if ( control.touchDrag === true ) { |
2005 return control.touchDrag = false; |
5159 return control.touchDrag = false; |
2006 } |
5160 } |
2007 |
5161 |
2008 // Prevent the modal from showing when the user clicks the action button. |
5162 // Prevent the modal from showing when the user clicks the action button. |
2009 if ( $( event.target ).is( '.theme-actions .button' ) ) { |
5163 if ( $( event.target ).is( '.theme-actions .button, .update-theme' ) ) { |
2010 return; |
5164 return; |
2011 } |
5165 } |
2012 |
5166 |
2013 var previewUrl = $( this ).data( 'previewUrl' ); |
|
2014 |
|
2015 $( '.wp-full-overlay' ).addClass( 'customize-loading' ); |
|
2016 |
|
2017 window.parent.location = previewUrl; |
|
2018 }); |
|
2019 |
|
2020 control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) { |
|
2021 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
|
2022 return; |
|
2023 } |
|
2024 |
|
2025 event.preventDefault(); // Keep this AFTER the key filter above |
5167 event.preventDefault(); // Keep this AFTER the key filter above |
2026 |
5168 section = api.section( control.section() ); |
2027 api.section( control.section() ).showDetails( control.params.theme ); |
5169 section.showDetails( control.params.theme, function() { |
|
5170 |
|
5171 // Temporary special function since supplying SFTP credentials does not work yet. See #42184. |
|
5172 if ( api.settings.theme._filesystemCredentialsNeeded ) { |
|
5173 section.overlay.find( '.theme-actions .delete-theme' ).remove(); |
|
5174 } |
|
5175 } ); |
2028 }); |
5176 }); |
2029 |
5177 |
2030 control.container.on( 'render-screenshot', function() { |
5178 control.container.on( 'render-screenshot', function() { |
2031 var $screenshot = $( this ).find( 'img' ), |
5179 var $screenshot = $( this ).find( 'img' ), |
2032 source = $screenshot.data( 'src' ); |
5180 source = $screenshot.data( 'src' ); |
2033 |
5181 |
2034 if ( source ) { |
5182 if ( source ) { |
2035 $screenshot.attr( 'src', source ); |
5183 $screenshot.attr( 'src', source ); |
2036 } |
5184 } |
|
5185 control.screenshotRendered = true; |
2037 }); |
5186 }); |
2038 }, |
5187 }, |
2039 |
5188 |
2040 /** |
5189 /** |
2041 * Show or hide the theme based on the presence of the term in the title, description, and author. |
5190 * Show or hide the theme based on the presence of the term in the title, description, tags, and author. |
2042 * |
5191 * |
2043 * @since 4.2.0 |
5192 * @since 4.2.0 |
2044 */ |
5193 * @param {Array} terms - An array of terms to search for. |
2045 filter: function( term ) { |
5194 * @returns {boolean} Whether a theme control was activated or not. |
|
5195 */ |
|
5196 filter: function( terms ) { |
2046 var control = this, |
5197 var control = this, |
|
5198 matchCount = 0, |
2047 haystack = control.params.theme.name + ' ' + |
5199 haystack = control.params.theme.name + ' ' + |
2048 control.params.theme.description + ' ' + |
5200 control.params.theme.description + ' ' + |
2049 control.params.theme.tags + ' ' + |
5201 control.params.theme.tags + ' ' + |
2050 control.params.theme.author; |
5202 control.params.theme.author + ' '; |
2051 haystack = haystack.toLowerCase().replace( '-', ' ' ); |
5203 haystack = haystack.toLowerCase().replace( '-', ' ' ); |
2052 if ( -1 !== haystack.search( term ) ) { |
5204 |
|
5205 // Back-compat for behavior in WordPress 4.2.0 to 4.8.X. |
|
5206 if ( ! _.isArray( terms ) ) { |
|
5207 terms = [ terms ]; |
|
5208 } |
|
5209 |
|
5210 // Always give exact name matches highest ranking. |
|
5211 if ( control.params.theme.name.toLowerCase() === terms.join( ' ' ) ) { |
|
5212 matchCount = 100; |
|
5213 } else { |
|
5214 |
|
5215 // Search for and weight (by 10) complete term matches. |
|
5216 matchCount = matchCount + 10 * ( haystack.split( terms.join( ' ' ) ).length - 1 ); |
|
5217 |
|
5218 // Search for each term individually (as whole-word and partial match) and sum weighted match counts. |
|
5219 _.each( terms, function( term ) { |
|
5220 matchCount = matchCount + 2 * ( haystack.split( term + ' ' ).length - 1 ); // Whole-word, double-weighted. |
|
5221 matchCount = matchCount + haystack.split( term ).length - 1; // Partial word, to minimize empty intermediate searches while typing. |
|
5222 }); |
|
5223 |
|
5224 // Upper limit on match ranking. |
|
5225 if ( matchCount > 99 ) { |
|
5226 matchCount = 99; |
|
5227 } |
|
5228 } |
|
5229 |
|
5230 if ( 0 !== matchCount ) { |
2053 control.activate(); |
5231 control.activate(); |
|
5232 control.params.priority = 101 - matchCount; // Sort results by match count. |
|
5233 return true; |
2054 } else { |
5234 } else { |
2055 control.deactivate(); |
5235 control.deactivate(); // Hide control |
2056 } |
5236 control.params.priority = 101; |
|
5237 return false; |
|
5238 } |
|
5239 }, |
|
5240 |
|
5241 /** |
|
5242 * Rerender the theme from its JS template with the installed type. |
|
5243 * |
|
5244 * @since 4.9.0 |
|
5245 * |
|
5246 * @returns {void} |
|
5247 */ |
|
5248 rerenderAsInstalled: function( installed ) { |
|
5249 var control = this, section; |
|
5250 if ( installed ) { |
|
5251 control.params.theme.type = 'installed'; |
|
5252 } else { |
|
5253 section = api.section( control.params.section ); |
|
5254 control.params.theme.type = section.params.action; |
|
5255 } |
|
5256 control.renderContent(); // Replaces existing content. |
|
5257 control.container.trigger( 'render-screenshot' ); |
2057 } |
5258 } |
2058 }); |
5259 }); |
2059 |
5260 |
|
5261 /** |
|
5262 * Class wp.customize.CodeEditorControl |
|
5263 * |
|
5264 * @since 4.9.0 |
|
5265 * |
|
5266 * @constructor |
|
5267 * @augments wp.customize.Control |
|
5268 * @augments wp.customize.Class |
|
5269 */ |
|
5270 api.CodeEditorControl = api.Control.extend({ |
|
5271 |
|
5272 /** |
|
5273 * Initialize. |
|
5274 * |
|
5275 * @since 4.9.0 |
|
5276 * @param {string} id - Unique identifier for the control instance. |
|
5277 * @param {object} options - Options hash for the control instance. |
|
5278 * @returns {void} |
|
5279 */ |
|
5280 initialize: function( id, options ) { |
|
5281 var control = this; |
|
5282 control.deferred = _.extend( control.deferred || {}, { |
|
5283 codemirror: $.Deferred() |
|
5284 } ); |
|
5285 api.Control.prototype.initialize.call( control, id, options ); |
|
5286 |
|
5287 // Note that rendering is debounced so the props will be used when rendering happens after add event. |
|
5288 control.notifications.bind( 'add', function( notification ) { |
|
5289 |
|
5290 // Skip if control notification is not from setting csslint_error notification. |
|
5291 if ( notification.code !== control.setting.id + ':csslint_error' ) { |
|
5292 return; |
|
5293 } |
|
5294 |
|
5295 // Customize the template and behavior of csslint_error notifications. |
|
5296 notification.templateId = 'customize-code-editor-lint-error-notification'; |
|
5297 notification.render = (function( render ) { |
|
5298 return function() { |
|
5299 var li = render.call( this ); |
|
5300 li.find( 'input[type=checkbox]' ).on( 'click', function() { |
|
5301 control.setting.notifications.remove( 'csslint_error' ); |
|
5302 } ); |
|
5303 return li; |
|
5304 }; |
|
5305 })( notification.render ); |
|
5306 } ); |
|
5307 }, |
|
5308 |
|
5309 /** |
|
5310 * Initialize the editor when the containing section is ready and expanded. |
|
5311 * |
|
5312 * @since 4.9.0 |
|
5313 * @returns {void} |
|
5314 */ |
|
5315 ready: function() { |
|
5316 var control = this; |
|
5317 if ( ! control.section() ) { |
|
5318 control.initEditor(); |
|
5319 return; |
|
5320 } |
|
5321 |
|
5322 // Wait to initialize editor until section is embedded and expanded. |
|
5323 api.section( control.section(), function( section ) { |
|
5324 section.deferred.embedded.done( function() { |
|
5325 var onceExpanded; |
|
5326 if ( section.expanded() ) { |
|
5327 control.initEditor(); |
|
5328 } else { |
|
5329 onceExpanded = function( isExpanded ) { |
|
5330 if ( isExpanded ) { |
|
5331 control.initEditor(); |
|
5332 section.expanded.unbind( onceExpanded ); |
|
5333 } |
|
5334 }; |
|
5335 section.expanded.bind( onceExpanded ); |
|
5336 } |
|
5337 } ); |
|
5338 } ); |
|
5339 }, |
|
5340 |
|
5341 /** |
|
5342 * Initialize editor. |
|
5343 * |
|
5344 * @since 4.9.0 |
|
5345 * @returns {void} |
|
5346 */ |
|
5347 initEditor: function() { |
|
5348 var control = this, element, editorSettings = false; |
|
5349 |
|
5350 // Obtain editorSettings for instantiation. |
|
5351 if ( wp.codeEditor && ( _.isUndefined( control.params.editor_settings ) || false !== control.params.editor_settings ) ) { |
|
5352 |
|
5353 // Obtain default editor settings. |
|
5354 editorSettings = wp.codeEditor.defaultSettings ? _.clone( wp.codeEditor.defaultSettings ) : {}; |
|
5355 editorSettings.codemirror = _.extend( |
|
5356 {}, |
|
5357 editorSettings.codemirror, |
|
5358 { |
|
5359 indentUnit: 2, |
|
5360 tabSize: 2 |
|
5361 } |
|
5362 ); |
|
5363 |
|
5364 // Merge editor_settings param on top of defaults. |
|
5365 if ( _.isObject( control.params.editor_settings ) ) { |
|
5366 _.each( control.params.editor_settings, function( value, key ) { |
|
5367 if ( _.isObject( value ) ) { |
|
5368 editorSettings[ key ] = _.extend( |
|
5369 {}, |
|
5370 editorSettings[ key ], |
|
5371 value |
|
5372 ); |
|
5373 } |
|
5374 } ); |
|
5375 } |
|
5376 } |
|
5377 |
|
5378 element = new api.Element( control.container.find( 'textarea' ) ); |
|
5379 control.elements.push( element ); |
|
5380 element.sync( control.setting ); |
|
5381 element.set( control.setting() ); |
|
5382 |
|
5383 if ( editorSettings ) { |
|
5384 control.initSyntaxHighlightingEditor( editorSettings ); |
|
5385 } else { |
|
5386 control.initPlainTextareaEditor(); |
|
5387 } |
|
5388 }, |
|
5389 |
|
5390 /** |
|
5391 * Make sure editor gets focused when control is focused. |
|
5392 * |
|
5393 * @since 4.9.0 |
|
5394 * @param {Object} [params] - Focus params. |
|
5395 * @param {Function} [params.completeCallback] - Function to call when expansion is complete. |
|
5396 * @returns {void} |
|
5397 */ |
|
5398 focus: function( params ) { |
|
5399 var control = this, extendedParams = _.extend( {}, params ), originalCompleteCallback; |
|
5400 originalCompleteCallback = extendedParams.completeCallback; |
|
5401 extendedParams.completeCallback = function() { |
|
5402 if ( originalCompleteCallback ) { |
|
5403 originalCompleteCallback(); |
|
5404 } |
|
5405 if ( control.editor ) { |
|
5406 control.editor.codemirror.focus(); |
|
5407 } |
|
5408 }; |
|
5409 api.Control.prototype.focus.call( control, extendedParams ); |
|
5410 }, |
|
5411 |
|
5412 /** |
|
5413 * Initialize syntax-highlighting editor. |
|
5414 * |
|
5415 * @since 4.9.0 |
|
5416 * @param {object} codeEditorSettings - Code editor settings. |
|
5417 * @returns {void} |
|
5418 */ |
|
5419 initSyntaxHighlightingEditor: function( codeEditorSettings ) { |
|
5420 var control = this, $textarea = control.container.find( 'textarea' ), settings, suspendEditorUpdate = false; |
|
5421 |
|
5422 settings = _.extend( {}, codeEditorSettings, { |
|
5423 onTabNext: _.bind( control.onTabNext, control ), |
|
5424 onTabPrevious: _.bind( control.onTabPrevious, control ), |
|
5425 onUpdateErrorNotice: _.bind( control.onUpdateErrorNotice, control ) |
|
5426 }); |
|
5427 |
|
5428 control.editor = wp.codeEditor.initialize( $textarea, settings ); |
|
5429 |
|
5430 // Improve the editor accessibility. |
|
5431 $( control.editor.codemirror.display.lineDiv ) |
|
5432 .attr({ |
|
5433 role: 'textbox', |
|
5434 'aria-multiline': 'true', |
|
5435 'aria-label': control.params.label, |
|
5436 'aria-describedby': 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4' |
|
5437 }); |
|
5438 |
|
5439 // Focus the editor when clicking on its label. |
|
5440 control.container.find( 'label' ).on( 'click', function() { |
|
5441 control.editor.codemirror.focus(); |
|
5442 }); |
|
5443 |
|
5444 /* |
|
5445 * When the CodeMirror instance changes, mirror to the textarea, |
|
5446 * where we have our "true" change event handler bound. |
|
5447 */ |
|
5448 control.editor.codemirror.on( 'change', function( codemirror ) { |
|
5449 suspendEditorUpdate = true; |
|
5450 $textarea.val( codemirror.getValue() ).trigger( 'change' ); |
|
5451 suspendEditorUpdate = false; |
|
5452 }); |
|
5453 |
|
5454 // Update CodeMirror when the setting is changed by another plugin. |
|
5455 control.setting.bind( function( value ) { |
|
5456 if ( ! suspendEditorUpdate ) { |
|
5457 control.editor.codemirror.setValue( value ); |
|
5458 } |
|
5459 }); |
|
5460 |
|
5461 // Prevent collapsing section when hitting Esc to tab out of editor. |
|
5462 control.editor.codemirror.on( 'keydown', function onKeydown( codemirror, event ) { |
|
5463 var escKeyCode = 27; |
|
5464 if ( escKeyCode === event.keyCode ) { |
|
5465 event.stopPropagation(); |
|
5466 } |
|
5467 }); |
|
5468 |
|
5469 control.deferred.codemirror.resolveWith( control, [ control.editor.codemirror ] ); |
|
5470 }, |
|
5471 |
|
5472 /** |
|
5473 * Handle tabbing to the field after the editor. |
|
5474 * |
|
5475 * @since 4.9.0 |
|
5476 * @returns {void} |
|
5477 */ |
|
5478 onTabNext: function onTabNext() { |
|
5479 var control = this, controls, controlIndex, section; |
|
5480 section = api.section( control.section() ); |
|
5481 controls = section.controls(); |
|
5482 controlIndex = controls.indexOf( control ); |
|
5483 if ( controls.length === controlIndex + 1 ) { |
|
5484 $( '#customize-footer-actions .collapse-sidebar' ).focus(); |
|
5485 } else { |
|
5486 controls[ controlIndex + 1 ].container.find( ':focusable:first' ).focus(); |
|
5487 } |
|
5488 }, |
|
5489 |
|
5490 /** |
|
5491 * Handle tabbing to the field before the editor. |
|
5492 * |
|
5493 * @since 4.9.0 |
|
5494 * @returns {void} |
|
5495 */ |
|
5496 onTabPrevious: function onTabPrevious() { |
|
5497 var control = this, controls, controlIndex, section; |
|
5498 section = api.section( control.section() ); |
|
5499 controls = section.controls(); |
|
5500 controlIndex = controls.indexOf( control ); |
|
5501 if ( 0 === controlIndex ) { |
|
5502 section.contentContainer.find( '.customize-section-title .customize-help-toggle, .customize-section-title .customize-section-description.open .section-description-close' ).last().focus(); |
|
5503 } else { |
|
5504 controls[ controlIndex - 1 ].contentContainer.find( ':focusable:first' ).focus(); |
|
5505 } |
|
5506 }, |
|
5507 |
|
5508 /** |
|
5509 * Update error notice. |
|
5510 * |
|
5511 * @since 4.9.0 |
|
5512 * @param {Array} errorAnnotations - Error annotations. |
|
5513 * @returns {void} |
|
5514 */ |
|
5515 onUpdateErrorNotice: function onUpdateErrorNotice( errorAnnotations ) { |
|
5516 var control = this, message; |
|
5517 control.setting.notifications.remove( 'csslint_error' ); |
|
5518 |
|
5519 if ( 0 !== errorAnnotations.length ) { |
|
5520 if ( 1 === errorAnnotations.length ) { |
|
5521 message = api.l10n.customCssError.singular.replace( '%d', '1' ); |
|
5522 } else { |
|
5523 message = api.l10n.customCssError.plural.replace( '%d', String( errorAnnotations.length ) ); |
|
5524 } |
|
5525 control.setting.notifications.add( new api.Notification( 'csslint_error', { |
|
5526 message: message, |
|
5527 type: 'error' |
|
5528 } ) ); |
|
5529 } |
|
5530 }, |
|
5531 |
|
5532 /** |
|
5533 * Initialize plain-textarea editor when syntax highlighting is disabled. |
|
5534 * |
|
5535 * @since 4.9.0 |
|
5536 * @returns {void} |
|
5537 */ |
|
5538 initPlainTextareaEditor: function() { |
|
5539 var control = this, $textarea = control.container.find( 'textarea' ), textarea = $textarea[0]; |
|
5540 |
|
5541 $textarea.on( 'blur', function onBlur() { |
|
5542 $textarea.data( 'next-tab-blurs', false ); |
|
5543 } ); |
|
5544 |
|
5545 $textarea.on( 'keydown', function onKeydown( event ) { |
|
5546 var selectionStart, selectionEnd, value, tabKeyCode = 9, escKeyCode = 27; |
|
5547 |
|
5548 if ( escKeyCode === event.keyCode ) { |
|
5549 if ( ! $textarea.data( 'next-tab-blurs' ) ) { |
|
5550 $textarea.data( 'next-tab-blurs', true ); |
|
5551 event.stopPropagation(); // Prevent collapsing the section. |
|
5552 } |
|
5553 return; |
|
5554 } |
|
5555 |
|
5556 // Short-circuit if tab key is not being pressed or if a modifier key *is* being pressed. |
|
5557 if ( tabKeyCode !== event.keyCode || event.ctrlKey || event.altKey || event.shiftKey ) { |
|
5558 return; |
|
5559 } |
|
5560 |
|
5561 // Prevent capturing Tab characters if Esc was pressed. |
|
5562 if ( $textarea.data( 'next-tab-blurs' ) ) { |
|
5563 return; |
|
5564 } |
|
5565 |
|
5566 selectionStart = textarea.selectionStart; |
|
5567 selectionEnd = textarea.selectionEnd; |
|
5568 value = textarea.value; |
|
5569 |
|
5570 if ( selectionStart >= 0 ) { |
|
5571 textarea.value = value.substring( 0, selectionStart ).concat( '\t', value.substring( selectionEnd ) ); |
|
5572 $textarea.selectionStart = textarea.selectionEnd = selectionStart + 1; |
|
5573 } |
|
5574 |
|
5575 event.stopPropagation(); |
|
5576 event.preventDefault(); |
|
5577 }); |
|
5578 |
|
5579 control.deferred.codemirror.rejectWith( control ); |
|
5580 } |
|
5581 }); |
|
5582 |
|
5583 /** |
|
5584 * Class wp.customize.DateTimeControl. |
|
5585 * |
|
5586 * @since 4.9.0 |
|
5587 * @constructor |
|
5588 * @augments wp.customize.Control |
|
5589 * @augments wp.customize.Class |
|
5590 */ |
|
5591 api.DateTimeControl = api.Control.extend({ |
|
5592 |
|
5593 /** |
|
5594 * Initialize behaviors. |
|
5595 * |
|
5596 * @since 4.9.0 |
|
5597 * @returns {void} |
|
5598 */ |
|
5599 ready: function ready() { |
|
5600 var control = this; |
|
5601 |
|
5602 control.inputElements = {}; |
|
5603 control.invalidDate = false; |
|
5604 |
|
5605 _.bindAll( control, 'populateSetting', 'updateDaysForMonth', 'populateDateInputs' ); |
|
5606 |
|
5607 if ( ! control.setting ) { |
|
5608 throw new Error( 'Missing setting' ); |
|
5609 } |
|
5610 |
|
5611 control.container.find( '.date-input' ).each( function() { |
|
5612 var input = $( this ), component, element; |
|
5613 component = input.data( 'component' ); |
|
5614 element = new api.Element( input ); |
|
5615 control.inputElements[ component ] = element; |
|
5616 control.elements.push( element ); |
|
5617 |
|
5618 // Add invalid date error once user changes (and has blurred the input). |
|
5619 input.on( 'change', function() { |
|
5620 if ( control.invalidDate ) { |
|
5621 control.notifications.add( new api.Notification( 'invalid_date', { |
|
5622 message: api.l10n.invalidDate |
|
5623 } ) ); |
|
5624 } |
|
5625 } ); |
|
5626 |
|
5627 // Remove the error immediately after validity change. |
|
5628 input.on( 'input', _.debounce( function() { |
|
5629 if ( ! control.invalidDate ) { |
|
5630 control.notifications.remove( 'invalid_date' ); |
|
5631 } |
|
5632 } ) ); |
|
5633 |
|
5634 // Add zero-padding when blurring field. |
|
5635 input.on( 'blur', _.debounce( function() { |
|
5636 if ( ! control.invalidDate ) { |
|
5637 control.populateDateInputs(); |
|
5638 } |
|
5639 } ) ); |
|
5640 } ); |
|
5641 |
|
5642 control.inputElements.month.bind( control.updateDaysForMonth ); |
|
5643 control.inputElements.year.bind( control.updateDaysForMonth ); |
|
5644 control.populateDateInputs(); |
|
5645 control.setting.bind( control.populateDateInputs ); |
|
5646 |
|
5647 // Start populating setting after inputs have been populated. |
|
5648 _.each( control.inputElements, function( element ) { |
|
5649 element.bind( control.populateSetting ); |
|
5650 } ); |
|
5651 }, |
|
5652 |
|
5653 /** |
|
5654 * Parse datetime string. |
|
5655 * |
|
5656 * @since 4.9.0 |
|
5657 * |
|
5658 * @param {string} datetime - Date/Time string. Accepts Y-m-d[ H:i[:s]] format. |
|
5659 * @returns {object|null} Returns object containing date components or null if parse error. |
|
5660 */ |
|
5661 parseDateTime: function parseDateTime( datetime ) { |
|
5662 var control = this, matches, date, midDayHour = 12; |
|
5663 |
|
5664 if ( datetime ) { |
|
5665 matches = datetime.match( /^(\d\d\d\d)-(\d\d)-(\d\d)(?: (\d\d):(\d\d)(?::(\d\d))?)?$/ ); |
|
5666 } |
|
5667 |
|
5668 if ( ! matches ) { |
|
5669 return null; |
|
5670 } |
|
5671 |
|
5672 matches.shift(); |
|
5673 |
|
5674 date = { |
|
5675 year: matches.shift(), |
|
5676 month: matches.shift(), |
|
5677 day: matches.shift(), |
|
5678 hour: matches.shift() || '00', |
|
5679 minute: matches.shift() || '00', |
|
5680 second: matches.shift() || '00' |
|
5681 }; |
|
5682 |
|
5683 if ( control.params.includeTime && control.params.twelveHourFormat ) { |
|
5684 date.hour = parseInt( date.hour, 10 ); |
|
5685 date.meridian = date.hour >= midDayHour ? 'pm' : 'am'; |
|
5686 date.hour = date.hour % midDayHour ? String( date.hour % midDayHour ) : String( midDayHour ); |
|
5687 delete date.second; // @todo Why only if twelveHourFormat? |
|
5688 } |
|
5689 |
|
5690 return date; |
|
5691 }, |
|
5692 |
|
5693 /** |
|
5694 * Validates if input components have valid date and time. |
|
5695 * |
|
5696 * @since 4.9.0 |
|
5697 * @return {boolean} If date input fields has error. |
|
5698 */ |
|
5699 validateInputs: function validateInputs() { |
|
5700 var control = this, components, validityInput; |
|
5701 |
|
5702 control.invalidDate = false; |
|
5703 |
|
5704 components = [ 'year', 'day' ]; |
|
5705 if ( control.params.includeTime ) { |
|
5706 components.push( 'hour', 'minute' ); |
|
5707 } |
|
5708 |
|
5709 _.find( components, function( component ) { |
|
5710 var element, max, min, value; |
|
5711 |
|
5712 element = control.inputElements[ component ]; |
|
5713 validityInput = element.element.get( 0 ); |
|
5714 max = parseInt( element.element.attr( 'max' ), 10 ); |
|
5715 min = parseInt( element.element.attr( 'min' ), 10 ); |
|
5716 value = parseInt( element(), 10 ); |
|
5717 control.invalidDate = isNaN( value ) || value > max || value < min; |
|
5718 |
|
5719 if ( ! control.invalidDate ) { |
|
5720 validityInput.setCustomValidity( '' ); |
|
5721 } |
|
5722 |
|
5723 return control.invalidDate; |
|
5724 } ); |
|
5725 |
|
5726 if ( control.inputElements.meridian && ! control.invalidDate ) { |
|
5727 validityInput = control.inputElements.meridian.element.get( 0 ); |
|
5728 if ( 'am' !== control.inputElements.meridian.get() && 'pm' !== control.inputElements.meridian.get() ) { |
|
5729 control.invalidDate = true; |
|
5730 } else { |
|
5731 validityInput.setCustomValidity( '' ); |
|
5732 } |
|
5733 } |
|
5734 |
|
5735 if ( control.invalidDate ) { |
|
5736 validityInput.setCustomValidity( api.l10n.invalidValue ); |
|
5737 } else { |
|
5738 validityInput.setCustomValidity( '' ); |
|
5739 } |
|
5740 if ( ! control.section() || api.section.has( control.section() ) && api.section( control.section() ).expanded() ) { |
|
5741 _.result( validityInput, 'reportValidity' ); |
|
5742 } |
|
5743 |
|
5744 return control.invalidDate; |
|
5745 }, |
|
5746 |
|
5747 /** |
|
5748 * Updates number of days according to the month and year selected. |
|
5749 * |
|
5750 * @since 4.9.0 |
|
5751 * @return {void} |
|
5752 */ |
|
5753 updateDaysForMonth: function updateDaysForMonth() { |
|
5754 var control = this, daysInMonth, year, month, day; |
|
5755 |
|
5756 month = parseInt( control.inputElements.month(), 10 ); |
|
5757 year = parseInt( control.inputElements.year(), 10 ); |
|
5758 day = parseInt( control.inputElements.day(), 10 ); |
|
5759 |
|
5760 if ( month && year ) { |
|
5761 daysInMonth = new Date( year, month, 0 ).getDate(); |
|
5762 control.inputElements.day.element.attr( 'max', daysInMonth ); |
|
5763 |
|
5764 if ( day > daysInMonth ) { |
|
5765 control.inputElements.day( String( daysInMonth ) ); |
|
5766 } |
|
5767 } |
|
5768 }, |
|
5769 |
|
5770 /** |
|
5771 * Populate setting value from the inputs. |
|
5772 * |
|
5773 * @since 4.9.0 |
|
5774 * @returns {boolean} If setting updated. |
|
5775 */ |
|
5776 populateSetting: function populateSetting() { |
|
5777 var control = this, date; |
|
5778 |
|
5779 if ( control.validateInputs() || ! control.params.allowPastDate && ! control.isFutureDate() ) { |
|
5780 return false; |
|
5781 } |
|
5782 |
|
5783 date = control.convertInputDateToString(); |
|
5784 control.setting.set( date ); |
|
5785 return true; |
|
5786 }, |
|
5787 |
|
5788 /** |
|
5789 * Converts input values to string in Y-m-d H:i:s format. |
|
5790 * |
|
5791 * @since 4.9.0 |
|
5792 * @return {string} Date string. |
|
5793 */ |
|
5794 convertInputDateToString: function convertInputDateToString() { |
|
5795 var control = this, date = '', dateFormat, hourInTwentyFourHourFormat, |
|
5796 getElementValue, pad; |
|
5797 |
|
5798 pad = function( number, padding ) { |
|
5799 var zeros; |
|
5800 if ( String( number ).length < padding ) { |
|
5801 zeros = padding - String( number ).length; |
|
5802 number = Math.pow( 10, zeros ).toString().substr( 1 ) + String( number ); |
|
5803 } |
|
5804 return number; |
|
5805 }; |
|
5806 |
|
5807 getElementValue = function( component ) { |
|
5808 var value = parseInt( control.inputElements[ component ].get(), 10 ); |
|
5809 |
|
5810 if ( _.contains( [ 'month', 'day', 'hour', 'minute' ], component ) ) { |
|
5811 value = pad( value, 2 ); |
|
5812 } else if ( 'year' === component ) { |
|
5813 value = pad( value, 4 ); |
|
5814 } |
|
5815 return value; |
|
5816 }; |
|
5817 |
|
5818 dateFormat = [ 'year', '-', 'month', '-', 'day' ]; |
|
5819 if ( control.params.includeTime ) { |
|
5820 hourInTwentyFourHourFormat = control.inputElements.meridian ? control.convertHourToTwentyFourHourFormat( control.inputElements.hour(), control.inputElements.meridian() ) : control.inputElements.hour(); |
|
5821 dateFormat = dateFormat.concat( [ ' ', pad( hourInTwentyFourHourFormat, 2 ), ':', 'minute', ':', '00' ] ); |
|
5822 } |
|
5823 |
|
5824 _.each( dateFormat, function( component ) { |
|
5825 date += control.inputElements[ component ] ? getElementValue( component ) : component; |
|
5826 } ); |
|
5827 |
|
5828 return date; |
|
5829 }, |
|
5830 |
|
5831 /** |
|
5832 * Check if the date is in the future. |
|
5833 * |
|
5834 * @since 4.9.0 |
|
5835 * @returns {boolean} True if future date. |
|
5836 */ |
|
5837 isFutureDate: function isFutureDate() { |
|
5838 var control = this; |
|
5839 return 0 < api.utils.getRemainingTime( control.convertInputDateToString() ); |
|
5840 }, |
|
5841 |
|
5842 /** |
|
5843 * Convert hour in twelve hour format to twenty four hour format. |
|
5844 * |
|
5845 * @since 4.9.0 |
|
5846 * @param {string} hourInTwelveHourFormat - Hour in twelve hour format. |
|
5847 * @param {string} meridian - Either 'am' or 'pm'. |
|
5848 * @returns {string} Hour in twenty four hour format. |
|
5849 */ |
|
5850 convertHourToTwentyFourHourFormat: function convertHour( hourInTwelveHourFormat, meridian ) { |
|
5851 var hourInTwentyFourHourFormat, hour, midDayHour = 12; |
|
5852 |
|
5853 hour = parseInt( hourInTwelveHourFormat, 10 ); |
|
5854 if ( isNaN( hour ) ) { |
|
5855 return ''; |
|
5856 } |
|
5857 |
|
5858 if ( 'pm' === meridian && hour < midDayHour ) { |
|
5859 hourInTwentyFourHourFormat = hour + midDayHour; |
|
5860 } else if ( 'am' === meridian && midDayHour === hour ) { |
|
5861 hourInTwentyFourHourFormat = hour - midDayHour; |
|
5862 } else { |
|
5863 hourInTwentyFourHourFormat = hour; |
|
5864 } |
|
5865 |
|
5866 return String( hourInTwentyFourHourFormat ); |
|
5867 }, |
|
5868 |
|
5869 /** |
|
5870 * Populates date inputs in date fields. |
|
5871 * |
|
5872 * @since 4.9.0 |
|
5873 * @returns {boolean} Whether the inputs were populated. |
|
5874 */ |
|
5875 populateDateInputs: function populateDateInputs() { |
|
5876 var control = this, parsed; |
|
5877 |
|
5878 parsed = control.parseDateTime( control.setting.get() ); |
|
5879 |
|
5880 if ( ! parsed ) { |
|
5881 return false; |
|
5882 } |
|
5883 |
|
5884 _.each( control.inputElements, function( element, component ) { |
|
5885 var value = parsed[ component ]; // This will be zero-padded string. |
|
5886 |
|
5887 // Set month and meridian regardless of focused state since they are dropdowns. |
|
5888 if ( 'month' === component || 'meridian' === component ) { |
|
5889 |
|
5890 // Options in dropdowns are not zero-padded. |
|
5891 value = value.replace( /^0/, '' ); |
|
5892 |
|
5893 element.set( value ); |
|
5894 } else { |
|
5895 |
|
5896 value = parseInt( value, 10 ); |
|
5897 if ( ! element.element.is( document.activeElement ) ) { |
|
5898 |
|
5899 // Populate element with zero-padded value if not focused. |
|
5900 element.set( parsed[ component ] ); |
|
5901 } else if ( value !== parseInt( element(), 10 ) ) { |
|
5902 |
|
5903 // Forcibly update the value if its underlying value changed, regardless of zero-padding. |
|
5904 element.set( String( value ) ); |
|
5905 } |
|
5906 } |
|
5907 } ); |
|
5908 |
|
5909 return true; |
|
5910 }, |
|
5911 |
|
5912 /** |
|
5913 * Toggle future date notification for date control. |
|
5914 * |
|
5915 * @since 4.9.0 |
|
5916 * @param {boolean} notify Add or remove the notification. |
|
5917 * @return {wp.customize.DateTimeControl} |
|
5918 */ |
|
5919 toggleFutureDateNotification: function toggleFutureDateNotification( notify ) { |
|
5920 var control = this, notificationCode, notification; |
|
5921 |
|
5922 notificationCode = 'not_future_date'; |
|
5923 |
|
5924 if ( notify ) { |
|
5925 notification = new api.Notification( notificationCode, { |
|
5926 type: 'error', |
|
5927 message: api.l10n.futureDateError |
|
5928 } ); |
|
5929 control.notifications.add( notification ); |
|
5930 } else { |
|
5931 control.notifications.remove( notificationCode ); |
|
5932 } |
|
5933 |
|
5934 return control; |
|
5935 } |
|
5936 }); |
|
5937 |
|
5938 /** |
|
5939 * Class PreviewLinkControl. |
|
5940 * |
|
5941 * @since 4.9.0 |
|
5942 * @constructor |
|
5943 * @augments wp.customize.Control |
|
5944 * @augments wp.customize.Class |
|
5945 */ |
|
5946 api.PreviewLinkControl = api.Control.extend({ |
|
5947 |
|
5948 defaults: _.extend( {}, api.Control.prototype.defaults, { |
|
5949 templateId: 'customize-preview-link-control' |
|
5950 } ), |
|
5951 |
|
5952 /** |
|
5953 * Initialize behaviors. |
|
5954 * |
|
5955 * @since 4.9.0 |
|
5956 * @returns {void} |
|
5957 */ |
|
5958 ready: function ready() { |
|
5959 var control = this, element, component, node, url, input, button; |
|
5960 |
|
5961 _.bindAll( control, 'updatePreviewLink' ); |
|
5962 |
|
5963 if ( ! control.setting ) { |
|
5964 control.setting = new api.Value(); |
|
5965 } |
|
5966 |
|
5967 control.previewElements = {}; |
|
5968 |
|
5969 control.container.find( '.preview-control-element' ).each( function() { |
|
5970 node = $( this ); |
|
5971 component = node.data( 'component' ); |
|
5972 element = new api.Element( node ); |
|
5973 control.previewElements[ component ] = element; |
|
5974 control.elements.push( element ); |
|
5975 } ); |
|
5976 |
|
5977 url = control.previewElements.url; |
|
5978 input = control.previewElements.input; |
|
5979 button = control.previewElements.button; |
|
5980 |
|
5981 input.link( control.setting ); |
|
5982 url.link( control.setting ); |
|
5983 |
|
5984 url.bind( function( value ) { |
|
5985 url.element.parent().attr( { |
|
5986 href: value, |
|
5987 target: api.settings.changeset.uuid |
|
5988 } ); |
|
5989 } ); |
|
5990 |
|
5991 api.bind( 'ready', control.updatePreviewLink ); |
|
5992 api.state( 'saved' ).bind( control.updatePreviewLink ); |
|
5993 api.state( 'changesetStatus' ).bind( control.updatePreviewLink ); |
|
5994 api.state( 'activated' ).bind( control.updatePreviewLink ); |
|
5995 api.previewer.previewUrl.bind( control.updatePreviewLink ); |
|
5996 |
|
5997 button.element.on( 'click', function( event ) { |
|
5998 event.preventDefault(); |
|
5999 if ( control.setting() ) { |
|
6000 input.element.select(); |
|
6001 document.execCommand( 'copy' ); |
|
6002 button( button.element.data( 'copied-text' ) ); |
|
6003 } |
|
6004 } ); |
|
6005 |
|
6006 url.element.parent().on( 'click', function( event ) { |
|
6007 if ( $( this ).hasClass( 'disabled' ) ) { |
|
6008 event.preventDefault(); |
|
6009 } |
|
6010 } ); |
|
6011 |
|
6012 button.element.on( 'mouseenter', function() { |
|
6013 if ( control.setting() ) { |
|
6014 button( button.element.data( 'copy-text' ) ); |
|
6015 } |
|
6016 } ); |
|
6017 }, |
|
6018 |
|
6019 /** |
|
6020 * Updates Preview Link |
|
6021 * |
|
6022 * @since 4.9.0 |
|
6023 * @return {void} |
|
6024 */ |
|
6025 updatePreviewLink: function updatePreviewLink() { |
|
6026 var control = this, unsavedDirtyValues; |
|
6027 |
|
6028 unsavedDirtyValues = ! api.state( 'saved' ).get() || '' === api.state( 'changesetStatus' ).get() || 'auto-draft' === api.state( 'changesetStatus' ).get(); |
|
6029 |
|
6030 control.toggleSaveNotification( unsavedDirtyValues ); |
|
6031 control.previewElements.url.element.parent().toggleClass( 'disabled', unsavedDirtyValues ); |
|
6032 control.previewElements.button.element.prop( 'disabled', unsavedDirtyValues ); |
|
6033 control.setting.set( api.previewer.getFrontendPreviewUrl() ); |
|
6034 }, |
|
6035 |
|
6036 /** |
|
6037 * Toggles save notification. |
|
6038 * |
|
6039 * @since 4.9.0 |
|
6040 * @param {boolean} notify Add or remove notification. |
|
6041 * @return {void} |
|
6042 */ |
|
6043 toggleSaveNotification: function toggleSaveNotification( notify ) { |
|
6044 var control = this, notificationCode, notification; |
|
6045 |
|
6046 notificationCode = 'changes_not_saved'; |
|
6047 |
|
6048 if ( notify ) { |
|
6049 notification = new api.Notification( notificationCode, { |
|
6050 type: 'info', |
|
6051 message: api.l10n.saveBeforeShare |
|
6052 } ); |
|
6053 control.notifications.add( notification ); |
|
6054 } else { |
|
6055 control.notifications.remove( notificationCode ); |
|
6056 } |
|
6057 } |
|
6058 }); |
|
6059 |
2060 // Change objects contained within the main customize object to Settings. |
6060 // Change objects contained within the main customize object to Settings. |
2061 api.defaultConstructor = api.Setting; |
6061 api.defaultConstructor = api.Setting; |
2062 |
6062 |
2063 // Create the collections for Controls, Sections and Panels. |
6063 /** |
|
6064 * Callback for resolved controls. |
|
6065 * |
|
6066 * @callback deferredControlsCallback |
|
6067 * @param {wp.customize.Control[]} Resolved controls. |
|
6068 */ |
|
6069 |
|
6070 /** |
|
6071 * Collection of all registered controls. |
|
6072 * |
|
6073 * @since 3.4.0 |
|
6074 * |
|
6075 * @type {Function} |
|
6076 * @param {...string} ids - One or more ids for controls to obtain. |
|
6077 * @param {deferredControlsCallback} [callback] - Function called when all supplied controls exist. |
|
6078 * @returns {wp.customize.Control|undefined|jQuery.promise} Control instance or undefined (if function called with one id param), or promise resolving to requested controls. |
|
6079 * |
|
6080 * @example <caption>Loop over all registered controls.</caption> |
|
6081 * wp.customize.control.each( function( control ) { ... } ); |
|
6082 * |
|
6083 * @example <caption>Getting `background_color` control instance.</caption> |
|
6084 * control = wp.customize.control( 'background_color' ); |
|
6085 * |
|
6086 * @example <caption>Check if control exists.</caption> |
|
6087 * hasControl = wp.customize.control.has( 'background_color' ); |
|
6088 * |
|
6089 * @example <caption>Deferred getting of `background_color` control until it exists, using callback.</caption> |
|
6090 * wp.customize.control( 'background_color', function( control ) { ... } ); |
|
6091 * |
|
6092 * @example <caption>Get title and tagline controls when they both exist, using promise (only available when multiple IDs are present).</caption> |
|
6093 * promise = wp.customize.control( 'blogname', 'blogdescription' ); |
|
6094 * promise.done( function( titleControl, taglineControl ) { ... } ); |
|
6095 * |
|
6096 * @example <caption>Get title and tagline controls when they both exist, using callback.</caption> |
|
6097 * wp.customize.control( 'blogname', 'blogdescription', function( titleControl, taglineControl ) { ... } ); |
|
6098 * |
|
6099 * @example <caption>Getting setting value for `background_color` control.</caption> |
|
6100 * value = wp.customize.control( 'background_color ').setting.get(); |
|
6101 * value = wp.customize( 'background_color' ).get(); // Same as above, since setting ID and control ID are the same. |
|
6102 * |
|
6103 * @example <caption>Add new control for site title.</caption> |
|
6104 * wp.customize.control.add( new wp.customize.Control( 'other_blogname', { |
|
6105 * setting: 'blogname', |
|
6106 * type: 'text', |
|
6107 * label: 'Site title', |
|
6108 * section: 'other_site_identify' |
|
6109 * } ) ); |
|
6110 * |
|
6111 * @example <caption>Remove control.</caption> |
|
6112 * wp.customize.control.remove( 'other_blogname' ); |
|
6113 * |
|
6114 * @example <caption>Listen for control being added.</caption> |
|
6115 * wp.customize.control.bind( 'add', function( addedControl ) { ... } ) |
|
6116 * |
|
6117 * @example <caption>Listen for control being removed.</caption> |
|
6118 * wp.customize.control.bind( 'removed', function( removedControl ) { ... } ) |
|
6119 */ |
2064 api.control = new api.Values({ defaultConstructor: api.Control }); |
6120 api.control = new api.Values({ defaultConstructor: api.Control }); |
|
6121 |
|
6122 /** |
|
6123 * Callback for resolved sections. |
|
6124 * |
|
6125 * @callback deferredSectionsCallback |
|
6126 * @param {wp.customize.Section[]} Resolved sections. |
|
6127 */ |
|
6128 |
|
6129 /** |
|
6130 * Collection of all registered sections. |
|
6131 * |
|
6132 * @since 3.4.0 |
|
6133 * |
|
6134 * @type {Function} |
|
6135 * @param {...string} ids - One or more ids for sections to obtain. |
|
6136 * @param {deferredSectionsCallback} [callback] - Function called when all supplied sections exist. |
|
6137 * @returns {wp.customize.Section|undefined|jQuery.promise} Section instance or undefined (if function called with one id param), or promise resolving to requested sections. |
|
6138 * |
|
6139 * @example <caption>Loop over all registered sections.</caption> |
|
6140 * wp.customize.section.each( function( section ) { ... } ) |
|
6141 * |
|
6142 * @example <caption>Getting `title_tagline` section instance.</caption> |
|
6143 * section = wp.customize.section( 'title_tagline' ) |
|
6144 * |
|
6145 * @example <caption>Expand dynamically-created section when it exists.</caption> |
|
6146 * wp.customize.section( 'dynamically_created', function( section ) { |
|
6147 * section.expand(); |
|
6148 * } ); |
|
6149 * |
|
6150 * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances. |
|
6151 */ |
2065 api.section = new api.Values({ defaultConstructor: api.Section }); |
6152 api.section = new api.Values({ defaultConstructor: api.Section }); |
|
6153 |
|
6154 /** |
|
6155 * Callback for resolved panels. |
|
6156 * |
|
6157 * @callback deferredPanelsCallback |
|
6158 * @param {wp.customize.Panel[]} Resolved panels. |
|
6159 */ |
|
6160 |
|
6161 /** |
|
6162 * Collection of all registered panels. |
|
6163 * |
|
6164 * @since 4.0.0 |
|
6165 * |
|
6166 * @type {Function} |
|
6167 * @param {...string} ids - One or more ids for panels to obtain. |
|
6168 * @param {deferredPanelsCallback} [callback] - Function called when all supplied panels exist. |
|
6169 * @returns {wp.customize.Panel|undefined|jQuery.promise} Panel instance or undefined (if function called with one id param), or promise resolving to requested panels. |
|
6170 * |
|
6171 * @example <caption>Loop over all registered panels.</caption> |
|
6172 * wp.customize.panel.each( function( panel ) { ... } ) |
|
6173 * |
|
6174 * @example <caption>Getting nav_menus panel instance.</caption> |
|
6175 * panel = wp.customize.panel( 'nav_menus' ); |
|
6176 * |
|
6177 * @example <caption>Expand dynamically-created panel when it exists.</caption> |
|
6178 * wp.customize.panel( 'dynamically_created', function( panel ) { |
|
6179 * panel.expand(); |
|
6180 * } ); |
|
6181 * |
|
6182 * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances. |
|
6183 */ |
2066 api.panel = new api.Values({ defaultConstructor: api.Panel }); |
6184 api.panel = new api.Values({ defaultConstructor: api.Panel }); |
2067 |
6185 |
2068 /** |
6186 /** |
|
6187 * Callback for resolved notifications. |
|
6188 * |
|
6189 * @callback deferredNotificationsCallback |
|
6190 * @param {wp.customize.Notification[]} Resolved notifications. |
|
6191 */ |
|
6192 |
|
6193 /** |
|
6194 * Collection of all global notifications. |
|
6195 * |
|
6196 * @since 4.9.0 |
|
6197 * |
|
6198 * @type {Function} |
|
6199 * @param {...string} codes - One or more codes for notifications to obtain. |
|
6200 * @param {deferredNotificationsCallback} [callback] - Function called when all supplied notifications exist. |
|
6201 * @returns {wp.customize.Notification|undefined|jQuery.promise} notification instance or undefined (if function called with one code param), or promise resolving to requested notifications. |
|
6202 * |
|
6203 * @example <caption>Check if existing notification</caption> |
|
6204 * exists = wp.customize.notifications.has( 'a_new_day_arrived' ); |
|
6205 * |
|
6206 * @example <caption>Obtain existing notification</caption> |
|
6207 * notification = wp.customize.notifications( 'a_new_day_arrived' ); |
|
6208 * |
|
6209 * @example <caption>Obtain notification that may not exist yet.</caption> |
|
6210 * wp.customize.notifications( 'a_new_day_arrived', function( notification ) { ... } ); |
|
6211 * |
|
6212 * @example <caption>Add a warning notification.</caption> |
|
6213 * wp.customize.notifications.add( new wp.customize.Notification( 'midnight_almost_here', { |
|
6214 * type: 'warning', |
|
6215 * message: 'Midnight has almost arrived!', |
|
6216 * dismissible: true |
|
6217 * } ) ); |
|
6218 * |
|
6219 * @example <caption>Remove a notification.</caption> |
|
6220 * wp.customize.notifications.remove( 'a_new_day_arrived' ); |
|
6221 * |
|
6222 * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances. |
|
6223 */ |
|
6224 api.notifications = new api.Notifications(); |
|
6225 |
|
6226 /** |
|
6227 * An object that fetches a preview in the background of the document, which |
|
6228 * allows for seamless replacement of an existing preview. |
|
6229 * |
2069 * @class |
6230 * @class |
2070 * @augments wp.customize.Messenger |
6231 * @augments wp.customize.Messenger |
2071 * @augments wp.customize.Class |
6232 * @augments wp.customize.Class |
2072 * @mixes wp.customize.Events |
6233 * @mixes wp.customize.Events |
2073 */ |
6234 */ |
2074 api.PreviewFrame = api.Messenger.extend({ |
6235 api.PreviewFrame = api.Messenger.extend({ |
2075 sensitivity: 2000, |
6236 sensitivity: null, // Will get set to api.settings.timeouts.previewFrameSensitivity. |
2076 |
6237 |
|
6238 /** |
|
6239 * Initialize the PreviewFrame. |
|
6240 * |
|
6241 * @param {object} params.container |
|
6242 * @param {object} params.previewUrl |
|
6243 * @param {object} params.query |
|
6244 * @param {object} options |
|
6245 */ |
2077 initialize: function( params, options ) { |
6246 initialize: function( params, options ) { |
2078 var deferred = $.Deferred(); |
6247 var deferred = $.Deferred(); |
2079 |
6248 |
2080 // This is the promise object. |
6249 /* |
|
6250 * Make the instance of the PreviewFrame the promise object |
|
6251 * so other objects can easily interact with it. |
|
6252 */ |
2081 deferred.promise( this ); |
6253 deferred.promise( this ); |
2082 |
6254 |
2083 this.container = params.container; |
6255 this.container = params.container; |
2084 this.signature = params.signature; |
|
2085 |
6256 |
2086 $.extend( params, { channel: api.PreviewFrame.uuid() }); |
6257 $.extend( params, { channel: api.PreviewFrame.uuid() }); |
2087 |
6258 |
2088 api.Messenger.prototype.initialize.call( this, params, options ); |
6259 api.Messenger.prototype.initialize.call( this, params, options ); |
2089 |
6260 |
2288 * @augments wp.customize.Messenger |
6471 * @augments wp.customize.Messenger |
2289 * @augments wp.customize.Class |
6472 * @augments wp.customize.Class |
2290 * @mixes wp.customize.Events |
6473 * @mixes wp.customize.Events |
2291 */ |
6474 */ |
2292 api.Previewer = api.Messenger.extend({ |
6475 api.Previewer = api.Messenger.extend({ |
2293 refreshBuffer: 250, |
6476 refreshBuffer: null, // Will get set to api.settings.timeouts.windowRefresh. |
2294 |
6477 |
2295 /** |
6478 /** |
2296 * Requires params: |
6479 * @param {array} params.allowedUrls |
2297 * - container - a selector or jQuery element |
6480 * @param {string} params.container A selector or jQuery element for the preview |
2298 * - previewUrl - the URL of preview frame |
6481 * frame to be placed. |
|
6482 * @param {string} params.form |
|
6483 * @param {string} params.previewUrl The URL to preview. |
|
6484 * @param {object} options |
2299 */ |
6485 */ |
2300 initialize: function( params, options ) { |
6486 initialize: function( params, options ) { |
2301 var self = this, |
6487 var previewer = this, |
2302 rscheme = /^https?/; |
6488 urlParser = document.createElement( 'a' ); |
2303 |
6489 |
2304 $.extend( this, options || {} ); |
6490 $.extend( previewer, options || {} ); |
2305 this.deferred = { |
6491 previewer.deferred = { |
2306 active: $.Deferred() |
6492 active: $.Deferred() |
2307 }; |
6493 }; |
2308 |
6494 |
|
6495 // Debounce to prevent hammering server and then wait for any pending update requests. |
|
6496 previewer.refresh = _.debounce( |
|
6497 ( function( originalRefresh ) { |
|
6498 return function() { |
|
6499 var isProcessingComplete, refreshOnceProcessingComplete; |
|
6500 isProcessingComplete = function() { |
|
6501 return 0 === api.state( 'processing' ).get(); |
|
6502 }; |
|
6503 if ( isProcessingComplete() ) { |
|
6504 originalRefresh.call( previewer ); |
|
6505 } else { |
|
6506 refreshOnceProcessingComplete = function() { |
|
6507 if ( isProcessingComplete() ) { |
|
6508 originalRefresh.call( previewer ); |
|
6509 api.state( 'processing' ).unbind( refreshOnceProcessingComplete ); |
|
6510 } |
|
6511 }; |
|
6512 api.state( 'processing' ).bind( refreshOnceProcessingComplete ); |
|
6513 } |
|
6514 }; |
|
6515 }( previewer.refresh ) ), |
|
6516 previewer.refreshBuffer |
|
6517 ); |
|
6518 |
|
6519 previewer.container = api.ensure( params.container ); |
|
6520 previewer.allowedUrls = params.allowedUrls; |
|
6521 |
|
6522 params.url = window.location.href; |
|
6523 |
|
6524 api.Messenger.prototype.initialize.call( previewer, params ); |
|
6525 |
|
6526 urlParser.href = previewer.origin(); |
|
6527 previewer.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) ); |
|
6528 |
|
6529 // Limit the URL to internal, front-end links. |
|
6530 // |
|
6531 // If the front end and the admin are served from the same domain, load the |
|
6532 // preview over ssl if the Customizer is being loaded over ssl. This avoids |
|
6533 // insecure content warnings. This is not attempted if the admin and front end |
|
6534 // are on different domains to avoid the case where the front end doesn't have |
|
6535 // ssl certs. |
|
6536 |
|
6537 previewer.add( 'previewUrl', params.previewUrl ).setter( function( to ) { |
|
6538 var result = null, urlParser, queryParams, parsedAllowedUrl, parsedCandidateUrls = []; |
|
6539 urlParser = document.createElement( 'a' ); |
|
6540 urlParser.href = to; |
|
6541 |
|
6542 // Abort if URL is for admin or (static) files in wp-includes or wp-content. |
|
6543 if ( /\/wp-(admin|includes|content)(\/|$)/.test( urlParser.pathname ) ) { |
|
6544 return null; |
|
6545 } |
|
6546 |
|
6547 // Remove state query params. |
|
6548 if ( urlParser.search.length > 1 ) { |
|
6549 queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); |
|
6550 delete queryParams.customize_changeset_uuid; |
|
6551 delete queryParams.customize_theme; |
|
6552 delete queryParams.customize_messenger_channel; |
|
6553 delete queryParams.customize_autosaved; |
|
6554 if ( _.isEmpty( queryParams ) ) { |
|
6555 urlParser.search = ''; |
|
6556 } else { |
|
6557 urlParser.search = $.param( queryParams ); |
|
6558 } |
|
6559 } |
|
6560 |
|
6561 parsedCandidateUrls.push( urlParser ); |
|
6562 |
|
6563 // Prepend list with URL that matches the scheme/protocol of the iframe. |
|
6564 if ( previewer.scheme.get() + ':' !== urlParser.protocol ) { |
|
6565 urlParser = document.createElement( 'a' ); |
|
6566 urlParser.href = parsedCandidateUrls[0].href; |
|
6567 urlParser.protocol = previewer.scheme.get() + ':'; |
|
6568 parsedCandidateUrls.unshift( urlParser ); |
|
6569 } |
|
6570 |
|
6571 // Attempt to match the URL to the control frame's scheme and check if it's allowed. If not, try the original URL. |
|
6572 parsedAllowedUrl = document.createElement( 'a' ); |
|
6573 _.find( parsedCandidateUrls, function( parsedCandidateUrl ) { |
|
6574 return ! _.isUndefined( _.find( previewer.allowedUrls, function( allowedUrl ) { |
|
6575 parsedAllowedUrl.href = allowedUrl; |
|
6576 if ( urlParser.protocol === parsedAllowedUrl.protocol && urlParser.host === parsedAllowedUrl.host && 0 === urlParser.pathname.indexOf( parsedAllowedUrl.pathname.replace( /\/$/, '' ) ) ) { |
|
6577 result = parsedCandidateUrl.href; |
|
6578 return true; |
|
6579 } |
|
6580 } ) ); |
|
6581 } ); |
|
6582 |
|
6583 return result; |
|
6584 }); |
|
6585 |
|
6586 previewer.bind( 'ready', previewer.ready ); |
|
6587 |
|
6588 // Start listening for keep-alive messages when iframe first loads. |
|
6589 previewer.deferred.active.done( _.bind( previewer.keepPreviewAlive, previewer ) ); |
|
6590 |
|
6591 previewer.bind( 'synced', function() { |
|
6592 previewer.send( 'active' ); |
|
6593 } ); |
|
6594 |
|
6595 // Refresh the preview when the URL is changed (but not yet). |
|
6596 previewer.previewUrl.bind( previewer.refresh ); |
|
6597 |
|
6598 previewer.scroll = 0; |
|
6599 previewer.bind( 'scroll', function( distance ) { |
|
6600 previewer.scroll = distance; |
|
6601 }); |
|
6602 |
|
6603 // Update the URL when the iframe sends a URL message, resetting scroll position. If URL is unchanged, then refresh. |
|
6604 previewer.bind( 'url', function( url ) { |
|
6605 var onUrlChange, urlChanged = false; |
|
6606 previewer.scroll = 0; |
|
6607 onUrlChange = function() { |
|
6608 urlChanged = true; |
|
6609 }; |
|
6610 previewer.previewUrl.bind( onUrlChange ); |
|
6611 previewer.previewUrl.set( url ); |
|
6612 previewer.previewUrl.unbind( onUrlChange ); |
|
6613 if ( ! urlChanged ) { |
|
6614 previewer.refresh(); |
|
6615 } |
|
6616 } ); |
|
6617 |
|
6618 // Update the document title when the preview changes. |
|
6619 previewer.bind( 'documentTitle', function ( title ) { |
|
6620 api.setDocumentTitle( title ); |
|
6621 } ); |
|
6622 }, |
|
6623 |
|
6624 /** |
|
6625 * Handle the preview receiving the ready message. |
|
6626 * |
|
6627 * @since 4.7.0 |
|
6628 * @access public |
|
6629 * |
|
6630 * @param {object} data - Data from preview. |
|
6631 * @param {string} data.currentUrl - Current URL. |
|
6632 * @param {object} data.activePanels - Active panels. |
|
6633 * @param {object} data.activeSections Active sections. |
|
6634 * @param {object} data.activeControls Active controls. |
|
6635 * @returns {void} |
|
6636 */ |
|
6637 ready: function( data ) { |
|
6638 var previewer = this, synced = {}, constructs; |
|
6639 |
|
6640 synced.settings = api.get(); |
|
6641 synced['settings-modified-while-loading'] = previewer.settingsModifiedWhileLoading; |
|
6642 if ( 'resolved' !== previewer.deferred.active.state() || previewer.loading ) { |
|
6643 synced.scroll = previewer.scroll; |
|
6644 } |
|
6645 synced['edit-shortcut-visibility'] = api.state( 'editShortcutVisibility' ).get(); |
|
6646 previewer.send( 'sync', synced ); |
|
6647 |
|
6648 // Set the previewUrl without causing the url to set the iframe. |
|
6649 if ( data.currentUrl ) { |
|
6650 previewer.previewUrl.unbind( previewer.refresh ); |
|
6651 previewer.previewUrl.set( data.currentUrl ); |
|
6652 previewer.previewUrl.bind( previewer.refresh ); |
|
6653 } |
|
6654 |
2309 /* |
6655 /* |
2310 * Wrap this.refresh to prevent it from hammering the servers: |
6656 * Walk over all panels, sections, and controls and set their |
2311 * |
6657 * respective active states to true if the preview explicitly |
2312 * If refresh is called once and no other refresh requests are |
6658 * indicates as such. |
2313 * loading, trigger the request immediately. |
|
2314 * |
|
2315 * If refresh is called while another refresh request is loading, |
|
2316 * debounce the refresh requests: |
|
2317 * 1. Stop the loading request (as it is instantly outdated). |
|
2318 * 2. Trigger the new request once refresh hasn't been called for |
|
2319 * self.refreshBuffer milliseconds. |
|
2320 */ |
6659 */ |
2321 this.refresh = (function( self ) { |
6660 constructs = { |
2322 var refresh = self.refresh, |
6661 panel: data.activePanels, |
2323 callback = function() { |
6662 section: data.activeSections, |
2324 timeout = null; |
6663 control: data.activeControls |
2325 refresh.call( self ); |
6664 }; |
2326 }, |
6665 _( constructs ).each( function ( activeConstructs, type ) { |
2327 timeout; |
6666 api[ type ].each( function ( construct, id ) { |
2328 |
6667 var isDynamicallyCreated = _.isUndefined( api.settings[ type + 's' ][ id ] ); |
2329 return function() { |
6668 |
2330 if ( typeof timeout !== 'number' ) { |
6669 /* |
2331 if ( self.loading ) { |
6670 * If the construct was created statically in PHP (not dynamically in JS) |
2332 self.abort(); |
6671 * then consider a missing (undefined) value in the activeConstructs to |
|
6672 * mean it should be deactivated (since it is gone). But if it is |
|
6673 * dynamically created then only toggle activation if the value is defined, |
|
6674 * as this means that the construct was also then correspondingly |
|
6675 * created statically in PHP and the active callback is available. |
|
6676 * Otherwise, dynamically-created constructs should normally have |
|
6677 * their active states toggled in JS rather than from PHP. |
|
6678 */ |
|
6679 if ( ! isDynamicallyCreated || ! _.isUndefined( activeConstructs[ id ] ) ) { |
|
6680 if ( activeConstructs[ id ] ) { |
|
6681 construct.activate(); |
2333 } else { |
6682 } else { |
2334 return callback(); |
6683 construct.deactivate(); |
2335 } |
6684 } |
2336 } |
6685 } |
2337 |
6686 } ); |
2338 clearTimeout( timeout ); |
|
2339 timeout = setTimeout( callback, self.refreshBuffer ); |
|
2340 }; |
|
2341 })( this ); |
|
2342 |
|
2343 this.container = api.ensure( params.container ); |
|
2344 this.allowedUrls = params.allowedUrls; |
|
2345 this.signature = params.signature; |
|
2346 |
|
2347 params.url = window.location.href; |
|
2348 |
|
2349 api.Messenger.prototype.initialize.call( this, params ); |
|
2350 |
|
2351 this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) { |
|
2352 var match = to.match( rscheme ); |
|
2353 return match ? match[0] : ''; |
|
2354 }); |
|
2355 |
|
2356 // Limit the URL to internal, front-end links. |
|
2357 // |
|
2358 // If the frontend and the admin are served from the same domain, load the |
|
2359 // preview over ssl if the Customizer is being loaded over ssl. This avoids |
|
2360 // insecure content warnings. This is not attempted if the admin and frontend |
|
2361 // are on different domains to avoid the case where the frontend doesn't have |
|
2362 // ssl certs. |
|
2363 |
|
2364 this.add( 'previewUrl', params.previewUrl ).setter( function( to ) { |
|
2365 var result; |
|
2366 |
|
2367 // Check for URLs that include "/wp-admin/" or end in "/wp-admin". |
|
2368 // Strip hashes and query strings before testing. |
|
2369 if ( /\/wp-admin(\/|$)/.test( to.replace( /[#?].*$/, '' ) ) ) |
|
2370 return null; |
|
2371 |
|
2372 // Attempt to match the URL to the control frame's scheme |
|
2373 // and check if it's allowed. If not, try the original URL. |
|
2374 $.each([ to.replace( rscheme, self.scheme() ), to ], function( i, url ) { |
|
2375 $.each( self.allowedUrls, function( i, allowed ) { |
|
2376 var path; |
|
2377 |
|
2378 allowed = allowed.replace( /\/+$/, '' ); |
|
2379 path = url.replace( allowed, '' ); |
|
2380 |
|
2381 if ( 0 === url.indexOf( allowed ) && /^([/#?]|$)/.test( path ) ) { |
|
2382 result = url; |
|
2383 return false; |
|
2384 } |
|
2385 }); |
|
2386 if ( result ) |
|
2387 return false; |
|
2388 }); |
|
2389 |
|
2390 // If we found a matching result, return it. If not, bail. |
|
2391 return result ? result : null; |
|
2392 }); |
|
2393 |
|
2394 // Refresh the preview when the URL is changed (but not yet). |
|
2395 this.previewUrl.bind( this.refresh ); |
|
2396 |
|
2397 this.scroll = 0; |
|
2398 this.bind( 'scroll', function( distance ) { |
|
2399 this.scroll = distance; |
|
2400 }); |
|
2401 |
|
2402 // Update the URL when the iframe sends a URL message. |
|
2403 this.bind( 'url', this.previewUrl ); |
|
2404 |
|
2405 // Update the document title when the preview changes. |
|
2406 this.bind( 'documentTitle', function ( title ) { |
|
2407 api.setDocumentTitle( title ); |
|
2408 } ); |
6687 } ); |
2409 }, |
6688 |
2410 |
6689 if ( data.settingValidities ) { |
|
6690 api._handleSettingValidities( { |
|
6691 settingValidities: data.settingValidities, |
|
6692 focusInvalidControl: false |
|
6693 } ); |
|
6694 } |
|
6695 }, |
|
6696 |
|
6697 /** |
|
6698 * Keep the preview alive by listening for ready and keep-alive messages. |
|
6699 * |
|
6700 * If a message is not received in the allotted time then the iframe will be set back to the last known valid URL. |
|
6701 * |
|
6702 * @since 4.7.0 |
|
6703 * @access public |
|
6704 * |
|
6705 * @returns {void} |
|
6706 */ |
|
6707 keepPreviewAlive: function keepPreviewAlive() { |
|
6708 var previewer = this, keepAliveTick, timeoutId, handleMissingKeepAlive, scheduleKeepAliveCheck; |
|
6709 |
|
6710 /** |
|
6711 * Schedule a preview keep-alive check. |
|
6712 * |
|
6713 * Note that if a page load takes longer than keepAliveCheck milliseconds, |
|
6714 * the keep-alive messages will still be getting sent from the previous |
|
6715 * URL. |
|
6716 */ |
|
6717 scheduleKeepAliveCheck = function() { |
|
6718 timeoutId = setTimeout( handleMissingKeepAlive, api.settings.timeouts.keepAliveCheck ); |
|
6719 }; |
|
6720 |
|
6721 /** |
|
6722 * Set the previewerAlive state to true when receiving a message from the preview. |
|
6723 */ |
|
6724 keepAliveTick = function() { |
|
6725 api.state( 'previewerAlive' ).set( true ); |
|
6726 clearTimeout( timeoutId ); |
|
6727 scheduleKeepAliveCheck(); |
|
6728 }; |
|
6729 |
|
6730 /** |
|
6731 * Set the previewerAlive state to false if keepAliveCheck milliseconds have transpired without a message. |
|
6732 * |
|
6733 * This is most likely to happen in the case of a connectivity error, or if the theme causes the browser |
|
6734 * to navigate to a non-allowed URL. Setting this state to false will force settings with a postMessage |
|
6735 * transport to use refresh instead, causing the preview frame also to be replaced with the current |
|
6736 * allowed preview URL. |
|
6737 */ |
|
6738 handleMissingKeepAlive = function() { |
|
6739 api.state( 'previewerAlive' ).set( false ); |
|
6740 }; |
|
6741 scheduleKeepAliveCheck(); |
|
6742 |
|
6743 previewer.bind( 'ready', keepAliveTick ); |
|
6744 previewer.bind( 'keep-alive', keepAliveTick ); |
|
6745 }, |
|
6746 |
|
6747 /** |
|
6748 * Query string data sent with each preview request. |
|
6749 * |
|
6750 * @abstract |
|
6751 */ |
2411 query: function() {}, |
6752 query: function() {}, |
2412 |
6753 |
2413 abort: function() { |
6754 abort: function() { |
2414 if ( this.loading ) { |
6755 if ( this.loading ) { |
2415 this.loading.destroy(); |
6756 this.loading.destroy(); |
2416 delete this.loading; |
6757 delete this.loading; |
2417 } |
6758 } |
2418 }, |
6759 }, |
2419 |
6760 |
|
6761 /** |
|
6762 * Refresh the preview seamlessly. |
|
6763 * |
|
6764 * @since 3.4.0 |
|
6765 * @access public |
|
6766 * @returns {void} |
|
6767 */ |
2420 refresh: function() { |
6768 refresh: function() { |
2421 var self = this; |
6769 var previewer = this, onSettingChange; |
2422 |
6770 |
2423 // Display loading indicator |
6771 // Display loading indicator |
2424 this.send( 'loading-initiated' ); |
6772 previewer.send( 'loading-initiated' ); |
2425 |
6773 |
2426 this.abort(); |
6774 previewer.abort(); |
2427 |
6775 |
2428 this.loading = new api.PreviewFrame({ |
6776 previewer.loading = new api.PreviewFrame({ |
2429 url: this.url(), |
6777 url: previewer.url(), |
2430 previewUrl: this.previewUrl(), |
6778 previewUrl: previewer.previewUrl(), |
2431 query: this.query() || {}, |
6779 query: previewer.query( { excludeCustomizedSaved: true } ) || {}, |
2432 container: this.container, |
6780 container: previewer.container |
2433 signature: this.signature |
|
2434 }); |
6781 }); |
2435 |
6782 |
2436 this.loading.done( function() { |
6783 previewer.settingsModifiedWhileLoading = {}; |
2437 // 'this' is the loading frame |
6784 onSettingChange = function( setting ) { |
2438 this.bind( 'synced', function() { |
6785 previewer.settingsModifiedWhileLoading[ setting.id ] = true; |
2439 if ( self.preview ) |
6786 }; |
2440 self.preview.destroy(); |
6787 api.bind( 'change', onSettingChange ); |
2441 self.preview = this; |
6788 previewer.loading.always( function() { |
2442 delete self.loading; |
6789 api.unbind( 'change', onSettingChange ); |
2443 |
6790 } ); |
2444 self.targetWindow( this.targetWindow() ); |
6791 |
2445 self.channel( this.channel() ); |
6792 previewer.loading.done( function( readyData ) { |
2446 |
6793 var loadingFrame = this, onceSynced; |
2447 self.deferred.active.resolve(); |
6794 |
2448 self.send( 'active' ); |
6795 previewer.preview = loadingFrame; |
2449 }); |
6796 previewer.targetWindow( loadingFrame.targetWindow() ); |
2450 |
6797 previewer.channel( loadingFrame.channel() ); |
2451 this.send( 'sync', { |
6798 |
2452 scroll: self.scroll, |
6799 onceSynced = function() { |
2453 settings: api.get() |
6800 loadingFrame.unbind( 'synced', onceSynced ); |
2454 }); |
6801 if ( previewer._previousPreview ) { |
|
6802 previewer._previousPreview.destroy(); |
|
6803 } |
|
6804 previewer._previousPreview = previewer.preview; |
|
6805 previewer.deferred.active.resolve(); |
|
6806 delete previewer.loading; |
|
6807 }; |
|
6808 loadingFrame.bind( 'synced', onceSynced ); |
|
6809 |
|
6810 // This event will be received directly by the previewer in normal navigation; this is only needed for seamless refresh. |
|
6811 previewer.trigger( 'ready', readyData ); |
2455 }); |
6812 }); |
2456 |
6813 |
2457 this.loading.fail( function( reason, location ) { |
6814 previewer.loading.fail( function( reason ) { |
2458 self.send( 'loading-failed' ); |
6815 previewer.send( 'loading-failed' ); |
2459 if ( 'redirect' === reason && location ) { |
|
2460 self.previewUrl( location ); |
|
2461 } |
|
2462 |
6816 |
2463 if ( 'logged out' === reason ) { |
6817 if ( 'logged out' === reason ) { |
2464 if ( self.preview ) { |
6818 if ( previewer.preview ) { |
2465 self.preview.destroy(); |
6819 previewer.preview.destroy(); |
2466 delete self.preview; |
6820 delete previewer.preview; |
2467 } |
6821 } |
2468 |
6822 |
2469 self.login().done( self.refresh ); |
6823 previewer.login().done( previewer.refresh ); |
2470 } |
6824 } |
2471 |
6825 |
2472 if ( 'cheatin' === reason ) { |
6826 if ( 'cheatin' === reason ) { |
2473 self.cheatin(); |
6827 previewer.cheatin(); |
2474 } |
6828 } |
2475 }); |
6829 }); |
2476 }, |
6830 }, |
2477 |
6831 |
2478 login: function() { |
6832 login: function() { |
2479 var previewer = this, |
6833 var previewer = this, |
2480 deferred, messenger, iframe; |
6834 deferred, messenger, iframe; |
2481 |
6835 |
2482 if ( this._login ) |
6836 if ( this._login ) { |
2483 return this._login; |
6837 return this._login; |
|
6838 } |
2484 |
6839 |
2485 deferred = $.Deferred(); |
6840 deferred = $.Deferred(); |
2486 this._login = deferred.promise(); |
6841 this._login = deferred.promise(); |
2487 |
6842 |
2488 messenger = new api.Messenger({ |
6843 messenger = new api.Messenger({ |
2541 |
6899 |
2542 return deferred; |
6900 return deferred; |
2543 } |
6901 } |
2544 }); |
6902 }); |
2545 |
6903 |
|
6904 api.settingConstructor = {}; |
2546 api.controlConstructor = { |
6905 api.controlConstructor = { |
2547 color: api.ColorControl, |
6906 color: api.ColorControl, |
2548 media: api.MediaControl, |
6907 media: api.MediaControl, |
2549 upload: api.UploadControl, |
6908 upload: api.UploadControl, |
2550 image: api.ImageControl, |
6909 image: api.ImageControl, |
2551 header: api.HeaderControl, |
6910 cropped_image: api.CroppedImageControl, |
2552 background: api.BackgroundControl, |
6911 site_icon: api.SiteIconControl, |
2553 theme: api.ThemeControl |
6912 header: api.HeaderControl, |
|
6913 background: api.BackgroundControl, |
|
6914 background_position: api.BackgroundPositionControl, |
|
6915 theme: api.ThemeControl, |
|
6916 date_time: api.DateTimeControl, |
|
6917 code_editor: api.CodeEditorControl |
2554 }; |
6918 }; |
2555 api.panelConstructor = {}; |
6919 api.panelConstructor = { |
|
6920 themes: api.ThemesPanel |
|
6921 }; |
2556 api.sectionConstructor = { |
6922 api.sectionConstructor = { |
2557 themes: api.ThemesSection |
6923 themes: api.ThemesSection, |
|
6924 outer: api.OuterSection |
2558 }; |
6925 }; |
|
6926 |
|
6927 /** |
|
6928 * Handle setting_validities in an error response for the customize-save request. |
|
6929 * |
|
6930 * Add notifications to the settings and focus on the first control that has an invalid setting. |
|
6931 * |
|
6932 * @since 4.6.0 |
|
6933 * @private |
|
6934 * |
|
6935 * @param {object} args |
|
6936 * @param {object} args.settingValidities |
|
6937 * @param {boolean} [args.focusInvalidControl=false] |
|
6938 * @returns {void} |
|
6939 */ |
|
6940 api._handleSettingValidities = function handleSettingValidities( args ) { |
|
6941 var invalidSettingControls, invalidSettings = [], wasFocused = false; |
|
6942 |
|
6943 // Find the controls that correspond to each invalid setting. |
|
6944 _.each( args.settingValidities, function( validity, settingId ) { |
|
6945 var setting = api( settingId ); |
|
6946 if ( setting ) { |
|
6947 |
|
6948 // Add notifications for invalidities. |
|
6949 if ( _.isObject( validity ) ) { |
|
6950 _.each( validity, function( params, code ) { |
|
6951 var notification, existingNotification, needsReplacement = false; |
|
6952 notification = new api.Notification( code, _.extend( { fromServer: true }, params ) ); |
|
6953 |
|
6954 // Remove existing notification if already exists for code but differs in parameters. |
|
6955 existingNotification = setting.notifications( notification.code ); |
|
6956 if ( existingNotification ) { |
|
6957 needsReplacement = notification.type !== existingNotification.type || notification.message !== existingNotification.message || ! _.isEqual( notification.data, existingNotification.data ); |
|
6958 } |
|
6959 if ( needsReplacement ) { |
|
6960 setting.notifications.remove( code ); |
|
6961 } |
|
6962 |
|
6963 if ( ! setting.notifications.has( notification.code ) ) { |
|
6964 setting.notifications.add( notification ); |
|
6965 } |
|
6966 invalidSettings.push( setting.id ); |
|
6967 } ); |
|
6968 } |
|
6969 |
|
6970 // Remove notification errors that are no longer valid. |
|
6971 setting.notifications.each( function( notification ) { |
|
6972 if ( notification.fromServer && 'error' === notification.type && ( true === validity || ! validity[ notification.code ] ) ) { |
|
6973 setting.notifications.remove( notification.code ); |
|
6974 } |
|
6975 } ); |
|
6976 } |
|
6977 } ); |
|
6978 |
|
6979 if ( args.focusInvalidControl ) { |
|
6980 invalidSettingControls = api.findControlsForSettings( invalidSettings ); |
|
6981 |
|
6982 // Focus on the first control that is inside of an expanded section (one that is visible). |
|
6983 _( _.values( invalidSettingControls ) ).find( function( controls ) { |
|
6984 return _( controls ).find( function( control ) { |
|
6985 var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded(); |
|
6986 if ( isExpanded && control.expanded ) { |
|
6987 isExpanded = control.expanded(); |
|
6988 } |
|
6989 if ( isExpanded ) { |
|
6990 control.focus(); |
|
6991 wasFocused = true; |
|
6992 } |
|
6993 return wasFocused; |
|
6994 } ); |
|
6995 } ); |
|
6996 |
|
6997 // Focus on the first invalid control. |
|
6998 if ( ! wasFocused && ! _.isEmpty( invalidSettingControls ) ) { |
|
6999 _.values( invalidSettingControls )[0][0].focus(); |
|
7000 } |
|
7001 } |
|
7002 }; |
|
7003 |
|
7004 /** |
|
7005 * Find all controls associated with the given settings. |
|
7006 * |
|
7007 * @since 4.6.0 |
|
7008 * @param {string[]} settingIds Setting IDs. |
|
7009 * @returns {object<string, wp.customize.Control>} Mapping setting ids to arrays of controls. |
|
7010 */ |
|
7011 api.findControlsForSettings = function findControlsForSettings( settingIds ) { |
|
7012 var controls = {}, settingControls; |
|
7013 _.each( _.unique( settingIds ), function( settingId ) { |
|
7014 var setting = api( settingId ); |
|
7015 if ( setting ) { |
|
7016 settingControls = setting.findControls(); |
|
7017 if ( settingControls && settingControls.length > 0 ) { |
|
7018 controls[ settingId ] = settingControls; |
|
7019 } |
|
7020 } |
|
7021 } ); |
|
7022 return controls; |
|
7023 }; |
|
7024 |
|
7025 /** |
|
7026 * Sort panels, sections, controls by priorities. Hide empty sections and panels. |
|
7027 * |
|
7028 * @since 4.1.0 |
|
7029 */ |
|
7030 api.reflowPaneContents = _.bind( function () { |
|
7031 |
|
7032 var appendContainer, activeElement, rootHeadContainers, rootNodes = [], wasReflowed = false; |
|
7033 |
|
7034 if ( document.activeElement ) { |
|
7035 activeElement = $( document.activeElement ); |
|
7036 } |
|
7037 |
|
7038 // Sort the sections within each panel |
|
7039 api.panel.each( function ( panel ) { |
|
7040 if ( 'themes' === panel.id ) { |
|
7041 return; // Don't reflow theme sections, as doing so moves them after the themes container. |
|
7042 } |
|
7043 |
|
7044 var sections = panel.sections(), |
|
7045 sectionHeadContainers = _.pluck( sections, 'headContainer' ); |
|
7046 rootNodes.push( panel ); |
|
7047 appendContainer = ( panel.contentContainer.is( 'ul' ) ) ? panel.contentContainer : panel.contentContainer.find( 'ul:first' ); |
|
7048 if ( ! api.utils.areElementListsEqual( sectionHeadContainers, appendContainer.children( '[id]' ) ) ) { |
|
7049 _( sections ).each( function ( section ) { |
|
7050 appendContainer.append( section.headContainer ); |
|
7051 } ); |
|
7052 wasReflowed = true; |
|
7053 } |
|
7054 } ); |
|
7055 |
|
7056 // Sort the controls within each section |
|
7057 api.section.each( function ( section ) { |
|
7058 var controls = section.controls(), |
|
7059 controlContainers = _.pluck( controls, 'container' ); |
|
7060 if ( ! section.panel() ) { |
|
7061 rootNodes.push( section ); |
|
7062 } |
|
7063 appendContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' ); |
|
7064 if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) { |
|
7065 _( controls ).each( function ( control ) { |
|
7066 appendContainer.append( control.container ); |
|
7067 } ); |
|
7068 wasReflowed = true; |
|
7069 } |
|
7070 } ); |
|
7071 |
|
7072 // Sort the root panels and sections |
|
7073 rootNodes.sort( api.utils.prioritySort ); |
|
7074 rootHeadContainers = _.pluck( rootNodes, 'headContainer' ); |
|
7075 appendContainer = $( '#customize-theme-controls .customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable |
|
7076 if ( ! api.utils.areElementListsEqual( rootHeadContainers, appendContainer.children() ) ) { |
|
7077 _( rootNodes ).each( function ( rootNode ) { |
|
7078 appendContainer.append( rootNode.headContainer ); |
|
7079 } ); |
|
7080 wasReflowed = true; |
|
7081 } |
|
7082 |
|
7083 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered |
|
7084 api.panel.each( function ( panel ) { |
|
7085 var value = panel.active(); |
|
7086 panel.active.callbacks.fireWith( panel.active, [ value, value ] ); |
|
7087 } ); |
|
7088 api.section.each( function ( section ) { |
|
7089 var value = section.active(); |
|
7090 section.active.callbacks.fireWith( section.active, [ value, value ] ); |
|
7091 } ); |
|
7092 |
|
7093 // Restore focus if there was a reflow and there was an active (focused) element |
|
7094 if ( wasReflowed && activeElement ) { |
|
7095 activeElement.focus(); |
|
7096 } |
|
7097 api.trigger( 'pane-contents-reflowed' ); |
|
7098 }, api ); |
|
7099 |
|
7100 // Define state values. |
|
7101 api.state = new api.Values(); |
|
7102 _.each( [ |
|
7103 'saved', |
|
7104 'saving', |
|
7105 'trashing', |
|
7106 'activated', |
|
7107 'processing', |
|
7108 'paneVisible', |
|
7109 'expandedPanel', |
|
7110 'expandedSection', |
|
7111 'changesetDate', |
|
7112 'selectedChangesetDate', |
|
7113 'changesetStatus', |
|
7114 'selectedChangesetStatus', |
|
7115 'remainingTimeToPublish', |
|
7116 'previewerAlive', |
|
7117 'editShortcutVisibility', |
|
7118 'changesetLocked', |
|
7119 'previewedDevice' |
|
7120 ], function( name ) { |
|
7121 api.state.create( name ); |
|
7122 }); |
2559 |
7123 |
2560 $( function() { |
7124 $( function() { |
2561 api.settings = window._wpCustomizeSettings; |
7125 api.settings = window._wpCustomizeSettings; |
2562 api.l10n = window._wpCustomizeControlsL10n; |
7126 api.l10n = window._wpCustomizeControlsL10n; |
2563 |
7127 |
2564 // Check if we can run the Customizer. |
7128 // Check if we can run the Customizer. |
2565 if ( ! api.settings ) { |
7129 if ( ! api.settings ) { |
2566 return; |
7130 return; |
2567 } |
7131 } |
2568 |
7132 |
2569 // Redirect to the fallback preview if any incompatibilities are found. |
7133 // Bail if any incompatibilities are found. |
2570 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) |
7134 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) { |
2571 return window.location = api.settings.url.fallback; |
7135 return; |
2572 |
7136 } |
2573 var parent, topFocus, |
7137 |
|
7138 if ( null === api.PreviewFrame.prototype.sensitivity ) { |
|
7139 api.PreviewFrame.prototype.sensitivity = api.settings.timeouts.previewFrameSensitivity; |
|
7140 } |
|
7141 if ( null === api.Previewer.prototype.refreshBuffer ) { |
|
7142 api.Previewer.prototype.refreshBuffer = api.settings.timeouts.windowRefresh; |
|
7143 } |
|
7144 |
|
7145 var parent, |
2574 body = $( document.body ), |
7146 body = $( document.body ), |
2575 overlay = body.children( '.wp-full-overlay' ), |
7147 overlay = body.children( '.wp-full-overlay' ), |
2576 title = $( '#customize-info .theme-name.site-title' ), |
7148 title = $( '#customize-info .panel-title.site-title' ), |
2577 closeBtn = $( '.customize-controls-close' ), |
7149 closeBtn = $( '.customize-controls-close' ), |
2578 saveBtn = $( '#save' ); |
7150 saveBtn = $( '#save' ), |
|
7151 btnWrapper = $( '#customize-save-button-wrapper' ), |
|
7152 publishSettingsBtn = $( '#publish-settings' ), |
|
7153 footerActions = $( '#customize-footer-actions' ); |
|
7154 |
|
7155 // Add publish settings section in JS instead of PHP since the Customizer depends on it to function. |
|
7156 api.bind( 'ready', function() { |
|
7157 api.section.add( new api.OuterSection( 'publish_settings', { |
|
7158 title: api.l10n.publishSettings, |
|
7159 priority: 0, |
|
7160 active: api.settings.theme.active |
|
7161 } ) ); |
|
7162 } ); |
|
7163 |
|
7164 // Set up publish settings section and its controls. |
|
7165 api.section( 'publish_settings', function( section ) { |
|
7166 var updateButtonsState, trashControl, updateSectionActive, isSectionActive, statusControl, dateControl, toggleDateControl, publishWhenTime, pollInterval, updateTimeArrivedPoller, cancelScheduleButtonReminder, timeArrivedPollingInterval = 1000; |
|
7167 |
|
7168 trashControl = new api.Control( 'trash_changeset', { |
|
7169 type: 'button', |
|
7170 section: section.id, |
|
7171 priority: 30, |
|
7172 input_attrs: { |
|
7173 'class': 'button-link button-link-delete', |
|
7174 value: api.l10n.discardChanges |
|
7175 } |
|
7176 } ); |
|
7177 api.control.add( trashControl ); |
|
7178 trashControl.deferred.embedded.done( function() { |
|
7179 trashControl.container.find( '.button-link' ).on( 'click', function() { |
|
7180 if ( confirm( api.l10n.trashConfirm ) ) { |
|
7181 wp.customize.previewer.trash(); |
|
7182 } |
|
7183 } ); |
|
7184 } ); |
|
7185 |
|
7186 api.control.add( new api.PreviewLinkControl( 'changeset_preview_link', { |
|
7187 section: section.id, |
|
7188 priority: 100 |
|
7189 } ) ); |
|
7190 |
|
7191 /** |
|
7192 * Return whether the pubish settings section should be active. |
|
7193 * |
|
7194 * @return {boolean} Is section active. |
|
7195 */ |
|
7196 isSectionActive = function() { |
|
7197 if ( ! api.state( 'activated' ).get() ) { |
|
7198 return false; |
|
7199 } |
|
7200 if ( api.state( 'trashing' ).get() || 'trash' === api.state( 'changesetStatus' ).get() ) { |
|
7201 return false; |
|
7202 } |
|
7203 if ( '' === api.state( 'changesetStatus' ).get() && api.state( 'saved' ).get() ) { |
|
7204 return false; |
|
7205 } |
|
7206 return true; |
|
7207 }; |
|
7208 |
|
7209 // Make sure publish settings are not available while the theme is not active and the customizer is in a published state. |
|
7210 section.active.validate = isSectionActive; |
|
7211 updateSectionActive = function() { |
|
7212 section.active.set( isSectionActive() ); |
|
7213 }; |
|
7214 api.state( 'activated' ).bind( updateSectionActive ); |
|
7215 api.state( 'trashing' ).bind( updateSectionActive ); |
|
7216 api.state( 'saved' ).bind( updateSectionActive ); |
|
7217 api.state( 'changesetStatus' ).bind( updateSectionActive ); |
|
7218 updateSectionActive(); |
|
7219 |
|
7220 // Bind visibility of the publish settings button to whether the section is active. |
|
7221 updateButtonsState = function() { |
|
7222 publishSettingsBtn.toggle( section.active.get() ); |
|
7223 saveBtn.toggleClass( 'has-next-sibling', section.active.get() ); |
|
7224 }; |
|
7225 updateButtonsState(); |
|
7226 section.active.bind( updateButtonsState ); |
|
7227 |
|
7228 function highlightScheduleButton() { |
|
7229 if ( ! cancelScheduleButtonReminder ) { |
|
7230 cancelScheduleButtonReminder = api.utils.highlightButton( btnWrapper, { |
|
7231 delay: 1000, |
|
7232 |
|
7233 // Only abort the reminder when the save button is focused. |
|
7234 // If the user clicks the settings button to toggle the |
|
7235 // settings closed, we'll still remind them. |
|
7236 focusTarget: saveBtn |
|
7237 } ); |
|
7238 } |
|
7239 } |
|
7240 function cancelHighlightScheduleButton() { |
|
7241 if ( cancelScheduleButtonReminder ) { |
|
7242 cancelScheduleButtonReminder(); |
|
7243 cancelScheduleButtonReminder = null; |
|
7244 } |
|
7245 } |
|
7246 api.state( 'selectedChangesetStatus' ).bind( cancelHighlightScheduleButton ); |
|
7247 |
|
7248 section.contentContainer.find( '.customize-action' ).text( api.l10n.updating ); |
|
7249 section.contentContainer.find( '.customize-section-back' ).removeAttr( 'tabindex' ); |
|
7250 publishSettingsBtn.prop( 'disabled', false ); |
|
7251 |
|
7252 publishSettingsBtn.on( 'click', function( event ) { |
|
7253 event.preventDefault(); |
|
7254 section.expanded.set( ! section.expanded.get() ); |
|
7255 } ); |
|
7256 |
|
7257 section.expanded.bind( function( isExpanded ) { |
|
7258 var defaultChangesetStatus; |
|
7259 publishSettingsBtn.attr( 'aria-expanded', String( isExpanded ) ); |
|
7260 publishSettingsBtn.toggleClass( 'active', isExpanded ); |
|
7261 |
|
7262 if ( isExpanded ) { |
|
7263 cancelHighlightScheduleButton(); |
|
7264 return; |
|
7265 } |
|
7266 |
|
7267 defaultChangesetStatus = api.state( 'changesetStatus' ).get(); |
|
7268 if ( '' === defaultChangesetStatus || 'auto-draft' === defaultChangesetStatus ) { |
|
7269 defaultChangesetStatus = 'publish'; |
|
7270 } |
|
7271 |
|
7272 if ( api.state( 'selectedChangesetStatus' ).get() !== defaultChangesetStatus ) { |
|
7273 highlightScheduleButton(); |
|
7274 } else if ( 'future' === api.state( 'selectedChangesetStatus' ).get() && api.state( 'selectedChangesetDate' ).get() !== api.state( 'changesetDate' ).get() ) { |
|
7275 highlightScheduleButton(); |
|
7276 } |
|
7277 } ); |
|
7278 |
|
7279 statusControl = new api.Control( 'changeset_status', { |
|
7280 priority: 10, |
|
7281 type: 'radio', |
|
7282 section: 'publish_settings', |
|
7283 setting: api.state( 'selectedChangesetStatus' ), |
|
7284 templateId: 'customize-selected-changeset-status-control', |
|
7285 label: api.l10n.action, |
|
7286 choices: api.settings.changeset.statusChoices |
|
7287 } ); |
|
7288 api.control.add( statusControl ); |
|
7289 |
|
7290 dateControl = new api.DateTimeControl( 'changeset_scheduled_date', { |
|
7291 priority: 20, |
|
7292 section: 'publish_settings', |
|
7293 setting: api.state( 'selectedChangesetDate' ), |
|
7294 minYear: ( new Date() ).getFullYear(), |
|
7295 allowPastDate: false, |
|
7296 includeTime: true, |
|
7297 twelveHourFormat: /a/i.test( api.settings.timeFormat ), |
|
7298 description: api.l10n.scheduleDescription |
|
7299 } ); |
|
7300 dateControl.notifications.alt = true; |
|
7301 api.control.add( dateControl ); |
|
7302 |
|
7303 publishWhenTime = function() { |
|
7304 api.state( 'selectedChangesetStatus' ).set( 'publish' ); |
|
7305 api.previewer.save(); |
|
7306 }; |
|
7307 |
|
7308 // Start countdown for when the dateTime arrives, or clear interval when it is . |
|
7309 updateTimeArrivedPoller = function() { |
|
7310 var shouldPoll = ( |
|
7311 'future' === api.state( 'changesetStatus' ).get() && |
|
7312 'future' === api.state( 'selectedChangesetStatus' ).get() && |
|
7313 api.state( 'changesetDate' ).get() && |
|
7314 api.state( 'selectedChangesetDate' ).get() === api.state( 'changesetDate' ).get() && |
|
7315 api.utils.getRemainingTime( api.state( 'changesetDate' ).get() ) >= 0 |
|
7316 ); |
|
7317 |
|
7318 if ( shouldPoll && ! pollInterval ) { |
|
7319 pollInterval = setInterval( function() { |
|
7320 var remainingTime = api.utils.getRemainingTime( api.state( 'changesetDate' ).get() ); |
|
7321 api.state( 'remainingTimeToPublish' ).set( remainingTime ); |
|
7322 if ( remainingTime <= 0 ) { |
|
7323 clearInterval( pollInterval ); |
|
7324 pollInterval = 0; |
|
7325 publishWhenTime(); |
|
7326 } |
|
7327 }, timeArrivedPollingInterval ); |
|
7328 } else if ( ! shouldPoll && pollInterval ) { |
|
7329 clearInterval( pollInterval ); |
|
7330 pollInterval = 0; |
|
7331 } |
|
7332 }; |
|
7333 |
|
7334 api.state( 'changesetDate' ).bind( updateTimeArrivedPoller ); |
|
7335 api.state( 'selectedChangesetDate' ).bind( updateTimeArrivedPoller ); |
|
7336 api.state( 'changesetStatus' ).bind( updateTimeArrivedPoller ); |
|
7337 api.state( 'selectedChangesetStatus' ).bind( updateTimeArrivedPoller ); |
|
7338 updateTimeArrivedPoller(); |
|
7339 |
|
7340 // Ensure dateControl only appears when selected status is future. |
|
7341 dateControl.active.validate = function() { |
|
7342 return 'future' === api.state( 'selectedChangesetStatus' ).get(); |
|
7343 }; |
|
7344 toggleDateControl = function( value ) { |
|
7345 dateControl.active.set( 'future' === value ); |
|
7346 }; |
|
7347 toggleDateControl( api.state( 'selectedChangesetStatus' ).get() ); |
|
7348 api.state( 'selectedChangesetStatus' ).bind( toggleDateControl ); |
|
7349 |
|
7350 // Show notification on date control when status is future but it isn't a future date. |
|
7351 api.state( 'saving' ).bind( function( isSaving ) { |
|
7352 if ( isSaving && 'future' === api.state( 'selectedChangesetStatus' ).get() ) { |
|
7353 dateControl.toggleFutureDateNotification( ! dateControl.isFutureDate() ); |
|
7354 } |
|
7355 } ); |
|
7356 } ); |
2579 |
7357 |
2580 // Prevent the form from saving when enter is pressed on an input or select element. |
7358 // Prevent the form from saving when enter is pressed on an input or select element. |
2581 $('#customize-controls').on( 'keydown', function( e ) { |
7359 $('#customize-controls').on( 'keydown', function( e ) { |
2582 var isEnter = ( 13 === e.which ), |
7360 var isEnter = ( 13 === e.which ), |
2583 $el = $( e.target ); |
7361 $el = $( e.target ); |
2586 e.preventDefault(); |
7364 e.preventDefault(); |
2587 } |
7365 } |
2588 }); |
7366 }); |
2589 |
7367 |
2590 // Expand/Collapse the main customizer customize info. |
7368 // Expand/Collapse the main customizer customize info. |
2591 $( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( event ) { |
7369 $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() { |
2592 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
7370 var section = $( this ).closest( '.accordion-section' ), |
2593 return; |
7371 content = section.find( '.customize-panel-description:first' ); |
2594 } |
|
2595 event.preventDefault(); // Keep this AFTER the key filter above |
|
2596 |
|
2597 var section = $( this ).parent(), |
|
2598 content = section.find( '.accordion-section-content:first' ); |
|
2599 |
7372 |
2600 if ( section.hasClass( 'cannot-expand' ) ) { |
7373 if ( section.hasClass( 'cannot-expand' ) ) { |
2601 return; |
7374 return; |
2602 } |
7375 } |
2603 |
7376 |
2604 if ( section.hasClass( 'open' ) ) { |
7377 if ( section.hasClass( 'open' ) ) { |
2605 section.toggleClass( 'open' ); |
7378 section.toggleClass( 'open' ); |
2606 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration ); |
7379 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration, function() { |
|
7380 content.trigger( 'toggled' ); |
|
7381 } ); |
|
7382 $( this ).attr( 'aria-expanded', false ); |
2607 } else { |
7383 } else { |
2608 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration ); |
7384 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration, function() { |
|
7385 content.trigger( 'toggled' ); |
|
7386 } ); |
2609 section.toggleClass( 'open' ); |
7387 section.toggleClass( 'open' ); |
|
7388 $( this ).attr( 'aria-expanded', true ); |
2610 } |
7389 } |
2611 }); |
7390 }); |
2612 |
7391 |
2613 // Initialize Previewer |
7392 // Initialize Previewer |
2614 api.previewer = new api.Previewer({ |
7393 api.previewer = new api.Previewer({ |
2615 container: '#customize-preview', |
7394 container: '#customize-preview', |
2616 form: '#customize-controls', |
7395 form: '#customize-controls', |
2617 previewUrl: api.settings.url.preview, |
7396 previewUrl: api.settings.url.preview, |
2618 allowedUrls: api.settings.url.allowed, |
7397 allowedUrls: api.settings.url.allowed |
2619 signature: 'WP_CUSTOMIZER_SIGNATURE' |
|
2620 }, { |
7398 }, { |
2621 |
7399 |
2622 nonce: api.settings.nonce, |
7400 nonce: api.settings.nonce, |
2623 |
7401 |
2624 query: function() { |
7402 /** |
2625 var dirtyCustomized = {}; |
7403 * Build the query to send along with the Preview request. |
2626 api.each( function ( value, key ) { |
7404 * |
2627 if ( value._dirty ) { |
7405 * @since 3.4.0 |
2628 dirtyCustomized[ key ] = value(); |
7406 * @since 4.7.0 Added options param. |
2629 } |
7407 * @access public |
2630 } ); |
7408 * |
2631 |
7409 * @param {object} [options] Options. |
2632 return { |
7410 * @param {boolean} [options.excludeCustomizedSaved=false] Exclude saved settings in customized response (values pending writing to changeset). |
|
7411 * @return {object} Query vars. |
|
7412 */ |
|
7413 query: function( options ) { |
|
7414 var queryVars = { |
2633 wp_customize: 'on', |
7415 wp_customize: 'on', |
2634 theme: api.settings.theme.stylesheet, |
7416 customize_theme: api.settings.theme.stylesheet, |
2635 customized: JSON.stringify( dirtyCustomized ), |
7417 nonce: this.nonce.preview, |
2636 nonce: this.nonce.preview |
7418 customize_changeset_uuid: api.settings.changeset.uuid |
2637 }; |
7419 }; |
|
7420 if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) { |
|
7421 queryVars.customize_autosaved = 'on'; |
|
7422 } |
|
7423 |
|
7424 /* |
|
7425 * Exclude customized data if requested especially for calls to requestChangesetUpdate. |
|
7426 * Changeset updates are differential and so it is a performance waste to send all of |
|
7427 * the dirty settings with each update. |
|
7428 */ |
|
7429 queryVars.customized = JSON.stringify( api.dirtyValues( { |
|
7430 unsaved: options && options.excludeCustomizedSaved |
|
7431 } ) ); |
|
7432 |
|
7433 return queryVars; |
2638 }, |
7434 }, |
2639 |
7435 |
2640 save: function() { |
7436 /** |
2641 var self = this, |
7437 * Save (and publish) the customizer changeset. |
|
7438 * |
|
7439 * Updates to the changeset are transactional. If any of the settings |
|
7440 * are invalid then none of them will be written into the changeset. |
|
7441 * A revision will be made for the changeset post if revisions support |
|
7442 * has been added to the post type. |
|
7443 * |
|
7444 * @since 3.4.0 |
|
7445 * @since 4.7.0 Added args param and return value. |
|
7446 * |
|
7447 * @param {object} [args] Args. |
|
7448 * @param {string} [args.status=publish] Status. |
|
7449 * @param {string} [args.date] Date, in local time in MySQL format. |
|
7450 * @param {string} [args.title] Title |
|
7451 * @returns {jQuery.promise} Promise. |
|
7452 */ |
|
7453 save: function( args ) { |
|
7454 var previewer = this, |
|
7455 deferred = $.Deferred(), |
|
7456 changesetStatus = api.state( 'selectedChangesetStatus' ).get(), |
|
7457 selectedChangesetDate = api.state( 'selectedChangesetDate' ).get(), |
2642 processing = api.state( 'processing' ), |
7458 processing = api.state( 'processing' ), |
2643 submitWhenDoneProcessing, |
7459 submitWhenDoneProcessing, |
2644 submit; |
7460 submit, |
2645 |
7461 modifiedWhileSaving = {}, |
2646 body.addClass( 'saving' ); |
7462 invalidSettings = [], |
|
7463 invalidControls = [], |
|
7464 invalidSettingLessControls = []; |
|
7465 |
|
7466 if ( args && args.status ) { |
|
7467 changesetStatus = args.status; |
|
7468 } |
|
7469 |
|
7470 if ( api.state( 'saving' ).get() ) { |
|
7471 deferred.reject( 'already_saving' ); |
|
7472 deferred.promise(); |
|
7473 } |
|
7474 |
|
7475 api.state( 'saving' ).set( true ); |
|
7476 |
|
7477 function captureSettingModifiedDuringSave( setting ) { |
|
7478 modifiedWhileSaving[ setting.id ] = true; |
|
7479 } |
2647 |
7480 |
2648 submit = function () { |
7481 submit = function () { |
2649 var request, query; |
7482 var request, query, settingInvalidities = {}, latestRevision = api._latestRevision, errorCode = 'client_side_error'; |
2650 query = $.extend( self.query(), { |
7483 |
2651 nonce: self.nonce.save |
7484 api.bind( 'change', captureSettingModifiedDuringSave ); |
|
7485 api.notifications.remove( errorCode ); |
|
7486 |
|
7487 /* |
|
7488 * Block saving if there are any settings that are marked as |
|
7489 * invalid from the client (not from the server). Focus on |
|
7490 * the control. |
|
7491 */ |
|
7492 api.each( function( setting ) { |
|
7493 setting.notifications.each( function( notification ) { |
|
7494 if ( 'error' === notification.type && ! notification.fromServer ) { |
|
7495 invalidSettings.push( setting.id ); |
|
7496 if ( ! settingInvalidities[ setting.id ] ) { |
|
7497 settingInvalidities[ setting.id ] = {}; |
|
7498 } |
|
7499 settingInvalidities[ setting.id ][ notification.code ] = notification; |
|
7500 } |
|
7501 } ); |
2652 } ); |
7502 } ); |
|
7503 |
|
7504 // Find all invalid setting less controls with notification type error. |
|
7505 api.control.each( function( control ) { |
|
7506 if ( ! control.setting || ! control.setting.id && control.active.get() ) { |
|
7507 control.notifications.each( function( notification ) { |
|
7508 if ( 'error' === notification.type ) { |
|
7509 invalidSettingLessControls.push( [ control ] ); |
|
7510 } |
|
7511 } ); |
|
7512 } |
|
7513 } ); |
|
7514 |
|
7515 invalidControls = _.union( invalidSettingLessControls, _.values( api.findControlsForSettings( invalidSettings ) ) ); |
|
7516 if ( ! _.isEmpty( invalidControls ) ) { |
|
7517 |
|
7518 invalidControls[0][0].focus(); |
|
7519 api.unbind( 'change', captureSettingModifiedDuringSave ); |
|
7520 |
|
7521 if ( invalidSettings.length ) { |
|
7522 api.notifications.add( new api.Notification( errorCode, { |
|
7523 message: ( 1 === invalidSettings.length ? api.l10n.saveBlockedError.singular : api.l10n.saveBlockedError.plural ).replace( /%s/g, String( invalidSettings.length ) ), |
|
7524 type: 'error', |
|
7525 dismissible: true, |
|
7526 saveFailure: true |
|
7527 } ) ); |
|
7528 } |
|
7529 |
|
7530 deferred.rejectWith( previewer, [ |
|
7531 { setting_invalidities: settingInvalidities } |
|
7532 ] ); |
|
7533 api.state( 'saving' ).set( false ); |
|
7534 return deferred.promise(); |
|
7535 } |
|
7536 |
|
7537 /* |
|
7538 * Note that excludeCustomizedSaved is intentionally false so that the entire |
|
7539 * set of customized data will be included if bypassed changeset update. |
|
7540 */ |
|
7541 query = $.extend( previewer.query( { excludeCustomizedSaved: false } ), { |
|
7542 nonce: previewer.nonce.save, |
|
7543 customize_changeset_status: changesetStatus |
|
7544 } ); |
|
7545 |
|
7546 if ( args && args.date ) { |
|
7547 query.customize_changeset_date = args.date; |
|
7548 } else if ( 'future' === changesetStatus && selectedChangesetDate ) { |
|
7549 query.customize_changeset_date = selectedChangesetDate; |
|
7550 } |
|
7551 |
|
7552 if ( args && args.title ) { |
|
7553 query.customize_changeset_title = args.title; |
|
7554 } |
|
7555 |
|
7556 // Allow plugins to modify the params included with the save request. |
|
7557 api.trigger( 'save-request-params', query ); |
|
7558 |
|
7559 /* |
|
7560 * Note that the dirty customized values will have already been set in the |
|
7561 * changeset and so technically query.customized could be deleted. However, |
|
7562 * it is remaining here to make sure that any settings that got updated |
|
7563 * quietly which may have not triggered an update request will also get |
|
7564 * included in the values that get saved to the changeset. This will ensure |
|
7565 * that values that get injected via the saved event will be included in |
|
7566 * the changeset. This also ensures that setting values that were invalid |
|
7567 * will get re-validated, perhaps in the case of settings that are invalid |
|
7568 * due to dependencies on other settings. |
|
7569 */ |
2653 request = wp.ajax.post( 'customize_save', query ); |
7570 request = wp.ajax.post( 'customize_save', query ); |
|
7571 api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 ); |
2654 |
7572 |
2655 api.trigger( 'save', request ); |
7573 api.trigger( 'save', request ); |
2656 |
7574 |
2657 request.always( function () { |
7575 request.always( function () { |
2658 body.removeClass( 'saving' ); |
7576 api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 ); |
|
7577 api.state( 'saving' ).set( false ); |
|
7578 api.unbind( 'change', captureSettingModifiedDuringSave ); |
2659 } ); |
7579 } ); |
2660 |
7580 |
|
7581 // Remove notifications that were added due to save failures. |
|
7582 api.notifications.each( function( notification ) { |
|
7583 if ( notification.saveFailure ) { |
|
7584 api.notifications.remove( notification.code ); |
|
7585 } |
|
7586 }); |
|
7587 |
2661 request.fail( function ( response ) { |
7588 request.fail( function ( response ) { |
|
7589 var notification, notificationArgs; |
|
7590 notificationArgs = { |
|
7591 type: 'error', |
|
7592 dismissible: true, |
|
7593 fromServer: true, |
|
7594 saveFailure: true |
|
7595 }; |
|
7596 |
2662 if ( '0' === response ) { |
7597 if ( '0' === response ) { |
2663 response = 'not_logged_in'; |
7598 response = 'not_logged_in'; |
2664 } else if ( '-1' === response ) { |
7599 } else if ( '-1' === response ) { |
2665 // Back-compat in case any other check_ajax_referer() call is dying |
7600 // Back-compat in case any other check_ajax_referer() call is dying |
2666 response = 'invalid_nonce'; |
7601 response = 'invalid_nonce'; |
2667 } |
7602 } |
2668 |
7603 |
2669 if ( 'invalid_nonce' === response ) { |
7604 if ( 'invalid_nonce' === response ) { |
2670 self.cheatin(); |
7605 previewer.cheatin(); |
2671 } else if ( 'not_logged_in' === response ) { |
7606 } else if ( 'not_logged_in' === response ) { |
2672 self.preview.iframe.hide(); |
7607 previewer.preview.iframe.hide(); |
2673 self.login().done( function() { |
7608 previewer.login().done( function() { |
2674 self.save(); |
7609 previewer.save(); |
2675 self.preview.iframe.show(); |
7610 previewer.preview.iframe.show(); |
|
7611 } ); |
|
7612 } else if ( response.code ) { |
|
7613 if ( 'not_future_date' === response.code && api.section.has( 'publish_settings' ) && api.section( 'publish_settings' ).active.get() && api.control.has( 'changeset_scheduled_date' ) ) { |
|
7614 api.control( 'changeset_scheduled_date' ).toggleFutureDateNotification( true ).focus(); |
|
7615 } else if ( 'changeset_locked' !== response.code ) { |
|
7616 notification = new api.Notification( response.code, _.extend( notificationArgs, { |
|
7617 message: response.message |
|
7618 } ) ); |
|
7619 } |
|
7620 } else { |
|
7621 notification = new api.Notification( 'unknown_error', _.extend( notificationArgs, { |
|
7622 message: api.l10n.unknownRequestFail |
|
7623 } ) ); |
|
7624 } |
|
7625 |
|
7626 if ( notification ) { |
|
7627 api.notifications.add( notification ); |
|
7628 } |
|
7629 |
|
7630 if ( response.setting_validities ) { |
|
7631 api._handleSettingValidities( { |
|
7632 settingValidities: response.setting_validities, |
|
7633 focusInvalidControl: true |
2676 } ); |
7634 } ); |
2677 } |
7635 } |
|
7636 |
|
7637 deferred.rejectWith( previewer, [ response ] ); |
2678 api.trigger( 'error', response ); |
7638 api.trigger( 'error', response ); |
|
7639 |
|
7640 // Start a new changeset if the underlying changeset was published. |
|
7641 if ( 'changeset_already_published' === response.code && response.next_changeset_uuid ) { |
|
7642 api.settings.changeset.uuid = response.next_changeset_uuid; |
|
7643 api.state( 'changesetStatus' ).set( '' ); |
|
7644 if ( api.settings.changeset.branching ) { |
|
7645 parent.send( 'changeset-uuid', api.settings.changeset.uuid ); |
|
7646 } |
|
7647 api.previewer.send( 'changeset-uuid', api.settings.changeset.uuid ); |
|
7648 } |
2679 } ); |
7649 } ); |
2680 |
7650 |
2681 request.done( function( response ) { |
7651 request.done( function( response ) { |
2682 // Clear setting dirty states |
7652 |
2683 api.each( function ( value ) { |
7653 previewer.send( 'saved', response ); |
2684 value._dirty = false; |
7654 |
2685 } ); |
7655 api.state( 'changesetStatus' ).set( response.changeset_status ); |
2686 |
7656 if ( response.changeset_date ) { |
|
7657 api.state( 'changesetDate' ).set( response.changeset_date ); |
|
7658 } |
|
7659 |
|
7660 if ( 'publish' === response.changeset_status ) { |
|
7661 |
|
7662 // Mark all published as clean if they haven't been modified during the request. |
|
7663 api.each( function( setting ) { |
|
7664 /* |
|
7665 * Note that the setting revision will be undefined in the case of setting |
|
7666 * values that are marked as dirty when the customizer is loaded, such as |
|
7667 * when applying starter content. All other dirty settings will have an |
|
7668 * associated revision due to their modification triggering a change event. |
|
7669 */ |
|
7670 if ( setting._dirty && ( _.isUndefined( api._latestSettingRevisions[ setting.id ] ) || api._latestSettingRevisions[ setting.id ] <= latestRevision ) ) { |
|
7671 setting._dirty = false; |
|
7672 } |
|
7673 } ); |
|
7674 |
|
7675 api.state( 'changesetStatus' ).set( '' ); |
|
7676 api.settings.changeset.uuid = response.next_changeset_uuid; |
|
7677 if ( api.settings.changeset.branching ) { |
|
7678 parent.send( 'changeset-uuid', api.settings.changeset.uuid ); |
|
7679 } |
|
7680 } |
|
7681 |
|
7682 // Prevent subsequent requestChangesetUpdate() calls from including the settings that have been saved. |
|
7683 api._lastSavedRevision = Math.max( latestRevision, api._lastSavedRevision ); |
|
7684 |
|
7685 if ( response.setting_validities ) { |
|
7686 api._handleSettingValidities( { |
|
7687 settingValidities: response.setting_validities, |
|
7688 focusInvalidControl: true |
|
7689 } ); |
|
7690 } |
|
7691 |
|
7692 deferred.resolveWith( previewer, [ response ] ); |
2687 api.trigger( 'saved', response ); |
7693 api.trigger( 'saved', response ); |
|
7694 |
|
7695 // Restore the global dirty state if any settings were modified during save. |
|
7696 if ( ! _.isEmpty( modifiedWhileSaving ) ) { |
|
7697 api.state( 'saved' ).set( false ); |
|
7698 } |
2688 } ); |
7699 } ); |
2689 }; |
7700 }; |
2690 |
7701 |
2691 if ( 0 === processing() ) { |
7702 if ( 0 === processing() ) { |
2692 submit(); |
7703 submit(); |
2710 |
7825 |
2711 // Refresh the nonces if login sends updated nonces over. |
7826 // Refresh the nonces if login sends updated nonces over. |
2712 api.bind( 'nonce-refresh', function( nonce ) { |
7827 api.bind( 'nonce-refresh', function( nonce ) { |
2713 $.extend( api.settings.nonce, nonce ); |
7828 $.extend( api.settings.nonce, nonce ); |
2714 $.extend( api.previewer.nonce, nonce ); |
7829 $.extend( api.previewer.nonce, nonce ); |
|
7830 api.previewer.send( 'nonce-refresh', nonce ); |
2715 }); |
7831 }); |
2716 |
7832 |
2717 // Create Settings |
7833 // Create Settings |
2718 $.each( api.settings.settings, function( id, data ) { |
7834 $.each( api.settings.settings, function( id, data ) { |
2719 api.create( id, id, data.value, { |
7835 var Constructor = api.settingConstructor[ data.type ] || api.Setting; |
|
7836 api.add( new Constructor( id, data.value, { |
2720 transport: data.transport, |
7837 transport: data.transport, |
2721 previewer: api.previewer, |
7838 previewer: api.previewer, |
2722 dirty: !! data.dirty |
7839 dirty: !! data.dirty |
2723 } ); |
7840 } ) ); |
2724 }); |
7841 }); |
2725 |
7842 |
2726 // Create Panels |
7843 // Create Panels |
2727 $.each( api.settings.panels, function ( id, data ) { |
7844 $.each( api.settings.panels, function ( id, data ) { |
2728 var constructor = api.panelConstructor[ data.type ] || api.Panel, |
7845 var Constructor = api.panelConstructor[ data.type ] || api.Panel, options; |
2729 panel; |
7846 options = _.extend( { params: data }, data ); // Inclusion of params alias is for back-compat for custom panels that expect to augment this property. |
2730 |
7847 api.panel.add( new Constructor( id, options ) ); |
2731 panel = new constructor( id, { |
|
2732 params: data |
|
2733 } ); |
|
2734 api.panel.add( id, panel ); |
|
2735 }); |
7848 }); |
2736 |
7849 |
2737 // Create Sections |
7850 // Create Sections |
2738 $.each( api.settings.sections, function ( id, data ) { |
7851 $.each( api.settings.sections, function ( id, data ) { |
2739 var constructor = api.sectionConstructor[ data.type ] || api.Section, |
7852 var Constructor = api.sectionConstructor[ data.type ] || api.Section, options; |
2740 section; |
7853 options = _.extend( { params: data }, data ); // Inclusion of params alias is for back-compat for custom sections that expect to augment this property. |
2741 |
7854 api.section.add( new Constructor( id, options ) ); |
2742 section = new constructor( id, { |
|
2743 params: data |
|
2744 } ); |
|
2745 api.section.add( id, section ); |
|
2746 }); |
7855 }); |
2747 |
7856 |
2748 // Create Controls |
7857 // Create Controls |
2749 $.each( api.settings.controls, function( id, data ) { |
7858 $.each( api.settings.controls, function( id, data ) { |
2750 var constructor = api.controlConstructor[ data.type ] || api.Control, |
7859 var Constructor = api.controlConstructor[ data.type ] || api.Control, options; |
2751 control; |
7860 options = _.extend( { params: data }, data ); // Inclusion of params alias is for back-compat for custom controls that expect to augment this property. |
2752 |
7861 api.control.add( new Constructor( id, options ) ); |
2753 control = new constructor( id, { |
|
2754 params: data, |
|
2755 previewer: api.previewer |
|
2756 } ); |
|
2757 api.control.add( id, control ); |
|
2758 }); |
7862 }); |
2759 |
7863 |
2760 // Focus the autofocused element |
7864 // Focus the autofocused element |
2761 _.each( [ 'panel', 'section', 'control' ], function ( type ) { |
7865 _.each( [ 'panel', 'section', 'control' ], function( type ) { |
2762 var instance, id = api.settings.autofocus[ type ]; |
7866 var id = api.settings.autofocus[ type ]; |
2763 if ( id && api[ type ]( id ) ) { |
7867 if ( ! id ) { |
2764 instance = api[ type ]( id ); |
7868 return; |
2765 // Wait until the element is embedded in the DOM |
7869 } |
2766 instance.deferred.embedded.done( function () { |
7870 |
2767 // Wait until the preview has activated and so active panels, sections, controls have been set |
7871 /* |
2768 api.previewer.deferred.active.done( function () { |
7872 * Defer focus until: |
|
7873 * 1. The panel, section, or control exists (especially for dynamically-created ones). |
|
7874 * 2. The instance is embedded in the document (and so is focusable). |
|
7875 * 3. The preview has finished loading so that the active states have been set. |
|
7876 */ |
|
7877 api[ type ]( id, function( instance ) { |
|
7878 instance.deferred.embedded.done( function() { |
|
7879 api.previewer.deferred.active.done( function() { |
2769 instance.focus(); |
7880 instance.focus(); |
2770 }); |
7881 }); |
2771 }); |
7882 }); |
2772 } |
7883 }); |
2773 }); |
7884 }); |
2774 |
7885 |
2775 /** |
7886 api.bind( 'ready', api.reflowPaneContents ); |
2776 * Sort panels, sections, controls by priorities. Hide empty sections and panels. |
7887 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) { |
2777 * |
7888 var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, api.settings.timeouts.reflowPaneContents ); |
2778 * @since 4.1.0 |
7889 values.bind( 'add', debouncedReflowPaneContents ); |
2779 */ |
7890 values.bind( 'change', debouncedReflowPaneContents ); |
2780 api.reflowPaneContents = _.bind( function () { |
7891 values.bind( 'remove', debouncedReflowPaneContents ); |
2781 |
7892 } ); |
2782 var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false; |
7893 |
2783 |
7894 // Set up global notifications area. |
2784 if ( document.activeElement ) { |
7895 api.bind( 'ready', function setUpGlobalNotificationsArea() { |
2785 activeElement = $( document.activeElement ); |
7896 var sidebar, containerHeight, containerInitialTop; |
2786 } |
7897 api.notifications.container = $( '#customize-notifications-area' ); |
2787 |
7898 |
2788 // Sort the sections within each panel |
7899 api.notifications.bind( 'change', _.debounce( function() { |
2789 api.panel.each( function ( panel ) { |
7900 api.notifications.render(); |
2790 var sections = panel.sections(), |
7901 } ) ); |
2791 sectionContainers = _.pluck( sections, 'container' ); |
7902 |
2792 rootNodes.push( panel ); |
7903 sidebar = $( '.wp-full-overlay-sidebar-content' ); |
2793 appendContainer = panel.container.find( 'ul:first' ); |
7904 api.notifications.bind( 'rendered', function updateSidebarTop() { |
2794 if ( ! api.utils.areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) { |
7905 sidebar.css( 'top', '' ); |
2795 _( sections ).each( function ( section ) { |
7906 if ( 0 !== api.notifications.count() ) { |
2796 appendContainer.append( section.container ); |
7907 containerHeight = api.notifications.container.outerHeight() + 1; |
|
7908 containerInitialTop = parseInt( sidebar.css( 'top' ), 10 ); |
|
7909 sidebar.css( 'top', containerInitialTop + containerHeight + 'px' ); |
|
7910 } |
|
7911 api.notifications.trigger( 'sidebarTopUpdated' ); |
|
7912 }); |
|
7913 |
|
7914 api.notifications.render(); |
|
7915 }); |
|
7916 |
|
7917 // Save and activated states |
|
7918 (function( state ) { |
|
7919 var saved = state.instance( 'saved' ), |
|
7920 saving = state.instance( 'saving' ), |
|
7921 trashing = state.instance( 'trashing' ), |
|
7922 activated = state.instance( 'activated' ), |
|
7923 processing = state.instance( 'processing' ), |
|
7924 paneVisible = state.instance( 'paneVisible' ), |
|
7925 expandedPanel = state.instance( 'expandedPanel' ), |
|
7926 expandedSection = state.instance( 'expandedSection' ), |
|
7927 changesetStatus = state.instance( 'changesetStatus' ), |
|
7928 selectedChangesetStatus = state.instance( 'selectedChangesetStatus' ), |
|
7929 changesetDate = state.instance( 'changesetDate' ), |
|
7930 selectedChangesetDate = state.instance( 'selectedChangesetDate' ), |
|
7931 previewerAlive = state.instance( 'previewerAlive' ), |
|
7932 editShortcutVisibility = state.instance( 'editShortcutVisibility' ), |
|
7933 changesetLocked = state.instance( 'changesetLocked' ), |
|
7934 populateChangesetUuidParam, defaultSelectedChangesetStatus; |
|
7935 |
|
7936 state.bind( 'change', function() { |
|
7937 var canSave; |
|
7938 |
|
7939 if ( ! activated() ) { |
|
7940 saveBtn.val( api.l10n.activate ); |
|
7941 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); |
|
7942 |
|
7943 } else if ( '' === changesetStatus.get() && saved() ) { |
|
7944 if ( api.settings.changeset.currentUserCanPublish ) { |
|
7945 saveBtn.val( api.l10n.published ); |
|
7946 } else { |
|
7947 saveBtn.val( api.l10n.saved ); |
|
7948 } |
|
7949 closeBtn.find( '.screen-reader-text' ).text( api.l10n.close ); |
|
7950 |
|
7951 } else { |
|
7952 if ( 'draft' === selectedChangesetStatus() ) { |
|
7953 if ( saved() && selectedChangesetStatus() === changesetStatus() ) { |
|
7954 saveBtn.val( api.l10n.draftSaved ); |
|
7955 } else { |
|
7956 saveBtn.val( api.l10n.saveDraft ); |
|
7957 } |
|
7958 } else if ( 'future' === selectedChangesetStatus() ) { |
|
7959 if ( saved() && selectedChangesetStatus() === changesetStatus() ) { |
|
7960 if ( changesetDate.get() !== selectedChangesetDate.get() ) { |
|
7961 saveBtn.val( api.l10n.schedule ); |
|
7962 } else { |
|
7963 saveBtn.val( api.l10n.scheduled ); |
|
7964 } |
|
7965 } else { |
|
7966 saveBtn.val( api.l10n.schedule ); |
|
7967 } |
|
7968 } else if ( api.settings.changeset.currentUserCanPublish ) { |
|
7969 saveBtn.val( api.l10n.publish ); |
|
7970 } |
|
7971 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); |
|
7972 } |
|
7973 |
|
7974 /* |
|
7975 * Save (publish) button should be enabled if saving is not currently happening, |
|
7976 * and if the theme is not active or the changeset exists but is not published. |
|
7977 */ |
|
7978 canSave = ! saving() && ! trashing() && ! changesetLocked() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) ); |
|
7979 |
|
7980 saveBtn.prop( 'disabled', ! canSave ); |
|
7981 }); |
|
7982 |
|
7983 selectedChangesetStatus.validate = function( status ) { |
|
7984 if ( '' === status || 'auto-draft' === status ) { |
|
7985 return null; |
|
7986 } |
|
7987 return status; |
|
7988 }; |
|
7989 |
|
7990 defaultSelectedChangesetStatus = api.settings.changeset.currentUserCanPublish ? 'publish' : 'draft'; |
|
7991 |
|
7992 // Set default states. |
|
7993 changesetStatus( api.settings.changeset.status ); |
|
7994 changesetLocked( Boolean( api.settings.changeset.lockUser ) ); |
|
7995 changesetDate( api.settings.changeset.publishDate ); |
|
7996 selectedChangesetDate( api.settings.changeset.publishDate ); |
|
7997 selectedChangesetStatus( '' === api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ? defaultSelectedChangesetStatus : api.settings.changeset.status ); |
|
7998 selectedChangesetStatus.link( changesetStatus ); // Ensure that direct updates to status on server via wp.customizer.previewer.save() will update selection. |
|
7999 saved( true ); |
|
8000 if ( '' === changesetStatus() ) { // Handle case for loading starter content. |
|
8001 api.each( function( setting ) { |
|
8002 if ( setting._dirty ) { |
|
8003 saved( false ); |
|
8004 } |
|
8005 } ); |
|
8006 } |
|
8007 saving( false ); |
|
8008 activated( api.settings.theme.active ); |
|
8009 processing( 0 ); |
|
8010 paneVisible( true ); |
|
8011 expandedPanel( false ); |
|
8012 expandedSection( false ); |
|
8013 previewerAlive( true ); |
|
8014 editShortcutVisibility( 'visible' ); |
|
8015 |
|
8016 api.bind( 'change', function() { |
|
8017 if ( state( 'saved' ).get() ) { |
|
8018 state( 'saved' ).set( false ); |
|
8019 } |
|
8020 }); |
|
8021 |
|
8022 // Populate changeset UUID param when state becomes dirty. |
|
8023 if ( api.settings.changeset.branching ) { |
|
8024 saved.bind( function( isSaved ) { |
|
8025 if ( ! isSaved ) { |
|
8026 populateChangesetUuidParam( true ); |
|
8027 } |
|
8028 }); |
|
8029 } |
|
8030 |
|
8031 saving.bind( function( isSaving ) { |
|
8032 body.toggleClass( 'saving', isSaving ); |
|
8033 } ); |
|
8034 trashing.bind( function( isTrashing ) { |
|
8035 body.toggleClass( 'trashing', isTrashing ); |
|
8036 } ); |
|
8037 |
|
8038 api.bind( 'saved', function( response ) { |
|
8039 state('saved').set( true ); |
|
8040 if ( 'publish' === response.changeset_status ) { |
|
8041 state( 'activated' ).set( true ); |
|
8042 } |
|
8043 }); |
|
8044 |
|
8045 activated.bind( function( to ) { |
|
8046 if ( to ) { |
|
8047 api.trigger( 'activated' ); |
|
8048 } |
|
8049 }); |
|
8050 |
|
8051 /** |
|
8052 * Populate URL with UUID via `history.replaceState()`. |
|
8053 * |
|
8054 * @since 4.7.0 |
|
8055 * @access private |
|
8056 * |
|
8057 * @param {boolean} isIncluded Is UUID included. |
|
8058 * @returns {void} |
|
8059 */ |
|
8060 populateChangesetUuidParam = function( isIncluded ) { |
|
8061 var urlParser, queryParams; |
|
8062 |
|
8063 // Abort on IE9 which doesn't support history management. |
|
8064 if ( ! history.replaceState ) { |
|
8065 return; |
|
8066 } |
|
8067 |
|
8068 urlParser = document.createElement( 'a' ); |
|
8069 urlParser.href = location.href; |
|
8070 queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); |
|
8071 if ( isIncluded ) { |
|
8072 if ( queryParams.changeset_uuid === api.settings.changeset.uuid ) { |
|
8073 return; |
|
8074 } |
|
8075 queryParams.changeset_uuid = api.settings.changeset.uuid; |
|
8076 } else { |
|
8077 if ( ! queryParams.changeset_uuid ) { |
|
8078 return; |
|
8079 } |
|
8080 delete queryParams.changeset_uuid; |
|
8081 } |
|
8082 urlParser.search = $.param( queryParams ); |
|
8083 history.replaceState( {}, document.title, urlParser.href ); |
|
8084 }; |
|
8085 |
|
8086 // Show changeset UUID in URL when in branching mode and there is a saved changeset. |
|
8087 if ( api.settings.changeset.branching ) { |
|
8088 changesetStatus.bind( function( newStatus ) { |
|
8089 populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus && 'trash' !== newStatus ); |
|
8090 } ); |
|
8091 } |
|
8092 }( api.state ) ); |
|
8093 |
|
8094 /** |
|
8095 * Handles lock notice and take over request. |
|
8096 * |
|
8097 * @since 4.9.0 |
|
8098 */ |
|
8099 ( function checkAndDisplayLockNotice() { |
|
8100 |
|
8101 /** |
|
8102 * A notification that is displayed in a full-screen overlay with information about the locked changeset. |
|
8103 * |
|
8104 * @since 4.9.0 |
|
8105 * @class |
|
8106 * @augments wp.customize.Notification |
|
8107 * @augments wp.customize.OverlayNotification |
|
8108 */ |
|
8109 var LockedNotification = api.OverlayNotification.extend({ |
|
8110 |
|
8111 /** |
|
8112 * Template ID. |
|
8113 * |
|
8114 * @type {string} |
|
8115 */ |
|
8116 templateId: 'customize-changeset-locked-notification', |
|
8117 |
|
8118 /** |
|
8119 * Lock user. |
|
8120 * |
|
8121 * @type {object} |
|
8122 */ |
|
8123 lockUser: null, |
|
8124 |
|
8125 /** |
|
8126 * Initialize. |
|
8127 * |
|
8128 * @since 4.9.0 |
|
8129 * |
|
8130 * @param {string} [code] - Code. |
|
8131 * @param {object} [params] - Params. |
|
8132 */ |
|
8133 initialize: function( code, params ) { |
|
8134 var notification = this, _code, _params; |
|
8135 _code = code || 'changeset_locked'; |
|
8136 _params = _.extend( |
|
8137 { |
|
8138 type: 'warning', |
|
8139 containerClasses: '', |
|
8140 lockUser: {} |
|
8141 }, |
|
8142 params |
|
8143 ); |
|
8144 _params.containerClasses += ' notification-changeset-locked'; |
|
8145 api.OverlayNotification.prototype.initialize.call( notification, _code, _params ); |
|
8146 }, |
|
8147 |
|
8148 /** |
|
8149 * Render notification. |
|
8150 * |
|
8151 * @since 4.9.0 |
|
8152 * |
|
8153 * @return {jQuery} Notification container. |
|
8154 */ |
|
8155 render: function() { |
|
8156 var notification = this, li, data, takeOverButton, request; |
|
8157 data = _.extend( |
|
8158 { |
|
8159 allowOverride: false, |
|
8160 returnUrl: api.settings.url['return'], |
|
8161 previewUrl: api.previewer.previewUrl.get(), |
|
8162 frontendPreviewUrl: api.previewer.getFrontendPreviewUrl() |
|
8163 }, |
|
8164 this |
|
8165 ); |
|
8166 |
|
8167 li = api.OverlayNotification.prototype.render.call( data ); |
|
8168 |
|
8169 // Try to autosave the changeset now. |
|
8170 api.requestChangesetUpdate( {}, { autosave: true } ).fail( function( response ) { |
|
8171 if ( ! response.autosaved ) { |
|
8172 li.find( '.notice-error' ).prop( 'hidden', false ).text( response.message || api.l10n.unknownRequestFail ); |
|
8173 } |
2797 } ); |
8174 } ); |
2798 wasReflowed = true; |
8175 |
2799 } |
8176 takeOverButton = li.find( '.customize-notice-take-over-button' ); |
|
8177 takeOverButton.on( 'click', function( event ) { |
|
8178 event.preventDefault(); |
|
8179 if ( request ) { |
|
8180 return; |
|
8181 } |
|
8182 |
|
8183 takeOverButton.addClass( 'disabled' ); |
|
8184 request = wp.ajax.post( 'customize_override_changeset_lock', { |
|
8185 wp_customize: 'on', |
|
8186 customize_theme: api.settings.theme.stylesheet, |
|
8187 customize_changeset_uuid: api.settings.changeset.uuid, |
|
8188 nonce: api.settings.nonce.override_lock |
|
8189 } ); |
|
8190 |
|
8191 request.done( function() { |
|
8192 api.notifications.remove( notification.code ); // Remove self. |
|
8193 api.state( 'changesetLocked' ).set( false ); |
|
8194 } ); |
|
8195 |
|
8196 request.fail( function( response ) { |
|
8197 var message = response.message || api.l10n.unknownRequestFail; |
|
8198 li.find( '.notice-error' ).prop( 'hidden', false ).text( message ); |
|
8199 |
|
8200 request.always( function() { |
|
8201 takeOverButton.removeClass( 'disabled' ); |
|
8202 } ); |
|
8203 } ); |
|
8204 |
|
8205 request.always( function() { |
|
8206 request = null; |
|
8207 } ); |
|
8208 } ); |
|
8209 |
|
8210 return li; |
|
8211 } |
|
8212 }); |
|
8213 |
|
8214 /** |
|
8215 * Start lock. |
|
8216 * |
|
8217 * @since 4.9.0 |
|
8218 * |
|
8219 * @param {object} [args] - Args. |
|
8220 * @param {object} [args.lockUser] - Lock user data. |
|
8221 * @param {boolean} [args.allowOverride=false] - Whether override is allowed. |
|
8222 * @returns {void} |
|
8223 */ |
|
8224 function startLock( args ) { |
|
8225 if ( args && args.lockUser ) { |
|
8226 api.settings.changeset.lockUser = args.lockUser; |
|
8227 } |
|
8228 api.state( 'changesetLocked' ).set( true ); |
|
8229 api.notifications.add( new LockedNotification( 'changeset_locked', { |
|
8230 lockUser: api.settings.changeset.lockUser, |
|
8231 allowOverride: Boolean( args && args.allowOverride ) |
|
8232 } ) ); |
|
8233 } |
|
8234 |
|
8235 // Show initial notification. |
|
8236 if ( api.settings.changeset.lockUser ) { |
|
8237 startLock( { allowOverride: true } ); |
|
8238 } |
|
8239 |
|
8240 // Check for lock when sending heartbeat requests. |
|
8241 $( document ).on( 'heartbeat-send.update_lock_notice', function( event, data ) { |
|
8242 data.check_changeset_lock = true; |
|
8243 data.changeset_uuid = api.settings.changeset.uuid; |
2800 } ); |
8244 } ); |
2801 |
8245 |
2802 // Sort the controls within each section |
8246 // Handle heartbeat ticks. |
2803 api.section.each( function ( section ) { |
8247 $( document ).on( 'heartbeat-tick.update_lock_notice', function( event, data ) { |
2804 var controls = section.controls(), |
8248 var notification, code = 'changeset_locked'; |
2805 controlContainers = _.pluck( controls, 'container' ); |
8249 if ( ! data.customize_changeset_lock_user ) { |
2806 if ( ! section.panel() ) { |
8250 return; |
2807 rootNodes.push( section ); |
8251 } |
2808 } |
8252 |
2809 appendContainer = section.container.find( 'ul:first' ); |
8253 // Update notification when a different user takes over. |
2810 if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) { |
8254 notification = api.notifications( code ); |
2811 _( controls ).each( function ( control ) { |
8255 if ( notification && notification.lockUser.id !== api.settings.changeset.lockUser.id ) { |
2812 appendContainer.append( control.container ); |
8256 api.notifications.remove( code ); |
|
8257 } |
|
8258 |
|
8259 startLock( { |
|
8260 lockUser: data.customize_changeset_lock_user |
|
8261 } ); |
|
8262 } ); |
|
8263 |
|
8264 // Handle locking in response to changeset save errors. |
|
8265 api.bind( 'error', function( response ) { |
|
8266 if ( 'changeset_locked' === response.code && response.lock_user ) { |
|
8267 startLock( { |
|
8268 lockUser: response.lock_user |
2813 } ); |
8269 } ); |
2814 wasReflowed = true; |
|
2815 } |
8270 } |
2816 } ); |
8271 } ); |
2817 |
8272 } )(); |
2818 // Sort the root panels and sections |
8273 |
2819 rootNodes.sort( api.utils.prioritySort ); |
8274 // Set up initial notifications. |
2820 rootContainers = _.pluck( rootNodes, 'container' ); |
8275 (function() { |
2821 appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable |
8276 var removedQueryParams = [], autosaveDismissed = false; |
2822 if ( ! api.utils.areElementListsEqual( rootContainers, appendContainer.children() ) ) { |
8277 |
2823 _( rootNodes ).each( function ( rootNode ) { |
8278 /** |
2824 appendContainer.append( rootNode.container ); |
8279 * Obtain the URL to restore the autosave. |
|
8280 * |
|
8281 * @returns {string} Customizer URL. |
|
8282 */ |
|
8283 function getAutosaveRestorationUrl() { |
|
8284 var urlParser, queryParams; |
|
8285 urlParser = document.createElement( 'a' ); |
|
8286 urlParser.href = location.href; |
|
8287 queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); |
|
8288 if ( api.settings.changeset.latestAutoDraftUuid ) { |
|
8289 queryParams.changeset_uuid = api.settings.changeset.latestAutoDraftUuid; |
|
8290 } else { |
|
8291 queryParams.customize_autosaved = 'on'; |
|
8292 } |
|
8293 queryParams['return'] = api.settings.url['return']; |
|
8294 urlParser.search = $.param( queryParams ); |
|
8295 return urlParser.href; |
|
8296 } |
|
8297 |
|
8298 /** |
|
8299 * Remove parameter from the URL. |
|
8300 * |
|
8301 * @param {Array} params - Parameter names to remove. |
|
8302 * @returns {void} |
|
8303 */ |
|
8304 function stripParamsFromLocation( params ) { |
|
8305 var urlParser = document.createElement( 'a' ), queryParams, strippedParams = 0; |
|
8306 urlParser.href = location.href; |
|
8307 queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); |
|
8308 _.each( params, function( param ) { |
|
8309 if ( 'undefined' !== typeof queryParams[ param ] ) { |
|
8310 strippedParams += 1; |
|
8311 delete queryParams[ param ]; |
|
8312 } |
2825 } ); |
8313 } ); |
2826 wasReflowed = true; |
8314 if ( 0 === strippedParams ) { |
2827 } |
8315 return; |
2828 |
8316 } |
2829 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered |
8317 |
2830 api.panel.each( function ( panel ) { |
8318 urlParser.search = $.param( queryParams ); |
2831 var value = panel.active(); |
8319 history.replaceState( {}, document.title, urlParser.href ); |
2832 panel.active.callbacks.fireWith( panel.active, [ value, value ] ); |
8320 } |
2833 } ); |
8321 |
2834 api.section.each( function ( section ) { |
8322 /** |
2835 var value = section.active(); |
8323 * Dismiss autosave. |
2836 section.active.callbacks.fireWith( section.active, [ value, value ] ); |
8324 * |
2837 } ); |
8325 * @returns {void} |
2838 |
8326 */ |
2839 // Restore focus if there was a reflow and there was an active (focused) element |
8327 function dismissAutosave() { |
2840 if ( wasReflowed && activeElement ) { |
8328 if ( autosaveDismissed ) { |
2841 activeElement.focus(); |
8329 return; |
2842 } |
8330 } |
2843 }, api ); |
8331 wp.ajax.post( 'customize_dismiss_autosave_or_lock', { |
2844 api.bind( 'ready', api.reflowPaneContents ); |
8332 wp_customize: 'on', |
2845 api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 ); |
8333 customize_theme: api.settings.theme.stylesheet, |
2846 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) { |
8334 customize_changeset_uuid: api.settings.changeset.uuid, |
2847 values.bind( 'add', api.reflowPaneContents ); |
8335 nonce: api.settings.nonce.dismiss_autosave_or_lock, |
2848 values.bind( 'change', api.reflowPaneContents ); |
8336 dismiss_autosave: true |
2849 values.bind( 'remove', api.reflowPaneContents ); |
8337 } ); |
2850 } ); |
8338 autosaveDismissed = true; |
|
8339 } |
|
8340 |
|
8341 /** |
|
8342 * Add notification regarding the availability of an autosave to restore. |
|
8343 * |
|
8344 * @returns {void} |
|
8345 */ |
|
8346 function addAutosaveRestoreNotification() { |
|
8347 var code = 'autosave_available', onStateChange; |
|
8348 |
|
8349 // Since there is an autosave revision and the user hasn't loaded with autosaved, add notification to prompt to load autosaved version. |
|
8350 api.notifications.add( new api.Notification( code, { |
|
8351 message: api.l10n.autosaveNotice, |
|
8352 type: 'warning', |
|
8353 dismissible: true, |
|
8354 render: function() { |
|
8355 var li = api.Notification.prototype.render.call( this ), link; |
|
8356 |
|
8357 // Handle clicking on restoration link. |
|
8358 link = li.find( 'a' ); |
|
8359 link.prop( 'href', getAutosaveRestorationUrl() ); |
|
8360 link.on( 'click', function( event ) { |
|
8361 event.preventDefault(); |
|
8362 location.replace( getAutosaveRestorationUrl() ); |
|
8363 } ); |
|
8364 |
|
8365 // Handle dismissal of notice. |
|
8366 li.find( '.notice-dismiss' ).on( 'click', dismissAutosave ); |
|
8367 |
|
8368 return li; |
|
8369 } |
|
8370 } ) ); |
|
8371 |
|
8372 // Remove the notification once the user starts making changes. |
|
8373 onStateChange = function() { |
|
8374 dismissAutosave(); |
|
8375 api.notifications.remove( code ); |
|
8376 api.unbind( 'change', onStateChange ); |
|
8377 api.state( 'changesetStatus' ).unbind( onStateChange ); |
|
8378 }; |
|
8379 api.bind( 'change', onStateChange ); |
|
8380 api.state( 'changesetStatus' ).bind( onStateChange ); |
|
8381 } |
|
8382 |
|
8383 if ( api.settings.changeset.autosaved ) { |
|
8384 api.state( 'saved' ).set( false ); |
|
8385 removedQueryParams.push( 'customize_autosaved' ); |
|
8386 } |
|
8387 if ( ! api.settings.changeset.branching && ( ! api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ) ) { |
|
8388 removedQueryParams.push( 'changeset_uuid' ); // Remove UUID when restoring autosave auto-draft. |
|
8389 } |
|
8390 if ( removedQueryParams.length > 0 ) { |
|
8391 stripParamsFromLocation( removedQueryParams ); |
|
8392 } |
|
8393 if ( api.settings.changeset.latestAutoDraftUuid || api.settings.changeset.hasAutosaveRevision ) { |
|
8394 addAutosaveRestoreNotification(); |
|
8395 } |
|
8396 })(); |
2851 |
8397 |
2852 // Check if preview url is valid and load the preview frame. |
8398 // Check if preview url is valid and load the preview frame. |
2853 if ( api.previewer.previewUrl() ) { |
8399 if ( api.previewer.previewUrl() ) { |
2854 api.previewer.refresh(); |
8400 api.previewer.refresh(); |
2855 } else { |
8401 } else { |
2856 api.previewer.previewUrl( api.settings.url.home ); |
8402 api.previewer.previewUrl( api.settings.url.home ); |
2857 } |
8403 } |
2858 |
8404 |
2859 // Save and activated states |
|
2860 (function() { |
|
2861 var state = new api.Values(), |
|
2862 saved = state.create( 'saved' ), |
|
2863 activated = state.create( 'activated' ), |
|
2864 processing = state.create( 'processing' ); |
|
2865 |
|
2866 state.bind( 'change', function() { |
|
2867 if ( ! activated() ) { |
|
2868 saveBtn.val( api.l10n.activate ).prop( 'disabled', false ); |
|
2869 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); |
|
2870 |
|
2871 } else if ( saved() ) { |
|
2872 saveBtn.val( api.l10n.saved ).prop( 'disabled', true ); |
|
2873 closeBtn.find( '.screen-reader-text' ).text( api.l10n.close ); |
|
2874 |
|
2875 } else { |
|
2876 saveBtn.val( api.l10n.save ).prop( 'disabled', false ); |
|
2877 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); |
|
2878 } |
|
2879 }); |
|
2880 |
|
2881 // Set default states. |
|
2882 saved( true ); |
|
2883 activated( api.settings.theme.active ); |
|
2884 processing( 0 ); |
|
2885 |
|
2886 api.bind( 'change', function() { |
|
2887 state('saved').set( false ); |
|
2888 }); |
|
2889 |
|
2890 api.bind( 'saved', function() { |
|
2891 state('saved').set( true ); |
|
2892 state('activated').set( true ); |
|
2893 }); |
|
2894 |
|
2895 activated.bind( function( to ) { |
|
2896 if ( to ) |
|
2897 api.trigger( 'activated' ); |
|
2898 }); |
|
2899 |
|
2900 // Expose states to the API. |
|
2901 api.state = state; |
|
2902 }()); |
|
2903 |
|
2904 // Button bindings. |
8405 // Button bindings. |
2905 saveBtn.click( function( event ) { |
8406 saveBtn.click( function( event ) { |
2906 api.previewer.save(); |
8407 api.previewer.save(); |
2907 event.preventDefault(); |
8408 event.preventDefault(); |
2908 }).keydown( function( event ) { |
8409 }).keydown( function( event ) { |
2909 if ( 9 === event.which ) // tab |
8410 if ( 9 === event.which ) { // Tab. |
2910 return; |
8411 return; |
2911 if ( 13 === event.which ) // enter |
8412 } |
|
8413 if ( 13 === event.which ) { // Enter. |
2912 api.previewer.save(); |
8414 api.previewer.save(); |
|
8415 } |
2913 event.preventDefault(); |
8416 event.preventDefault(); |
2914 }); |
8417 }); |
2915 |
8418 |
2916 // Go back to the top-level Customizer accordion. |
8419 closeBtn.keydown( function( event ) { |
2917 $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( event ) { |
8420 if ( 9 === event.which ) { // Tab. |
2918 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
|
2919 return; |
8421 return; |
2920 } |
8422 } |
2921 |
8423 if ( 13 === event.which ) { // Enter. |
2922 event.preventDefault(); // Keep this AFTER the key filter above |
|
2923 api.panel.each( function ( panel ) { |
|
2924 panel.collapse(); |
|
2925 }); |
|
2926 }); |
|
2927 |
|
2928 closeBtn.keydown( function( event ) { |
|
2929 if ( 9 === event.which ) // tab |
|
2930 return; |
|
2931 if ( 13 === event.which ) // enter |
|
2932 this.click(); |
8424 this.click(); |
|
8425 } |
2933 event.preventDefault(); |
8426 event.preventDefault(); |
2934 }); |
8427 }); |
2935 |
8428 |
2936 $('.collapse-sidebar').on( 'click keydown', function( event ) { |
8429 $( '.collapse-sidebar' ).on( 'click', function() { |
2937 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
8430 api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() ); |
|
8431 }); |
|
8432 |
|
8433 api.state( 'paneVisible' ).bind( function( paneVisible ) { |
|
8434 overlay.toggleClass( 'preview-only', ! paneVisible ); |
|
8435 overlay.toggleClass( 'expanded', paneVisible ); |
|
8436 overlay.toggleClass( 'collapsed', ! paneVisible ); |
|
8437 |
|
8438 if ( ! paneVisible ) { |
|
8439 $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar }); |
|
8440 } else { |
|
8441 $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar }); |
|
8442 } |
|
8443 }); |
|
8444 |
|
8445 // Keyboard shortcuts - esc to exit section/panel. |
|
8446 body.on( 'keydown', function( event ) { |
|
8447 var collapsedObject, expandedControls = [], expandedSections = [], expandedPanels = []; |
|
8448 |
|
8449 if ( 27 !== event.which ) { // Esc. |
2938 return; |
8450 return; |
2939 } |
8451 } |
2940 |
8452 |
2941 overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' ); |
8453 /* |
2942 event.preventDefault(); |
8454 * Abort if the event target is not the body (the default) and not inside of #customize-controls. |
|
8455 * This ensures that ESC meant to collapse a modal dialog or a TinyMCE toolbar won't collapse something else. |
|
8456 */ |
|
8457 if ( ! $( event.target ).is( 'body' ) && ! $.contains( $( '#customize-controls' )[0], event.target ) ) { |
|
8458 return; |
|
8459 } |
|
8460 |
|
8461 // Check for expanded expandable controls (e.g. widgets and nav menus items), sections, and panels. |
|
8462 api.control.each( function( control ) { |
|
8463 if ( control.expanded && control.expanded() && _.isFunction( control.collapse ) ) { |
|
8464 expandedControls.push( control ); |
|
8465 } |
|
8466 }); |
|
8467 api.section.each( function( section ) { |
|
8468 if ( section.expanded() ) { |
|
8469 expandedSections.push( section ); |
|
8470 } |
|
8471 }); |
|
8472 api.panel.each( function( panel ) { |
|
8473 if ( panel.expanded() ) { |
|
8474 expandedPanels.push( panel ); |
|
8475 } |
|
8476 }); |
|
8477 |
|
8478 // Skip collapsing expanded controls if there are no expanded sections. |
|
8479 if ( expandedControls.length > 0 && 0 === expandedSections.length ) { |
|
8480 expandedControls.length = 0; |
|
8481 } |
|
8482 |
|
8483 // Collapse the most granular expanded object. |
|
8484 collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0]; |
|
8485 if ( collapsedObject ) { |
|
8486 if ( 'themes' === collapsedObject.params.type ) { |
|
8487 |
|
8488 // Themes panel or section. |
|
8489 if ( body.hasClass( 'modal-open' ) ) { |
|
8490 collapsedObject.closeDetails(); |
|
8491 } else if ( api.panel.has( 'themes' ) ) { |
|
8492 |
|
8493 // If we're collapsing a section, collapse the panel also. |
|
8494 api.panel( 'themes' ).collapse(); |
|
8495 } |
|
8496 return; |
|
8497 } |
|
8498 collapsedObject.collapse(); |
|
8499 event.preventDefault(); |
|
8500 } |
2943 }); |
8501 }); |
2944 |
8502 |
2945 $( '.customize-controls-preview-toggle' ).on( 'click keydown', function( event ) { |
8503 $( '.customize-controls-preview-toggle' ).on( 'click', function() { |
2946 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
8504 api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() ); |
2947 return; |
|
2948 } |
|
2949 |
|
2950 overlay.toggleClass( 'preview-only' ); |
|
2951 event.preventDefault(); |
|
2952 }); |
8505 }); |
|
8506 |
|
8507 /* |
|
8508 * Sticky header feature. |
|
8509 */ |
|
8510 (function initStickyHeaders() { |
|
8511 var parentContainer = $( '.wp-full-overlay-sidebar-content' ), |
|
8512 changeContainer, updateHeaderHeight, releaseStickyHeader, resetStickyHeader, positionStickyHeader, |
|
8513 activeHeader, lastScrollTop; |
|
8514 |
|
8515 /** |
|
8516 * Determine which panel or section is currently expanded. |
|
8517 * |
|
8518 * @since 4.7.0 |
|
8519 * @access private |
|
8520 * |
|
8521 * @param {wp.customize.Panel|wp.customize.Section} container Construct. |
|
8522 * @returns {void} |
|
8523 */ |
|
8524 changeContainer = function( container ) { |
|
8525 var newInstance = container, |
|
8526 expandedSection = api.state( 'expandedSection' ).get(), |
|
8527 expandedPanel = api.state( 'expandedPanel' ).get(), |
|
8528 headerElement; |
|
8529 |
|
8530 if ( activeHeader && activeHeader.element ) { |
|
8531 // Release previously active header element. |
|
8532 releaseStickyHeader( activeHeader.element ); |
|
8533 |
|
8534 // Remove event listener in the previous panel or section. |
|
8535 activeHeader.element.find( '.description' ).off( 'toggled', updateHeaderHeight ); |
|
8536 } |
|
8537 |
|
8538 if ( ! newInstance ) { |
|
8539 if ( ! expandedSection && expandedPanel && expandedPanel.contentContainer ) { |
|
8540 newInstance = expandedPanel; |
|
8541 } else if ( ! expandedPanel && expandedSection && expandedSection.contentContainer ) { |
|
8542 newInstance = expandedSection; |
|
8543 } else { |
|
8544 activeHeader = false; |
|
8545 return; |
|
8546 } |
|
8547 } |
|
8548 |
|
8549 headerElement = newInstance.contentContainer.find( '.customize-section-title, .panel-meta' ).first(); |
|
8550 if ( headerElement.length ) { |
|
8551 activeHeader = { |
|
8552 instance: newInstance, |
|
8553 element: headerElement, |
|
8554 parent: headerElement.closest( '.customize-pane-child' ), |
|
8555 height: headerElement.outerHeight() |
|
8556 }; |
|
8557 |
|
8558 // Update header height whenever help text is expanded or collapsed. |
|
8559 activeHeader.element.find( '.description' ).on( 'toggled', updateHeaderHeight ); |
|
8560 |
|
8561 if ( expandedSection ) { |
|
8562 resetStickyHeader( activeHeader.element, activeHeader.parent ); |
|
8563 } |
|
8564 } else { |
|
8565 activeHeader = false; |
|
8566 } |
|
8567 }; |
|
8568 api.state( 'expandedSection' ).bind( changeContainer ); |
|
8569 api.state( 'expandedPanel' ).bind( changeContainer ); |
|
8570 |
|
8571 // Throttled scroll event handler. |
|
8572 parentContainer.on( 'scroll', _.throttle( function() { |
|
8573 if ( ! activeHeader ) { |
|
8574 return; |
|
8575 } |
|
8576 |
|
8577 var scrollTop = parentContainer.scrollTop(), |
|
8578 scrollDirection; |
|
8579 |
|
8580 if ( ! lastScrollTop ) { |
|
8581 scrollDirection = 1; |
|
8582 } else { |
|
8583 if ( scrollTop === lastScrollTop ) { |
|
8584 scrollDirection = 0; |
|
8585 } else if ( scrollTop > lastScrollTop ) { |
|
8586 scrollDirection = 1; |
|
8587 } else { |
|
8588 scrollDirection = -1; |
|
8589 } |
|
8590 } |
|
8591 lastScrollTop = scrollTop; |
|
8592 if ( 0 !== scrollDirection ) { |
|
8593 positionStickyHeader( activeHeader, scrollTop, scrollDirection ); |
|
8594 } |
|
8595 }, 8 ) ); |
|
8596 |
|
8597 // Update header position on sidebar layout change. |
|
8598 api.notifications.bind( 'sidebarTopUpdated', function() { |
|
8599 if ( activeHeader && activeHeader.element.hasClass( 'is-sticky' ) ) { |
|
8600 activeHeader.element.css( 'top', parentContainer.css( 'top' ) ); |
|
8601 } |
|
8602 }); |
|
8603 |
|
8604 // Release header element if it is sticky. |
|
8605 releaseStickyHeader = function( headerElement ) { |
|
8606 if ( ! headerElement.hasClass( 'is-sticky' ) ) { |
|
8607 return; |
|
8608 } |
|
8609 headerElement |
|
8610 .removeClass( 'is-sticky' ) |
|
8611 .addClass( 'maybe-sticky is-in-view' ) |
|
8612 .css( 'top', parentContainer.scrollTop() + 'px' ); |
|
8613 }; |
|
8614 |
|
8615 // Reset position of the sticky header. |
|
8616 resetStickyHeader = function( headerElement, headerParent ) { |
|
8617 if ( headerElement.hasClass( 'is-in-view' ) ) { |
|
8618 headerElement |
|
8619 .removeClass( 'maybe-sticky is-in-view' ) |
|
8620 .css( { |
|
8621 width: '', |
|
8622 top: '' |
|
8623 } ); |
|
8624 headerParent.css( 'padding-top', '' ); |
|
8625 } |
|
8626 }; |
|
8627 |
|
8628 /** |
|
8629 * Update active header height. |
|
8630 * |
|
8631 * @since 4.7.0 |
|
8632 * @access private |
|
8633 * |
|
8634 * @returns {void} |
|
8635 */ |
|
8636 updateHeaderHeight = function() { |
|
8637 activeHeader.height = activeHeader.element.outerHeight(); |
|
8638 }; |
|
8639 |
|
8640 /** |
|
8641 * Reposition header on throttled `scroll` event. |
|
8642 * |
|
8643 * @since 4.7.0 |
|
8644 * @access private |
|
8645 * |
|
8646 * @param {object} header - Header. |
|
8647 * @param {number} scrollTop - Scroll top. |
|
8648 * @param {number} scrollDirection - Scroll direction, negative number being up and positive being down. |
|
8649 * @returns {void} |
|
8650 */ |
|
8651 positionStickyHeader = function( header, scrollTop, scrollDirection ) { |
|
8652 var headerElement = header.element, |
|
8653 headerParent = header.parent, |
|
8654 headerHeight = header.height, |
|
8655 headerTop = parseInt( headerElement.css( 'top' ), 10 ), |
|
8656 maybeSticky = headerElement.hasClass( 'maybe-sticky' ), |
|
8657 isSticky = headerElement.hasClass( 'is-sticky' ), |
|
8658 isInView = headerElement.hasClass( 'is-in-view' ), |
|
8659 isScrollingUp = ( -1 === scrollDirection ); |
|
8660 |
|
8661 // When scrolling down, gradually hide sticky header. |
|
8662 if ( ! isScrollingUp ) { |
|
8663 if ( isSticky ) { |
|
8664 headerTop = scrollTop; |
|
8665 headerElement |
|
8666 .removeClass( 'is-sticky' ) |
|
8667 .css( { |
|
8668 top: headerTop + 'px', |
|
8669 width: '' |
|
8670 } ); |
|
8671 } |
|
8672 if ( isInView && scrollTop > headerTop + headerHeight ) { |
|
8673 headerElement.removeClass( 'is-in-view' ); |
|
8674 headerParent.css( 'padding-top', '' ); |
|
8675 } |
|
8676 return; |
|
8677 } |
|
8678 |
|
8679 // Scrolling up. |
|
8680 if ( ! maybeSticky && scrollTop >= headerHeight ) { |
|
8681 maybeSticky = true; |
|
8682 headerElement.addClass( 'maybe-sticky' ); |
|
8683 } else if ( 0 === scrollTop ) { |
|
8684 // Reset header in base position. |
|
8685 headerElement |
|
8686 .removeClass( 'maybe-sticky is-in-view is-sticky' ) |
|
8687 .css( { |
|
8688 top: '', |
|
8689 width: '' |
|
8690 } ); |
|
8691 headerParent.css( 'padding-top', '' ); |
|
8692 return; |
|
8693 } |
|
8694 |
|
8695 if ( isInView && ! isSticky ) { |
|
8696 // Header is in the view but is not yet sticky. |
|
8697 if ( headerTop >= scrollTop ) { |
|
8698 // Header is fully visible. |
|
8699 headerElement |
|
8700 .addClass( 'is-sticky' ) |
|
8701 .css( { |
|
8702 top: parentContainer.css( 'top' ), |
|
8703 width: headerParent.outerWidth() + 'px' |
|
8704 } ); |
|
8705 } |
|
8706 } else if ( maybeSticky && ! isInView ) { |
|
8707 // Header is out of the view. |
|
8708 headerElement |
|
8709 .addClass( 'is-in-view' ) |
|
8710 .css( 'top', ( scrollTop - headerHeight ) + 'px' ); |
|
8711 headerParent.css( 'padding-top', headerHeight + 'px' ); |
|
8712 } |
|
8713 }; |
|
8714 }()); |
|
8715 |
|
8716 // Previewed device bindings. (The api.previewedDevice property is how this Value was first introduced, but since it has moved to api.state.) |
|
8717 api.previewedDevice = api.state( 'previewedDevice' ); |
|
8718 |
|
8719 // Set the default device. |
|
8720 api.bind( 'ready', function() { |
|
8721 _.find( api.settings.previewableDevices, function( value, key ) { |
|
8722 if ( true === value['default'] ) { |
|
8723 api.previewedDevice.set( key ); |
|
8724 return true; |
|
8725 } |
|
8726 } ); |
|
8727 } ); |
|
8728 |
|
8729 // Set the toggled device. |
|
8730 footerActions.find( '.devices button' ).on( 'click', function( event ) { |
|
8731 api.previewedDevice.set( $( event.currentTarget ).data( 'device' ) ); |
|
8732 }); |
|
8733 |
|
8734 // Bind device changes. |
|
8735 api.previewedDevice.bind( function( newDevice ) { |
|
8736 var overlay = $( '.wp-full-overlay' ), |
|
8737 devices = ''; |
|
8738 |
|
8739 footerActions.find( '.devices button' ) |
|
8740 .removeClass( 'active' ) |
|
8741 .attr( 'aria-pressed', false ); |
|
8742 |
|
8743 footerActions.find( '.devices .preview-' + newDevice ) |
|
8744 .addClass( 'active' ) |
|
8745 .attr( 'aria-pressed', true ); |
|
8746 |
|
8747 $.each( api.settings.previewableDevices, function( device ) { |
|
8748 devices += ' preview-' + device; |
|
8749 } ); |
|
8750 |
|
8751 overlay |
|
8752 .removeClass( devices ) |
|
8753 .addClass( 'preview-' + newDevice ); |
|
8754 } ); |
2953 |
8755 |
2954 // Bind site title display to the corresponding field. |
8756 // Bind site title display to the corresponding field. |
2955 if ( title.length ) { |
8757 if ( title.length ) { |
2956 $( '#customize-control-blogname input' ).on( 'input', function() { |
8758 api( 'blogname', function( setting ) { |
2957 title.text( this.value ); |
8759 var updateTitle = function() { |
|
8760 title.text( $.trim( setting() ) || api.l10n.untitledBlogName ); |
|
8761 }; |
|
8762 setting.bind( updateTitle ); |
|
8763 updateTitle(); |
2958 } ); |
8764 } ); |
2959 } |
8765 } |
2960 |
8766 |
2961 // Create a potential postMessage connection with the parent frame. |
8767 /* |
|
8768 * Create a postMessage connection with a parent frame, |
|
8769 * in case the Customizer frame was opened with the Customize loader. |
|
8770 * |
|
8771 * @see wp.customize.Loader |
|
8772 */ |
2962 parent = new api.Messenger({ |
8773 parent = new api.Messenger({ |
2963 url: api.settings.url.parent, |
8774 url: api.settings.url.parent, |
2964 channel: 'loader' |
8775 channel: 'loader' |
2965 }); |
8776 }); |
2966 |
8777 |
2967 // If we receive a 'back' event, we're inside an iframe. |
8778 // Handle exiting of Customizer. |
2968 // Send any clicks to the 'Return' link to the parent page. |
8779 (function() { |
2969 parent.bind( 'back', function() { |
8780 var isInsideIframe = false; |
|
8781 |
|
8782 function isCleanState() { |
|
8783 var defaultChangesetStatus; |
|
8784 |
|
8785 /* |
|
8786 * Handle special case of previewing theme switch since some settings (for nav menus and widgets) |
|
8787 * are pre-dirty and non-active themes can only ever be auto-drafts. |
|
8788 */ |
|
8789 if ( ! api.state( 'activated' ).get() ) { |
|
8790 return 0 === api._latestRevision; |
|
8791 } |
|
8792 |
|
8793 // Dirty if the changeset status has been changed but not saved yet. |
|
8794 defaultChangesetStatus = api.state( 'changesetStatus' ).get(); |
|
8795 if ( '' === defaultChangesetStatus || 'auto-draft' === defaultChangesetStatus ) { |
|
8796 defaultChangesetStatus = 'publish'; |
|
8797 } |
|
8798 if ( api.state( 'selectedChangesetStatus' ).get() !== defaultChangesetStatus ) { |
|
8799 return false; |
|
8800 } |
|
8801 |
|
8802 // Dirty if scheduled but the changeset date hasn't been saved yet. |
|
8803 if ( 'future' === api.state( 'selectedChangesetStatus' ).get() && api.state( 'selectedChangesetDate' ).get() !== api.state( 'changesetDate' ).get() ) { |
|
8804 return false; |
|
8805 } |
|
8806 |
|
8807 return api.state( 'saved' ).get() && 'auto-draft' !== api.state( 'changesetStatus' ).get(); |
|
8808 } |
|
8809 |
|
8810 /* |
|
8811 * If we receive a 'back' event, we're inside an iframe. |
|
8812 * Send any clicks to the 'Return' link to the parent page. |
|
8813 */ |
|
8814 parent.bind( 'back', function() { |
|
8815 isInsideIframe = true; |
|
8816 }); |
|
8817 |
|
8818 function startPromptingBeforeUnload() { |
|
8819 api.unbind( 'change', startPromptingBeforeUnload ); |
|
8820 api.state( 'selectedChangesetStatus' ).unbind( startPromptingBeforeUnload ); |
|
8821 api.state( 'selectedChangesetDate' ).unbind( startPromptingBeforeUnload ); |
|
8822 |
|
8823 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes |
|
8824 $( window ).on( 'beforeunload.customize-confirm', function() { |
|
8825 if ( ! isCleanState() && ! api.state( 'changesetLocked' ).get() ) { |
|
8826 setTimeout( function() { |
|
8827 overlay.removeClass( 'customize-loading' ); |
|
8828 }, 1 ); |
|
8829 return api.l10n.saveAlert; |
|
8830 } |
|
8831 }); |
|
8832 } |
|
8833 api.bind( 'change', startPromptingBeforeUnload ); |
|
8834 api.state( 'selectedChangesetStatus' ).bind( startPromptingBeforeUnload ); |
|
8835 api.state( 'selectedChangesetDate' ).bind( startPromptingBeforeUnload ); |
|
8836 |
|
8837 function requestClose() { |
|
8838 var clearedToClose = $.Deferred(), dismissAutoSave = false, dismissLock = false; |
|
8839 |
|
8840 if ( isCleanState() ) { |
|
8841 dismissLock = true; |
|
8842 } else if ( confirm( api.l10n.saveAlert ) ) { |
|
8843 |
|
8844 dismissLock = true; |
|
8845 |
|
8846 // Mark all settings as clean to prevent another call to requestChangesetUpdate. |
|
8847 api.each( function( setting ) { |
|
8848 setting._dirty = false; |
|
8849 }); |
|
8850 $( document ).off( 'visibilitychange.wp-customize-changeset-update' ); |
|
8851 $( window ).off( 'beforeunload.wp-customize-changeset-update' ); |
|
8852 |
|
8853 closeBtn.css( 'cursor', 'progress' ); |
|
8854 if ( '' !== api.state( 'changesetStatus' ).get() ) { |
|
8855 dismissAutoSave = true; |
|
8856 } |
|
8857 } else { |
|
8858 clearedToClose.reject(); |
|
8859 } |
|
8860 |
|
8861 if ( dismissLock || dismissAutoSave ) { |
|
8862 wp.ajax.send( 'customize_dismiss_autosave_or_lock', { |
|
8863 timeout: 500, // Don't wait too long. |
|
8864 data: { |
|
8865 wp_customize: 'on', |
|
8866 customize_theme: api.settings.theme.stylesheet, |
|
8867 customize_changeset_uuid: api.settings.changeset.uuid, |
|
8868 nonce: api.settings.nonce.dismiss_autosave_or_lock, |
|
8869 dismiss_autosave: dismissAutoSave, |
|
8870 dismiss_lock: dismissLock |
|
8871 } |
|
8872 } ).always( function() { |
|
8873 clearedToClose.resolve(); |
|
8874 } ); |
|
8875 } |
|
8876 |
|
8877 return clearedToClose.promise(); |
|
8878 } |
|
8879 |
|
8880 parent.bind( 'confirm-close', function() { |
|
8881 requestClose().done( function() { |
|
8882 parent.send( 'confirmed-close', true ); |
|
8883 } ).fail( function() { |
|
8884 parent.send( 'confirmed-close', false ); |
|
8885 } ); |
|
8886 } ); |
|
8887 |
2970 closeBtn.on( 'click.customize-controls-close', function( event ) { |
8888 closeBtn.on( 'click.customize-controls-close', function( event ) { |
2971 event.preventDefault(); |
8889 event.preventDefault(); |
2972 parent.send( 'close' ); |
8890 if ( isInsideIframe ) { |
|
8891 parent.send( 'close' ); // See confirm-close logic above. |
|
8892 } else { |
|
8893 requestClose().done( function() { |
|
8894 $( window ).off( 'beforeunload.customize-confirm' ); |
|
8895 window.location.href = closeBtn.prop( 'href' ); |
|
8896 } ); |
|
8897 } |
2973 }); |
8898 }); |
2974 }); |
8899 })(); |
2975 |
|
2976 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes |
|
2977 $( window ).on( 'beforeunload', function () { |
|
2978 if ( ! api.state( 'saved' )() ) { |
|
2979 setTimeout( function() { |
|
2980 overlay.removeClass( 'customize-loading' ); |
|
2981 }, 1 ); |
|
2982 return api.l10n.saveAlert; |
|
2983 } |
|
2984 } ); |
|
2985 |
8900 |
2986 // Pass events through to the parent. |
8901 // Pass events through to the parent. |
2987 $.each( [ 'saved', 'change' ], function ( i, event ) { |
8902 $.each( [ 'saved', 'change' ], function ( i, event ) { |
2988 api.bind( event, function() { |
8903 api.bind( event, function() { |
2989 parent.send( event ); |
8904 parent.send( event ); |
2990 }); |
8905 }); |
2991 } ); |
8906 } ); |
2992 |
8907 |
2993 // When activated, let the loader handle redirecting the page. |
|
2994 // If no loader exists, redirect the page ourselves (if a url exists). |
|
2995 api.bind( 'activated', function() { |
|
2996 if ( parent.targetWindow() ) |
|
2997 parent.send( 'activated', api.settings.url.activated ); |
|
2998 else if ( api.settings.url.activated ) |
|
2999 window.location = api.settings.url.activated; |
|
3000 }); |
|
3001 |
|
3002 // Pass titles to the parent |
8908 // Pass titles to the parent |
3003 api.bind( 'title', function( newTitle ) { |
8909 api.bind( 'title', function( newTitle ) { |
3004 parent.send( 'title', newTitle ); |
8910 parent.send( 'title', newTitle ); |
3005 }); |
8911 }); |
3006 |
8912 |
|
8913 if ( api.settings.changeset.branching ) { |
|
8914 parent.send( 'changeset-uuid', api.settings.changeset.uuid ); |
|
8915 } |
|
8916 |
3007 // Initialize the connection with the parent frame. |
8917 // Initialize the connection with the parent frame. |
3008 parent.send( 'ready' ); |
8918 parent.send( 'ready' ); |
3009 |
8919 |
3010 // Control visibility for default controls |
8920 // Control visibility for default controls |
3011 $.each({ |
8921 $.each({ |
3012 'background_image': { |
8922 'background_image': { |
3013 controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ], |
8923 controls: [ 'background_preset', 'background_position', 'background_size', 'background_repeat', 'background_attachment' ], |
3014 callback: function( to ) { return !! to; } |
8924 callback: function( to ) { return !! to; } |
3015 }, |
8925 }, |
3016 'show_on_front': { |
8926 'show_on_front': { |
3017 controls: [ 'page_on_front', 'page_for_posts' ], |
8927 controls: [ 'page_on_front', 'page_for_posts' ], |
3018 callback: function( to ) { return 'page' === to; } |
8928 callback: function( to ) { return 'page' === to; } |
3034 }); |
8944 }); |
3035 }); |
8945 }); |
3036 }); |
8946 }); |
3037 }); |
8947 }); |
3038 |
8948 |
|
8949 api.control( 'background_preset', function( control ) { |
|
8950 var visibility, defaultValues, values, toggleVisibility, updateSettings, preset; |
|
8951 |
|
8952 visibility = { // position, size, repeat, attachment |
|
8953 'default': [ false, false, false, false ], |
|
8954 'fill': [ true, false, false, false ], |
|
8955 'fit': [ true, false, true, false ], |
|
8956 'repeat': [ true, false, false, true ], |
|
8957 'custom': [ true, true, true, true ] |
|
8958 }; |
|
8959 |
|
8960 defaultValues = [ |
|
8961 _wpCustomizeBackground.defaults['default-position-x'], |
|
8962 _wpCustomizeBackground.defaults['default-position-y'], |
|
8963 _wpCustomizeBackground.defaults['default-size'], |
|
8964 _wpCustomizeBackground.defaults['default-repeat'], |
|
8965 _wpCustomizeBackground.defaults['default-attachment'] |
|
8966 ]; |
|
8967 |
|
8968 values = { // position_x, position_y, size, repeat, attachment |
|
8969 'default': defaultValues, |
|
8970 'fill': [ 'left', 'top', 'cover', 'no-repeat', 'fixed' ], |
|
8971 'fit': [ 'left', 'top', 'contain', 'no-repeat', 'fixed' ], |
|
8972 'repeat': [ 'left', 'top', 'auto', 'repeat', 'scroll' ] |
|
8973 }; |
|
8974 |
|
8975 // @todo These should actually toggle the active state, but without the preview overriding the state in data.activeControls. |
|
8976 toggleVisibility = function( preset ) { |
|
8977 _.each( [ 'background_position', 'background_size', 'background_repeat', 'background_attachment' ], function( controlId, i ) { |
|
8978 var control = api.control( controlId ); |
|
8979 if ( control ) { |
|
8980 control.container.toggle( visibility[ preset ][ i ] ); |
|
8981 } |
|
8982 } ); |
|
8983 }; |
|
8984 |
|
8985 updateSettings = function( preset ) { |
|
8986 _.each( [ 'background_position_x', 'background_position_y', 'background_size', 'background_repeat', 'background_attachment' ], function( settingId, i ) { |
|
8987 var setting = api( settingId ); |
|
8988 if ( setting ) { |
|
8989 setting.set( values[ preset ][ i ] ); |
|
8990 } |
|
8991 } ); |
|
8992 }; |
|
8993 |
|
8994 preset = control.setting.get(); |
|
8995 toggleVisibility( preset ); |
|
8996 |
|
8997 control.setting.bind( 'change', function( preset ) { |
|
8998 toggleVisibility( preset ); |
|
8999 if ( 'custom' !== preset ) { |
|
9000 updateSettings( preset ); |
|
9001 } |
|
9002 } ); |
|
9003 } ); |
|
9004 |
|
9005 api.control( 'background_repeat', function( control ) { |
|
9006 control.elements[0].unsync( api( 'background_repeat' ) ); |
|
9007 |
|
9008 control.element = new api.Element( control.container.find( 'input' ) ); |
|
9009 control.element.set( 'no-repeat' !== control.setting() ); |
|
9010 |
|
9011 control.element.bind( function( to ) { |
|
9012 control.setting.set( to ? 'repeat' : 'no-repeat' ); |
|
9013 } ); |
|
9014 |
|
9015 control.setting.bind( function( to ) { |
|
9016 control.element.set( 'no-repeat' !== to ); |
|
9017 } ); |
|
9018 } ); |
|
9019 |
|
9020 api.control( 'background_attachment', function( control ) { |
|
9021 control.elements[0].unsync( api( 'background_attachment' ) ); |
|
9022 |
|
9023 control.element = new api.Element( control.container.find( 'input' ) ); |
|
9024 control.element.set( 'fixed' !== control.setting() ); |
|
9025 |
|
9026 control.element.bind( function( to ) { |
|
9027 control.setting.set( to ? 'scroll' : 'fixed' ); |
|
9028 } ); |
|
9029 |
|
9030 control.setting.bind( function( to ) { |
|
9031 control.element.set( 'fixed' !== to ); |
|
9032 } ); |
|
9033 } ); |
|
9034 |
3039 // Juggle the two controls that use header_textcolor |
9035 // Juggle the two controls that use header_textcolor |
3040 api.control( 'display_header_text', function( control ) { |
9036 api.control( 'display_header_text', function( control ) { |
3041 var last = ''; |
9037 var last = ''; |
3042 |
9038 |
3043 control.elements[0].unsync( api( 'header_textcolor' ) ); |
9039 control.elements[0].unsync( api( 'header_textcolor' ) ); |
3044 |
9040 |
3045 control.element = new api.Element( control.container.find('input') ); |
9041 control.element = new api.Element( control.container.find('input') ); |
3046 control.element.set( 'blank' !== control.setting() ); |
9042 control.element.set( 'blank' !== control.setting() ); |
3047 |
9043 |
3048 control.element.bind( function( to ) { |
9044 control.element.bind( function( to ) { |
3049 if ( ! to ) |
9045 if ( ! to ) { |
3050 last = api( 'header_textcolor' ).get(); |
9046 last = api( 'header_textcolor' ).get(); |
|
9047 } |
3051 |
9048 |
3052 control.setting.set( to ? last : 'blank' ); |
9049 control.setting.set( to ? last : 'blank' ); |
3053 }); |
9050 }); |
3054 |
9051 |
3055 control.setting.bind( function( to ) { |
9052 control.setting.bind( function( to ) { |
3056 control.element.set( 'blank' !== to ); |
9053 control.element.set( 'blank' !== to ); |
3057 }); |
9054 }); |
3058 }); |
9055 }); |
3059 |
9056 |
|
9057 // Add behaviors to the static front page controls. |
|
9058 api( 'show_on_front', 'page_on_front', 'page_for_posts', function( showOnFront, pageOnFront, pageForPosts ) { |
|
9059 var handleChange = function() { |
|
9060 var setting = this, pageOnFrontId, pageForPostsId, errorCode = 'show_on_front_page_collision'; |
|
9061 pageOnFrontId = parseInt( pageOnFront(), 10 ); |
|
9062 pageForPostsId = parseInt( pageForPosts(), 10 ); |
|
9063 |
|
9064 if ( 'page' === showOnFront() ) { |
|
9065 |
|
9066 // Change previewed URL to the homepage when changing the page_on_front. |
|
9067 if ( setting === pageOnFront && pageOnFrontId > 0 ) { |
|
9068 api.previewer.previewUrl.set( api.settings.url.home ); |
|
9069 } |
|
9070 |
|
9071 // Change the previewed URL to the selected page when changing the page_for_posts. |
|
9072 if ( setting === pageForPosts && pageForPostsId > 0 ) { |
|
9073 api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageForPostsId ); |
|
9074 } |
|
9075 } |
|
9076 |
|
9077 // Toggle notification when the homepage and posts page are both set and the same. |
|
9078 if ( 'page' === showOnFront() && pageOnFrontId && pageForPostsId && pageOnFrontId === pageForPostsId ) { |
|
9079 showOnFront.notifications.add( new api.Notification( errorCode, { |
|
9080 type: 'error', |
|
9081 message: api.l10n.pageOnFrontError |
|
9082 } ) ); |
|
9083 } else { |
|
9084 showOnFront.notifications.remove( errorCode ); |
|
9085 } |
|
9086 }; |
|
9087 showOnFront.bind( handleChange ); |
|
9088 pageOnFront.bind( handleChange ); |
|
9089 pageForPosts.bind( handleChange ); |
|
9090 handleChange.call( showOnFront, showOnFront() ); // Make sure initial notification is added after loading existing changeset. |
|
9091 |
|
9092 // Move notifications container to the bottom. |
|
9093 api.control( 'show_on_front', function( showOnFrontControl ) { |
|
9094 showOnFrontControl.deferred.embedded.done( function() { |
|
9095 showOnFrontControl.container.append( showOnFrontControl.getNotificationsContainerElement() ); |
|
9096 }); |
|
9097 }); |
|
9098 }); |
|
9099 |
|
9100 // Add code editor for Custom CSS. |
|
9101 (function() { |
|
9102 var sectionReady = $.Deferred(); |
|
9103 |
|
9104 api.section( 'custom_css', function( section ) { |
|
9105 section.deferred.embedded.done( function() { |
|
9106 if ( section.expanded() ) { |
|
9107 sectionReady.resolve( section ); |
|
9108 } else { |
|
9109 section.expanded.bind( function( isExpanded ) { |
|
9110 if ( isExpanded ) { |
|
9111 sectionReady.resolve( section ); |
|
9112 } |
|
9113 } ); |
|
9114 } |
|
9115 }); |
|
9116 }); |
|
9117 |
|
9118 // Set up the section description behaviors. |
|
9119 sectionReady.done( function setupSectionDescription( section ) { |
|
9120 var control = api.control( 'custom_css' ); |
|
9121 |
|
9122 // Hide redundant label for visual users. |
|
9123 control.container.find( '.customize-control-title:first' ).addClass( 'screen-reader-text' ); |
|
9124 |
|
9125 // Close the section description when clicking the close button. |
|
9126 section.container.find( '.section-description-buttons .section-description-close' ).on( 'click', function() { |
|
9127 section.container.find( '.section-meta .customize-section-description:first' ) |
|
9128 .removeClass( 'open' ) |
|
9129 .slideUp(); |
|
9130 |
|
9131 section.container.find( '.customize-help-toggle' ) |
|
9132 .attr( 'aria-expanded', 'false' ) |
|
9133 .focus(); // Avoid focus loss. |
|
9134 }); |
|
9135 |
|
9136 // Reveal help text if setting is empty. |
|
9137 if ( control && ! control.setting.get() ) { |
|
9138 section.container.find( '.section-meta .customize-section-description:first' ) |
|
9139 .addClass( 'open' ) |
|
9140 .show() |
|
9141 .trigger( 'toggled' ); |
|
9142 |
|
9143 section.container.find( '.customize-help-toggle' ).attr( 'aria-expanded', 'true' ); |
|
9144 } |
|
9145 }); |
|
9146 })(); |
|
9147 |
|
9148 // Toggle visibility of Header Video notice when active state change. |
|
9149 api.control( 'header_video', function( headerVideoControl ) { |
|
9150 headerVideoControl.deferred.embedded.done( function() { |
|
9151 var toggleNotice = function() { |
|
9152 var section = api.section( headerVideoControl.section() ), noticeCode = 'video_header_not_available'; |
|
9153 if ( ! section ) { |
|
9154 return; |
|
9155 } |
|
9156 if ( headerVideoControl.active.get() ) { |
|
9157 section.notifications.remove( noticeCode ); |
|
9158 } else { |
|
9159 section.notifications.add( new api.Notification( noticeCode, { |
|
9160 type: 'info', |
|
9161 message: api.l10n.videoHeaderNotice |
|
9162 } ) ); |
|
9163 } |
|
9164 }; |
|
9165 toggleNotice(); |
|
9166 headerVideoControl.active.bind( toggleNotice ); |
|
9167 } ); |
|
9168 } ); |
|
9169 |
|
9170 // Update the setting validities. |
|
9171 api.previewer.bind( 'selective-refresh-setting-validities', function handleSelectiveRefreshedSettingValidities( settingValidities ) { |
|
9172 api._handleSettingValidities( { |
|
9173 settingValidities: settingValidities, |
|
9174 focusInvalidControl: false |
|
9175 } ); |
|
9176 } ); |
|
9177 |
|
9178 // Focus on the control that is associated with the given setting. |
|
9179 api.previewer.bind( 'focus-control-for-setting', function( settingId ) { |
|
9180 var matchedControls = []; |
|
9181 api.control.each( function( control ) { |
|
9182 var settingIds = _.pluck( control.settings, 'id' ); |
|
9183 if ( -1 !== _.indexOf( settingIds, settingId ) ) { |
|
9184 matchedControls.push( control ); |
|
9185 } |
|
9186 } ); |
|
9187 |
|
9188 // Focus on the matched control with the lowest priority (appearing higher). |
|
9189 if ( matchedControls.length ) { |
|
9190 matchedControls.sort( function( a, b ) { |
|
9191 return a.priority() - b.priority(); |
|
9192 } ); |
|
9193 matchedControls[0].focus(); |
|
9194 } |
|
9195 } ); |
|
9196 |
|
9197 // Refresh the preview when it requests. |
|
9198 api.previewer.bind( 'refresh', function() { |
|
9199 api.previewer.refresh(); |
|
9200 }); |
|
9201 |
|
9202 // Update the edit shortcut visibility state. |
|
9203 api.state( 'paneVisible' ).bind( function( isPaneVisible ) { |
|
9204 var isMobileScreen; |
|
9205 if ( window.matchMedia ) { |
|
9206 isMobileScreen = window.matchMedia( 'screen and ( max-width: 640px )' ).matches; |
|
9207 } else { |
|
9208 isMobileScreen = $( window ).width() <= 640; |
|
9209 } |
|
9210 api.state( 'editShortcutVisibility' ).set( isPaneVisible || isMobileScreen ? 'visible' : 'hidden' ); |
|
9211 } ); |
|
9212 if ( window.matchMedia ) { |
|
9213 window.matchMedia( 'screen and ( max-width: 640px )' ).addListener( function() { |
|
9214 var state = api.state( 'paneVisible' ); |
|
9215 state.callbacks.fireWith( state, [ state.get(), state.get() ] ); |
|
9216 } ); |
|
9217 } |
|
9218 api.previewer.bind( 'edit-shortcut-visibility', function( visibility ) { |
|
9219 api.state( 'editShortcutVisibility' ).set( visibility ); |
|
9220 } ); |
|
9221 api.state( 'editShortcutVisibility' ).bind( function( visibility ) { |
|
9222 api.previewer.send( 'edit-shortcut-visibility', visibility ); |
|
9223 } ); |
|
9224 |
|
9225 // Autosave changeset. |
|
9226 function startAutosaving() { |
|
9227 var timeoutId, updateChangesetWithReschedule, scheduleChangesetUpdate, updatePending = false; |
|
9228 |
|
9229 api.unbind( 'change', startAutosaving ); // Ensure startAutosaving only fires once. |
|
9230 |
|
9231 function onChangeSaved( isSaved ) { |
|
9232 if ( ! isSaved && ! api.settings.changeset.autosaved ) { |
|
9233 api.settings.changeset.autosaved = true; // Once a change is made then autosaving kicks in. |
|
9234 api.previewer.send( 'autosaving' ); |
|
9235 } |
|
9236 } |
|
9237 api.state( 'saved' ).bind( onChangeSaved ); |
|
9238 onChangeSaved( api.state( 'saved' ).get() ); |
|
9239 |
|
9240 /** |
|
9241 * Request changeset update and then re-schedule the next changeset update time. |
|
9242 * |
|
9243 * @since 4.7.0 |
|
9244 * @private |
|
9245 */ |
|
9246 updateChangesetWithReschedule = function() { |
|
9247 if ( ! updatePending ) { |
|
9248 updatePending = true; |
|
9249 api.requestChangesetUpdate( {}, { autosave: true } ).always( function() { |
|
9250 updatePending = false; |
|
9251 } ); |
|
9252 } |
|
9253 scheduleChangesetUpdate(); |
|
9254 }; |
|
9255 |
|
9256 /** |
|
9257 * Schedule changeset update. |
|
9258 * |
|
9259 * @since 4.7.0 |
|
9260 * @private |
|
9261 */ |
|
9262 scheduleChangesetUpdate = function() { |
|
9263 clearTimeout( timeoutId ); |
|
9264 timeoutId = setTimeout( function() { |
|
9265 updateChangesetWithReschedule(); |
|
9266 }, api.settings.timeouts.changesetAutoSave ); |
|
9267 }; |
|
9268 |
|
9269 // Start auto-save interval for updating changeset. |
|
9270 scheduleChangesetUpdate(); |
|
9271 |
|
9272 // Save changeset when focus removed from window. |
|
9273 $( document ).on( 'visibilitychange.wp-customize-changeset-update', function() { |
|
9274 if ( document.hidden ) { |
|
9275 updateChangesetWithReschedule(); |
|
9276 } |
|
9277 } ); |
|
9278 |
|
9279 // Save changeset before unloading window. |
|
9280 $( window ).on( 'beforeunload.wp-customize-changeset-update', function() { |
|
9281 updateChangesetWithReschedule(); |
|
9282 } ); |
|
9283 } |
|
9284 api.bind( 'change', startAutosaving ); |
|
9285 |
|
9286 // Make sure TinyMCE dialogs appear above Customizer UI. |
|
9287 $( document ).one( 'tinymce-editor-setup', function() { |
|
9288 if ( window.tinymce.ui.FloatPanel && ( ! window.tinymce.ui.FloatPanel.zIndex || window.tinymce.ui.FloatPanel.zIndex < 500001 ) ) { |
|
9289 window.tinymce.ui.FloatPanel.zIndex = 500001; |
|
9290 } |
|
9291 } ); |
|
9292 |
|
9293 body.addClass( 'ready' ); |
3060 api.trigger( 'ready' ); |
9294 api.trigger( 'ready' ); |
3061 |
|
3062 // Make sure left column gets focus |
|
3063 topFocus = closeBtn; |
|
3064 topFocus.focus(); |
|
3065 setTimeout(function () { |
|
3066 topFocus.focus(); |
|
3067 }, 200); |
|
3068 |
|
3069 }); |
9295 }); |
3070 |
9296 |
3071 })( wp, jQuery ); |
9297 })( wp, jQuery ); |