25 return this.previewer.send( 'setting', [ this.id, this() ] ); |
29 return this.previewer.send( 'setting', [ this.id, this() ] ); |
26 } |
30 } |
27 } |
31 } |
28 }); |
32 }); |
29 |
33 |
|
34 /** |
|
35 * Utility function namespace |
|
36 */ |
|
37 api.utils = {}; |
|
38 |
|
39 /** |
|
40 * Watch all changes to Value properties, and bubble changes to parent Values instance |
|
41 * |
|
42 * @since 4.1.0 |
|
43 * |
|
44 * @param {wp.customize.Class} instance |
|
45 * @param {Array} properties The names of the Value instances to watch. |
|
46 */ |
|
47 api.utils.bubbleChildValueChanges = function ( instance, properties ) { |
|
48 $.each( properties, function ( i, key ) { |
|
49 instance[ key ].bind( function ( to, from ) { |
|
50 if ( instance.parent && to !== from ) { |
|
51 instance.parent.trigger( 'change', instance ); |
|
52 } |
|
53 } ); |
|
54 } ); |
|
55 }; |
|
56 |
|
57 /** |
|
58 * Expand a panel, section, or control and focus on the first focusable element. |
|
59 * |
|
60 * @since 4.1.0 |
|
61 * |
|
62 * @param {Object} [params] |
|
63 * @param {Callback} [params.completeCallback] |
|
64 */ |
|
65 focus = function ( params ) { |
|
66 var construct, completeCallback, focus; |
|
67 construct = this; |
|
68 params = params || {}; |
|
69 focus = function () { |
|
70 var focusContainer; |
|
71 if ( construct.extended( api.Panel ) && construct.expanded() ) { |
|
72 focusContainer = construct.container.find( '.control-panel-content:first' ); |
|
73 } else { |
|
74 focusContainer = construct.container; |
|
75 } |
|
76 focusContainer.find( ':focusable:first' ).focus(); |
|
77 focusContainer[0].scrollIntoView( true ); |
|
78 }; |
|
79 if ( params.completeCallback ) { |
|
80 completeCallback = params.completeCallback; |
|
81 params.completeCallback = function () { |
|
82 focus(); |
|
83 completeCallback(); |
|
84 }; |
|
85 } else { |
|
86 params.completeCallback = focus; |
|
87 } |
|
88 if ( construct.expand ) { |
|
89 construct.expand( params ); |
|
90 } else { |
|
91 params.completeCallback(); |
|
92 } |
|
93 }; |
|
94 |
|
95 /** |
|
96 * Stable sort for Panels, Sections, and Controls. |
|
97 * |
|
98 * If a.priority() === b.priority(), then sort by their respective params.instanceNumber. |
|
99 * |
|
100 * @since 4.1.0 |
|
101 * |
|
102 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a |
|
103 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b |
|
104 * @returns {Number} |
|
105 */ |
|
106 api.utils.prioritySort = function ( a, b ) { |
|
107 if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) { |
|
108 return a.params.instanceNumber - b.params.instanceNumber; |
|
109 } else { |
|
110 return a.priority() - b.priority(); |
|
111 } |
|
112 }; |
|
113 |
|
114 /** |
|
115 * Return whether the supplied Event object is for a keydown event but not the Enter key. |
|
116 * |
|
117 * @since 4.1.0 |
|
118 * |
|
119 * @param {jQuery.Event} event |
|
120 * @returns {boolean} |
|
121 */ |
|
122 api.utils.isKeydownButNotEnterEvent = function ( event ) { |
|
123 return ( 'keydown' === event.type && 13 !== event.which ); |
|
124 }; |
|
125 |
|
126 /** |
|
127 * Return whether the two lists of elements are the same and are in the same order. |
|
128 * |
|
129 * @since 4.1.0 |
|
130 * |
|
131 * @param {Array|jQuery} listA |
|
132 * @param {Array|jQuery} listB |
|
133 * @returns {boolean} |
|
134 */ |
|
135 api.utils.areElementListsEqual = function ( listA, listB ) { |
|
136 var equal = ( |
|
137 listA.length === listB.length && // if lists are different lengths, then naturally they are not equal |
|
138 -1 === _.indexOf( _.map( // are there any false values in the list returned by map? |
|
139 _.zip( listA, listB ), // pair up each element between the two lists |
|
140 function ( pair ) { |
|
141 return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal |
|
142 } |
|
143 ), false ) // check for presence of false in map's return value |
|
144 ); |
|
145 return equal; |
|
146 }; |
|
147 |
|
148 /** |
|
149 * Base class for Panel and Section. |
|
150 * |
|
151 * @since 4.1.0 |
|
152 * |
|
153 * @class |
|
154 * @augments wp.customize.Class |
|
155 */ |
|
156 Container = api.Class.extend({ |
|
157 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop }, |
|
158 defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop }, |
|
159 |
|
160 /** |
|
161 * @since 4.1.0 |
|
162 * |
|
163 * @param {String} id |
|
164 * @param {Object} options |
|
165 */ |
|
166 initialize: function ( id, options ) { |
|
167 var container = this; |
|
168 container.id = id; |
|
169 container.params = {}; |
|
170 $.extend( container, options || {} ); |
|
171 container.container = $( container.params.content ); |
|
172 |
|
173 container.deferred = { |
|
174 embedded: new $.Deferred() |
|
175 }; |
|
176 container.priority = new api.Value(); |
|
177 container.active = new api.Value(); |
|
178 container.activeArgumentsQueue = []; |
|
179 container.expanded = new api.Value(); |
|
180 container.expandedArgumentsQueue = []; |
|
181 |
|
182 container.active.bind( function ( active ) { |
|
183 var args = container.activeArgumentsQueue.shift(); |
|
184 args = $.extend( {}, container.defaultActiveArguments, args ); |
|
185 active = ( active && container.isContextuallyActive() ); |
|
186 container.onChangeActive( active, args ); |
|
187 }); |
|
188 container.expanded.bind( function ( expanded ) { |
|
189 var args = container.expandedArgumentsQueue.shift(); |
|
190 args = $.extend( {}, container.defaultExpandedArguments, args ); |
|
191 container.onChangeExpanded( expanded, args ); |
|
192 }); |
|
193 |
|
194 container.attachEvents(); |
|
195 |
|
196 api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] ); |
|
197 |
|
198 container.priority.set( isNaN( container.params.priority ) ? 100 : container.params.priority ); |
|
199 container.active.set( container.params.active ); |
|
200 container.expanded.set( false ); |
|
201 }, |
|
202 |
|
203 /** |
|
204 * @since 4.1.0 |
|
205 * |
|
206 * @abstract |
|
207 */ |
|
208 ready: function() {}, |
|
209 |
|
210 /** |
|
211 * Get the child models associated with this parent, sorting them by their priority Value. |
|
212 * |
|
213 * @since 4.1.0 |
|
214 * |
|
215 * @param {String} parentType |
|
216 * @param {String} childType |
|
217 * @returns {Array} |
|
218 */ |
|
219 _children: function ( parentType, childType ) { |
|
220 var parent = this, |
|
221 children = []; |
|
222 api[ childType ].each( function ( child ) { |
|
223 if ( child[ parentType ].get() === parent.id ) { |
|
224 children.push( child ); |
|
225 } |
|
226 } ); |
|
227 children.sort( api.utils.prioritySort ); |
|
228 return children; |
|
229 }, |
|
230 |
|
231 /** |
|
232 * To override by subclass, to return whether the container has active children. |
|
233 * |
|
234 * @since 4.1.0 |
|
235 * |
|
236 * @abstract |
|
237 */ |
|
238 isContextuallyActive: function () { |
|
239 throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' ); |
|
240 }, |
|
241 |
|
242 /** |
|
243 * Handle changes to the active state. |
|
244 * |
|
245 * This does not change the active state, it merely handles the behavior |
|
246 * for when it does change. |
|
247 * |
|
248 * To override by subclass, update the container's UI to reflect the provided active state. |
|
249 * |
|
250 * @since 4.1.0 |
|
251 * |
|
252 * @param {Boolean} active |
|
253 * @param {Object} args |
|
254 * @param {Object} args.duration |
|
255 * @param {Object} args.completeCallback |
|
256 */ |
|
257 onChangeActive: function ( active, args ) { |
|
258 var duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 ); |
|
259 if ( ! $.contains( document, this.container ) ) { |
|
260 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM |
|
261 this.container.toggle( active ); |
|
262 if ( args.completeCallback ) { |
|
263 args.completeCallback(); |
|
264 } |
|
265 } else if ( active ) { |
|
266 this.container.stop( true, true ).slideDown( duration, args.completeCallback ); |
|
267 } else { |
|
268 this.container.stop( true, true ).slideUp( duration, args.completeCallback ); |
|
269 } |
|
270 }, |
|
271 |
|
272 /** |
|
273 * @since 4.1.0 |
|
274 * |
|
275 * @params {Boolean} active |
|
276 * @param {Object} [params] |
|
277 * @returns {Boolean} false if state already applied |
|
278 */ |
|
279 _toggleActive: function ( active, params ) { |
|
280 var self = this; |
|
281 params = params || {}; |
|
282 if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) { |
|
283 params.unchanged = true; |
|
284 self.onChangeActive( self.active.get(), params ); |
|
285 return false; |
|
286 } else { |
|
287 params.unchanged = false; |
|
288 this.activeArgumentsQueue.push( params ); |
|
289 this.active.set( active ); |
|
290 return true; |
|
291 } |
|
292 }, |
|
293 |
|
294 /** |
|
295 * @param {Object} [params] |
|
296 * @returns {Boolean} false if already active |
|
297 */ |
|
298 activate: function ( params ) { |
|
299 return this._toggleActive( true, params ); |
|
300 }, |
|
301 |
|
302 /** |
|
303 * @param {Object} [params] |
|
304 * @returns {Boolean} false if already inactive |
|
305 */ |
|
306 deactivate: function ( params ) { |
|
307 return this._toggleActive( false, params ); |
|
308 }, |
|
309 |
|
310 /** |
|
311 * To override by subclass, update the container's UI to reflect the provided active state. |
|
312 * @abstract |
|
313 */ |
|
314 onChangeExpanded: function () { |
|
315 throw new Error( 'Must override with subclass.' ); |
|
316 }, |
|
317 |
|
318 /** |
|
319 * @param {Boolean} expanded |
|
320 * @param {Object} [params] |
|
321 * @returns {Boolean} false if state already applied |
|
322 */ |
|
323 _toggleExpanded: function ( expanded, params ) { |
|
324 var self = this; |
|
325 params = params || {}; |
|
326 var section = this, previousCompleteCallback = params.completeCallback; |
|
327 params.completeCallback = function () { |
|
328 if ( previousCompleteCallback ) { |
|
329 previousCompleteCallback.apply( section, arguments ); |
|
330 } |
|
331 if ( expanded ) { |
|
332 section.container.trigger( 'expanded' ); |
|
333 } else { |
|
334 section.container.trigger( 'collapsed' ); |
|
335 } |
|
336 }; |
|
337 if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) { |
|
338 params.unchanged = true; |
|
339 self.onChangeExpanded( self.expanded.get(), params ); |
|
340 return false; |
|
341 } else { |
|
342 params.unchanged = false; |
|
343 this.expandedArgumentsQueue.push( params ); |
|
344 this.expanded.set( expanded ); |
|
345 return true; |
|
346 } |
|
347 }, |
|
348 |
|
349 /** |
|
350 * @param {Object} [params] |
|
351 * @returns {Boolean} false if already expanded |
|
352 */ |
|
353 expand: function ( params ) { |
|
354 return this._toggleExpanded( true, params ); |
|
355 }, |
|
356 |
|
357 /** |
|
358 * @param {Object} [params] |
|
359 * @returns {Boolean} false if already collapsed |
|
360 */ |
|
361 collapse: function ( params ) { |
|
362 return this._toggleExpanded( false, params ); |
|
363 }, |
|
364 |
|
365 /** |
|
366 * Bring the container into view and then expand this and bring it into view |
|
367 * @param {Object} [params] |
|
368 */ |
|
369 focus: focus |
|
370 }); |
|
371 |
|
372 /** |
|
373 * @since 4.1.0 |
|
374 * |
|
375 * @class |
|
376 * @augments wp.customize.Class |
|
377 */ |
|
378 api.Section = Container.extend({ |
|
379 |
|
380 /** |
|
381 * @since 4.1.0 |
|
382 * |
|
383 * @param {String} id |
|
384 * @param {Array} options |
|
385 */ |
|
386 initialize: function ( id, options ) { |
|
387 var section = this; |
|
388 Container.prototype.initialize.call( section, id, options ); |
|
389 |
|
390 section.id = id; |
|
391 section.panel = new api.Value(); |
|
392 section.panel.bind( function ( id ) { |
|
393 $( section.container ).toggleClass( 'control-subsection', !! id ); |
|
394 }); |
|
395 section.panel.set( section.params.panel || '' ); |
|
396 api.utils.bubbleChildValueChanges( section, [ 'panel' ] ); |
|
397 |
|
398 section.embed(); |
|
399 section.deferred.embedded.done( function () { |
|
400 section.ready(); |
|
401 }); |
|
402 }, |
|
403 |
|
404 /** |
|
405 * Embed the container in the DOM when any parent panel is ready. |
|
406 * |
|
407 * @since 4.1.0 |
|
408 */ |
|
409 embed: function () { |
|
410 var section = this, inject; |
|
411 |
|
412 // Watch for changes to the panel state |
|
413 inject = function ( panelId ) { |
|
414 var parentContainer; |
|
415 if ( panelId ) { |
|
416 // The panel has been supplied, so wait until the panel object is registered |
|
417 api.panel( panelId, function ( panel ) { |
|
418 // The panel has been registered, wait for it to become ready/initialized |
|
419 panel.deferred.embedded.done( function () { |
|
420 parentContainer = panel.container.find( 'ul:first' ); |
|
421 if ( ! section.container.parent().is( parentContainer ) ) { |
|
422 parentContainer.append( section.container ); |
|
423 } |
|
424 section.deferred.embedded.resolve(); |
|
425 }); |
|
426 } ); |
|
427 } else { |
|
428 // There is no panel, so embed the section in the root of the customizer |
|
429 parentContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable |
|
430 if ( ! section.container.parent().is( parentContainer ) ) { |
|
431 parentContainer.append( section.container ); |
|
432 } |
|
433 section.deferred.embedded.resolve(); |
|
434 } |
|
435 }; |
|
436 section.panel.bind( inject ); |
|
437 inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one |
|
438 }, |
|
439 |
|
440 /** |
|
441 * Add behaviors for the accordion section. |
|
442 * |
|
443 * @since 4.1.0 |
|
444 */ |
|
445 attachEvents: function () { |
|
446 var section = this; |
|
447 |
|
448 // Expand/Collapse accordion sections on click. |
|
449 section.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) { |
|
450 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
|
451 return; |
|
452 } |
|
453 event.preventDefault(); // Keep this AFTER the key filter above |
|
454 |
|
455 if ( section.expanded() ) { |
|
456 section.collapse(); |
|
457 } else { |
|
458 section.expand(); |
|
459 } |
|
460 }); |
|
461 }, |
|
462 |
|
463 /** |
|
464 * Return whether this section has any active controls. |
|
465 * |
|
466 * @since 4.1.0 |
|
467 * |
|
468 * @returns {Boolean} |
|
469 */ |
|
470 isContextuallyActive: function () { |
|
471 var section = this, |
|
472 controls = section.controls(), |
|
473 activeCount = 0; |
|
474 _( controls ).each( function ( control ) { |
|
475 if ( control.active() ) { |
|
476 activeCount += 1; |
|
477 } |
|
478 } ); |
|
479 return ( activeCount !== 0 ); |
|
480 }, |
|
481 |
|
482 /** |
|
483 * Get the controls that are associated with this section, sorted by their priority Value. |
|
484 * |
|
485 * @since 4.1.0 |
|
486 * |
|
487 * @returns {Array} |
|
488 */ |
|
489 controls: function () { |
|
490 return this._children( 'section', 'control' ); |
|
491 }, |
|
492 |
|
493 /** |
|
494 * Update UI to reflect expanded state. |
|
495 * |
|
496 * @since 4.1.0 |
|
497 * |
|
498 * @param {Boolean} expanded |
|
499 * @param {Object} args |
|
500 */ |
|
501 onChangeExpanded: function ( expanded, args ) { |
|
502 var section = this, |
|
503 content = section.container.find( '.accordion-section-content' ), |
|
504 expand; |
|
505 |
|
506 if ( expanded ) { |
|
507 |
|
508 if ( args.unchanged ) { |
|
509 expand = args.completeCallback; |
|
510 } else { |
|
511 expand = function () { |
|
512 content.stop().slideDown( args.duration, args.completeCallback ); |
|
513 section.container.addClass( 'open' ); |
|
514 }; |
|
515 } |
|
516 |
|
517 if ( ! args.allowMultiple ) { |
|
518 api.section.each( function ( otherSection ) { |
|
519 if ( otherSection !== section ) { |
|
520 otherSection.collapse( { duration: args.duration } ); |
|
521 } |
|
522 }); |
|
523 } |
|
524 |
|
525 if ( section.panel() ) { |
|
526 api.panel( section.panel() ).expand({ |
|
527 duration: args.duration, |
|
528 completeCallback: expand |
|
529 }); |
|
530 } else { |
|
531 expand(); |
|
532 } |
|
533 |
|
534 } else { |
|
535 section.container.removeClass( 'open' ); |
|
536 content.slideUp( args.duration, args.completeCallback ); |
|
537 } |
|
538 } |
|
539 }); |
|
540 |
|
541 /** |
|
542 * wp.customize.ThemesSection |
|
543 * |
|
544 * Custom section for themes that functions similarly to a backwards panel, |
|
545 * and also handles the theme-details view rendering and navigation. |
|
546 * |
|
547 * @constructor |
|
548 * @augments wp.customize.Section |
|
549 * @augments wp.customize.Container |
|
550 */ |
|
551 api.ThemesSection = api.Section.extend({ |
|
552 currentTheme: '', |
|
553 overlay: '', |
|
554 template: '', |
|
555 screenshotQueue: null, |
|
556 $window: $( window ), |
|
557 |
|
558 /** |
|
559 * @since 4.2.0 |
|
560 */ |
|
561 initialize: function () { |
|
562 this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' ); |
|
563 return api.Section.prototype.initialize.apply( this, arguments ); |
|
564 }, |
|
565 |
|
566 /** |
|
567 * @since 4.2.0 |
|
568 */ |
|
569 ready: function () { |
|
570 var section = this; |
|
571 section.overlay = section.container.find( '.theme-overlay' ); |
|
572 section.template = wp.template( 'customize-themes-details-view' ); |
|
573 |
|
574 // Bind global keyboard events. |
|
575 $( 'body' ).on( 'keyup', function( event ) { |
|
576 if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) { |
|
577 return; |
|
578 } |
|
579 |
|
580 // Pressing the right arrow key fires a theme:next event |
|
581 if ( 39 === event.keyCode ) { |
|
582 section.nextTheme(); |
|
583 } |
|
584 |
|
585 // Pressing the left arrow key fires a theme:previous event |
|
586 if ( 37 === event.keyCode ) { |
|
587 section.previousTheme(); |
|
588 } |
|
589 |
|
590 // Pressing the escape key fires a theme:collapse event |
|
591 if ( 27 === event.keyCode ) { |
|
592 section.closeDetails(); |
|
593 } |
|
594 }); |
|
595 |
|
596 _.bindAll( this, 'renderScreenshots' ); |
|
597 }, |
|
598 |
|
599 /** |
|
600 * Override Section.isContextuallyActive method. |
|
601 * |
|
602 * Ignore the active states' of the contained theme controls, and just |
|
603 * use the section's own active state instead. This ensures empty search |
|
604 * results for themes to cause the section to become inactive. |
|
605 * |
|
606 * @since 4.2.0 |
|
607 * |
|
608 * @returns {Boolean} |
|
609 */ |
|
610 isContextuallyActive: function () { |
|
611 return this.active(); |
|
612 }, |
|
613 |
|
614 /** |
|
615 * @since 4.2.0 |
|
616 */ |
|
617 attachEvents: function () { |
|
618 var section = this; |
|
619 |
|
620 // Expand/Collapse section/panel. |
|
621 section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) { |
|
622 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
|
623 return; |
|
624 } |
|
625 event.preventDefault(); // Keep this AFTER the key filter above |
|
626 |
|
627 if ( section.expanded() ) { |
|
628 section.collapse(); |
|
629 } else { |
|
630 section.expand(); |
|
631 } |
|
632 }); |
|
633 |
|
634 // Theme navigation in details view. |
|
635 section.container.on( 'click keydown', '.left', function( event ) { |
|
636 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
|
637 return; |
|
638 } |
|
639 |
|
640 event.preventDefault(); // Keep this AFTER the key filter above |
|
641 |
|
642 section.previousTheme(); |
|
643 }); |
|
644 |
|
645 section.container.on( 'click keydown', '.right', function( event ) { |
|
646 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
|
647 return; |
|
648 } |
|
649 |
|
650 event.preventDefault(); // Keep this AFTER the key filter above |
|
651 |
|
652 section.nextTheme(); |
|
653 }); |
|
654 |
|
655 section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) { |
|
656 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
|
657 return; |
|
658 } |
|
659 |
|
660 event.preventDefault(); // Keep this AFTER the key filter above |
|
661 |
|
662 section.closeDetails(); |
|
663 }); |
|
664 |
|
665 var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 ); |
|
666 section.container.on( 'input', '#themes-filter', function( event ) { |
|
667 var count, |
|
668 term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ), |
|
669 controls = section.controls(); |
|
670 |
|
671 _.each( controls, function( control ) { |
|
672 control.filter( term ); |
|
673 }); |
|
674 |
|
675 renderScreenshots(); |
|
676 |
|
677 // Update theme count. |
|
678 count = section.container.find( 'li.customize-control:visible' ).length; |
|
679 section.container.find( '.theme-count' ).text( count ); |
|
680 }); |
|
681 |
|
682 // Pre-load the first 3 theme screenshots. |
|
683 api.bind( 'ready', function () { |
|
684 _.each( section.controls().slice( 0, 3 ), function ( control ) { |
|
685 var img, src = control.params.theme.screenshot[0]; |
|
686 if ( src ) { |
|
687 img = new Image(); |
|
688 img.src = src; |
|
689 } |
|
690 }); |
|
691 }); |
|
692 }, |
|
693 |
|
694 /** |
|
695 * Update UI to reflect expanded state |
|
696 * |
|
697 * @since 4.2.0 |
|
698 * |
|
699 * @param {Boolean} expanded |
|
700 * @param {Object} args |
|
701 * @param {Boolean} args.unchanged |
|
702 * @param {Callback} args.completeCallback |
|
703 */ |
|
704 onChangeExpanded: function ( expanded, args ) { |
|
705 |
|
706 // Immediately call the complete callback if there were no changes |
|
707 if ( args.unchanged ) { |
|
708 if ( args.completeCallback ) { |
|
709 args.completeCallback(); |
|
710 } |
|
711 return; |
|
712 } |
|
713 |
|
714 // Note: there is a second argument 'args' passed |
|
715 var position, scroll, |
|
716 panel = this, |
|
717 section = panel.container.closest( '.accordion-section' ), |
|
718 overlay = section.closest( '.wp-full-overlay' ), |
|
719 container = section.closest( '.wp-full-overlay-sidebar-content' ), |
|
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 |
|
728 // Collapse any sibling sections/panels |
|
729 api.section.each( function ( otherSection ) { |
|
730 if ( otherSection !== panel ) { |
|
731 otherSection.collapse( { duration: args.duration } ); |
|
732 } |
|
733 }); |
|
734 api.panel.each( function ( otherPanel ) { |
|
735 otherPanel.collapse( { duration: 0 } ); |
|
736 }); |
|
737 |
|
738 content.show( 0, function() { |
|
739 position = content.offset().top; |
|
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 ) { |
|
787 return; |
|
788 } |
|
789 |
|
790 section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) { |
|
791 var $imageWrapper = control.container.find( '.theme-screenshot' ), |
|
792 $image = $imageWrapper.find( 'img' ); |
|
793 |
|
794 if ( ! $image.length ) { |
|
795 return false; |
|
796 } |
|
797 |
|
798 if ( $image.is( ':hidden' ) ) { |
|
799 return true; |
|
800 } |
|
801 |
|
802 // Based on unveil.js. |
|
803 var wt = section.$window.scrollTop(), |
|
804 wb = wt + section.$window.height(), |
|
805 et = $image.offset().top, |
|
806 ih = $imageWrapper.height(), |
|
807 eb = et + ih, |
|
808 threshold = ih * 3, |
|
809 inView = eb >= wt - threshold && et <= wb + threshold; |
|
810 |
|
811 if ( inView ) { |
|
812 control.container.trigger( 'render-screenshot' ); |
|
813 } |
|
814 |
|
815 // If the image is in view return false so it's cleared from the queue. |
|
816 return ! inView; |
|
817 } ); |
|
818 }, |
|
819 |
|
820 /** |
|
821 * Advance the modal to the next theme. |
|
822 * |
|
823 * @since 4.2.0 |
|
824 */ |
|
825 nextTheme: function () { |
|
826 var section = this; |
|
827 if ( section.getNextTheme() ) { |
|
828 section.showDetails( section.getNextTheme(), function() { |
|
829 section.overlay.find( '.right' ).focus(); |
|
830 } ); |
|
831 } |
|
832 }, |
|
833 |
|
834 /** |
|
835 * Get the next theme model. |
|
836 * |
|
837 * @since 4.2.0 |
|
838 */ |
|
839 getNextTheme: function () { |
|
840 var control, next; |
|
841 control = api.control( 'theme_' + this.currentTheme ); |
|
842 next = control.container.next( 'li.customize-control-theme' ); |
|
843 if ( ! next.length ) { |
|
844 return false; |
|
845 } |
|
846 next = next[0].id.replace( 'customize-control-', '' ); |
|
847 control = api.control( next ); |
|
848 |
|
849 return control.params.theme; |
|
850 }, |
|
851 |
|
852 /** |
|
853 * Advance the modal to the previous theme. |
|
854 * |
|
855 * @since 4.2.0 |
|
856 */ |
|
857 previousTheme: function () { |
|
858 var section = this; |
|
859 if ( section.getPreviousTheme() ) { |
|
860 section.showDetails( section.getPreviousTheme(), function() { |
|
861 section.overlay.find( '.left' ).focus(); |
|
862 } ); |
|
863 } |
|
864 }, |
|
865 |
|
866 /** |
|
867 * Get the previous theme model. |
|
868 * |
|
869 * @since 4.2.0 |
|
870 */ |
|
871 getPreviousTheme: function () { |
|
872 var control, previous; |
|
873 control = api.control( 'theme_' + this.currentTheme ); |
|
874 previous = control.container.prev( 'li.customize-control-theme' ); |
|
875 if ( ! previous.length ) { |
|
876 return false; |
|
877 } |
|
878 previous = previous[0].id.replace( 'customize-control-', '' ); |
|
879 control = api.control( previous ); |
|
880 |
|
881 return control.params.theme; |
|
882 }, |
|
883 |
|
884 /** |
|
885 * Disable buttons when we're viewing the first or last theme. |
|
886 * |
|
887 * @since 4.2.0 |
|
888 */ |
|
889 updateLimits: function () { |
|
890 if ( ! this.getNextTheme() ) { |
|
891 this.overlay.find( '.right' ).addClass( 'disabled' ); |
|
892 } |
|
893 if ( ! this.getPreviousTheme() ) { |
|
894 this.overlay.find( '.left' ).addClass( 'disabled' ); |
|
895 } |
|
896 }, |
|
897 |
|
898 /** |
|
899 * Render & show the theme details for a given theme model. |
|
900 * |
|
901 * @since 4.2.0 |
|
902 * |
|
903 * @param {Object} theme |
|
904 */ |
|
905 showDetails: function ( theme, callback ) { |
|
906 var section = this; |
|
907 callback = callback || function(){}; |
|
908 section.currentTheme = theme.id; |
|
909 section.overlay.html( section.template( theme ) ) |
|
910 .fadeIn( 'fast' ) |
|
911 .focus(); |
|
912 $( 'body' ).addClass( 'modal-open' ); |
|
913 section.containFocus( section.overlay ); |
|
914 section.updateLimits(); |
|
915 callback(); |
|
916 }, |
|
917 |
|
918 /** |
|
919 * Close the theme details modal. |
|
920 * |
|
921 * @since 4.2.0 |
|
922 */ |
|
923 closeDetails: function () { |
|
924 $( 'body' ).removeClass( 'modal-open' ); |
|
925 this.overlay.fadeOut( 'fast' ); |
|
926 api.control( 'theme_' + this.currentTheme ).focus(); |
|
927 }, |
|
928 |
|
929 /** |
|
930 * Keep tab focus within the theme details modal. |
|
931 * |
|
932 * @since 4.2.0 |
|
933 */ |
|
934 containFocus: function( el ) { |
|
935 var tabbables; |
|
936 |
|
937 el.on( 'keydown', function( event ) { |
|
938 |
|
939 // Return if it's not the tab key |
|
940 // When navigating with prev/next focus is already handled |
|
941 if ( 9 !== event.keyCode ) { |
|
942 return; |
|
943 } |
|
944 |
|
945 // uses jQuery UI to get the tabbable elements |
|
946 tabbables = $( ':tabbable', el ); |
|
947 |
|
948 // Keep focus within the overlay |
|
949 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) { |
|
950 tabbables.first().focus(); |
|
951 return false; |
|
952 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) { |
|
953 tabbables.last().focus(); |
|
954 return false; |
|
955 } |
|
956 }); |
|
957 } |
|
958 }); |
|
959 |
|
960 /** |
|
961 * @since 4.1.0 |
|
962 * |
|
963 * @class |
|
964 * @augments wp.customize.Class |
|
965 */ |
|
966 api.Panel = Container.extend({ |
|
967 /** |
|
968 * @since 4.1.0 |
|
969 * |
|
970 * @param {String} id |
|
971 * @param {Object} options |
|
972 */ |
|
973 initialize: function ( id, options ) { |
|
974 var panel = this; |
|
975 Container.prototype.initialize.call( panel, id, options ); |
|
976 panel.embed(); |
|
977 panel.deferred.embedded.done( function () { |
|
978 panel.ready(); |
|
979 }); |
|
980 }, |
|
981 |
|
982 /** |
|
983 * Embed the container in the DOM when any parent panel is ready. |
|
984 * |
|
985 * @since 4.1.0 |
|
986 */ |
|
987 embed: function () { |
|
988 var panel = this, |
|
989 parentContainer = $( '#customize-theme-controls > ul' ); // @todo This should be defined elsewhere, and to be configurable |
|
990 |
|
991 if ( ! panel.container.parent().is( parentContainer ) ) { |
|
992 parentContainer.append( panel.container ); |
|
993 } |
|
994 panel.deferred.embedded.resolve(); |
|
995 }, |
|
996 |
|
997 /** |
|
998 * @since 4.1.0 |
|
999 */ |
|
1000 attachEvents: function () { |
|
1001 var meta, panel = this; |
|
1002 |
|
1003 // Expand/Collapse accordion sections on click. |
|
1004 panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) { |
|
1005 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
|
1006 return; |
|
1007 } |
|
1008 event.preventDefault(); // Keep this AFTER the key filter above |
|
1009 |
|
1010 if ( ! panel.expanded() ) { |
|
1011 panel.expand(); |
|
1012 } |
|
1013 }); |
|
1014 |
|
1015 meta = panel.container.find( '.panel-meta:first' ); |
|
1016 |
|
1017 meta.find( '> .accordion-section-title' ).on( 'click keydown', function( event ) { |
|
1018 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
|
1019 return; |
|
1020 } |
|
1021 event.preventDefault(); // Keep this AFTER the key filter above |
|
1022 |
|
1023 if ( meta.hasClass( 'cannot-expand' ) ) { |
|
1024 return; |
|
1025 } |
|
1026 |
|
1027 var content = meta.find( '.accordion-section-content:first' ); |
|
1028 if ( meta.hasClass( 'open' ) ) { |
|
1029 meta.toggleClass( 'open' ); |
|
1030 content.slideUp( panel.defaultExpandedArguments.duration ); |
|
1031 } else { |
|
1032 content.slideDown( panel.defaultExpandedArguments.duration ); |
|
1033 meta.toggleClass( 'open' ); |
|
1034 } |
|
1035 }); |
|
1036 |
|
1037 }, |
|
1038 |
|
1039 /** |
|
1040 * Get the sections that are associated with this panel, sorted by their priority Value. |
|
1041 * |
|
1042 * @since 4.1.0 |
|
1043 * |
|
1044 * @returns {Array} |
|
1045 */ |
|
1046 sections: function () { |
|
1047 return this._children( 'panel', 'section' ); |
|
1048 }, |
|
1049 |
|
1050 /** |
|
1051 * Return whether this panel has any active sections. |
|
1052 * |
|
1053 * @since 4.1.0 |
|
1054 * |
|
1055 * @returns {boolean} |
|
1056 */ |
|
1057 isContextuallyActive: function () { |
|
1058 var panel = this, |
|
1059 sections = panel.sections(), |
|
1060 activeCount = 0; |
|
1061 _( sections ).each( function ( section ) { |
|
1062 if ( section.active() && section.isContextuallyActive() ) { |
|
1063 activeCount += 1; |
|
1064 } |
|
1065 } ); |
|
1066 return ( activeCount !== 0 ); |
|
1067 }, |
|
1068 |
|
1069 /** |
|
1070 * Update UI to reflect expanded state |
|
1071 * |
|
1072 * @since 4.1.0 |
|
1073 * |
|
1074 * @param {Boolean} expanded |
|
1075 * @param {Object} args |
|
1076 * @param {Boolean} args.unchanged |
|
1077 * @param {Callback} args.completeCallback |
|
1078 */ |
|
1079 onChangeExpanded: function ( expanded, args ) { |
|
1080 |
|
1081 // Immediately call the complete callback if there were no changes |
|
1082 if ( args.unchanged ) { |
|
1083 if ( args.completeCallback ) { |
|
1084 args.completeCallback(); |
|
1085 } |
|
1086 return; |
|
1087 } |
|
1088 |
|
1089 // Note: there is a second argument 'args' passed |
|
1090 var position, scroll, |
|
1091 panel = this, |
|
1092 section = panel.container.closest( '.accordion-section' ), |
|
1093 overlay = section.closest( '.wp-full-overlay' ), |
|
1094 container = section.closest( '.wp-full-overlay-sidebar-content' ), |
|
1095 siblings = container.find( '.open' ), |
|
1096 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ), |
|
1097 backBtn = overlay.find( '.control-panel-back' ), |
|
1098 panelTitle = section.find( '.accordion-section-title' ).first(), |
|
1099 content = section.find( '.control-panel-content' ); |
|
1100 |
|
1101 if ( expanded ) { |
|
1102 |
|
1103 // Collapse any sibling sections/panels |
|
1104 api.section.each( function ( section ) { |
|
1105 if ( ! section.panel() ) { |
|
1106 section.collapse( { duration: 0 } ); |
|
1107 } |
|
1108 }); |
|
1109 api.panel.each( function ( otherPanel ) { |
|
1110 if ( panel !== otherPanel ) { |
|
1111 otherPanel.collapse( { duration: 0 } ); |
|
1112 } |
|
1113 }); |
|
1114 |
|
1115 content.show( 0, function() { |
|
1116 content.parent().show(); |
|
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' ); |
|
1122 container.scrollTop( 0 ); |
|
1123 if ( args.completeCallback ) { |
|
1124 args.completeCallback(); |
|
1125 } |
|
1126 } ); |
|
1127 topPanel.attr( 'tabindex', '-1' ); |
|
1128 backBtn.attr( 'tabindex', '0' ); |
|
1129 backBtn.focus(); |
|
1130 } else { |
|
1131 siblings.removeClass( 'open' ); |
|
1132 section.removeClass( 'current-panel' ); |
|
1133 overlay.removeClass( 'in-sub-panel' ); |
|
1134 content.delay( 180 ).hide( 0, function() { |
|
1135 content.css( 'margin-top', 'inherit' ); // Reset |
|
1136 if ( args.completeCallback ) { |
|
1137 args.completeCallback(); |
|
1138 } |
|
1139 } ); |
|
1140 topPanel.attr( 'tabindex', '0' ); |
|
1141 backBtn.attr( 'tabindex', '-1' ); |
|
1142 panelTitle.focus(); |
|
1143 container.scrollTop( 0 ); |
|
1144 } |
|
1145 } |
|
1146 }); |
|
1147 |
|
1148 /** |
|
1149 * A Customizer Control. |
|
1150 * |
|
1151 * A control provides a UI element that allows a user to modify a Customizer Setting. |
|
1152 * |
|
1153 * @see PHP class WP_Customize_Control. |
|
1154 * |
|
1155 * @class |
|
1156 * @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 */ |
30 api.Control = api.Class.extend({ |
1170 api.Control = api.Class.extend({ |
|
1171 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop }, |
|
1172 |
31 initialize: function( id, options ) { |
1173 initialize: function( id, options ) { |
32 var control = this, |
1174 var control = this, |
33 nodes, radios, settings; |
1175 nodes, radios, settings; |
34 |
1176 |
35 this.params = {}; |
1177 control.params = {}; |
36 $.extend( this, options || {} ); |
1178 $.extend( control, options || {} ); |
37 |
1179 control.id = id; |
38 this.id = id; |
1180 control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); |
39 this.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); |
1181 control.templateSelector = 'customize-control-' + control.params.type + '-content'; |
40 this.container = $( this.selector ); |
1182 control.container = control.params.content ? $( control.params.content ) : $( control.selector ); |
41 |
1183 |
42 settings = $.map( this.params.settings, function( value ) { |
1184 control.deferred = { |
43 return value; |
1185 embedded: new $.Deferred() |
44 }); |
1186 }; |
45 |
1187 control.section = new api.Value(); |
46 api.apply( api, settings.concat( function() { |
1188 control.priority = new api.Value(); |
47 var key; |
1189 control.active = new api.Value(); |
48 |
1190 control.activeArgumentsQueue = []; |
49 control.settings = {}; |
|
50 for ( key in control.params.settings ) { |
|
51 control.settings[ key ] = api( control.params.settings[ key ] ); |
|
52 } |
|
53 |
|
54 control.setting = control.settings['default'] || null; |
|
55 control.ready(); |
|
56 }) ); |
|
57 |
1191 |
58 control.elements = []; |
1192 control.elements = []; |
59 |
1193 |
60 nodes = this.container.find('[data-customize-setting-link]'); |
1194 nodes = control.container.find('[data-customize-setting-link]'); |
61 radios = {}; |
1195 radios = {}; |
62 |
1196 |
63 nodes.each( function() { |
1197 nodes.each( function() { |
64 var node = $(this), |
1198 var node = $( this ), |
65 name; |
1199 name; |
66 |
1200 |
67 if ( node.is(':radio') ) { |
1201 if ( node.is( ':radio' ) ) { |
68 name = node.prop('name'); |
1202 name = node.prop( 'name' ); |
69 if ( radios[ name ] ) |
1203 if ( radios[ name ] ) { |
70 return; |
1204 return; |
|
1205 } |
71 |
1206 |
72 radios[ name ] = true; |
1207 radios[ name ] = true; |
73 node = nodes.filter( '[name="' + name + '"]' ); |
1208 node = nodes.filter( '[name="' + name + '"]' ); |
74 } |
1209 } |
75 |
1210 |
76 api( node.data('customizeSettingLink'), function( setting ) { |
1211 api( node.data( 'customizeSettingLink' ), function( setting ) { |
77 var element = new api.Element( node ); |
1212 var element = new api.Element( node ); |
78 control.elements.push( element ); |
1213 control.elements.push( element ); |
79 element.sync( setting ); |
1214 element.sync( setting ); |
80 element.set( setting() ); |
1215 element.set( setting() ); |
81 }); |
1216 }); |
82 }); |
1217 }); |
83 }, |
1218 |
84 |
1219 control.active.bind( function ( active ) { |
|
1220 var args = control.activeArgumentsQueue.shift(); |
|
1221 args = $.extend( {}, control.defaultActiveArguments, args ); |
|
1222 control.onChangeActive( active, args ); |
|
1223 } ); |
|
1224 |
|
1225 control.section.set( control.params.section ); |
|
1226 control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority ); |
|
1227 control.active.set( control.params.active ); |
|
1228 |
|
1229 api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] ); |
|
1230 |
|
1231 // Associate this control with its settings when they are created |
|
1232 settings = $.map( control.params.settings, function( value ) { |
|
1233 return value; |
|
1234 }); |
|
1235 api.apply( api, settings.concat( function () { |
|
1236 var key; |
|
1237 |
|
1238 control.settings = {}; |
|
1239 for ( key in control.params.settings ) { |
|
1240 control.settings[ key ] = api( control.params.settings[ key ] ); |
|
1241 } |
|
1242 |
|
1243 control.setting = control.settings['default'] || null; |
|
1244 |
|
1245 control.embed(); |
|
1246 }) ); |
|
1247 |
|
1248 control.deferred.embedded.done( function () { |
|
1249 control.ready(); |
|
1250 }); |
|
1251 }, |
|
1252 |
|
1253 /** |
|
1254 * Embed the control into the page. |
|
1255 */ |
|
1256 embed: function () { |
|
1257 var control = this, |
|
1258 inject; |
|
1259 |
|
1260 // Watch for changes to the section state |
|
1261 inject = function ( sectionId ) { |
|
1262 var parentContainer; |
|
1263 if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the frontend |
|
1264 return; |
|
1265 } |
|
1266 // Wait for the section to be registered |
|
1267 api.section( sectionId, function ( section ) { |
|
1268 // Wait for the section to be ready/initialized |
|
1269 section.deferred.embedded.done( function () { |
|
1270 parentContainer = section.container.find( 'ul:first' ); |
|
1271 if ( ! control.container.parent().is( parentContainer ) ) { |
|
1272 parentContainer.append( control.container ); |
|
1273 control.renderContent(); |
|
1274 } |
|
1275 control.deferred.embedded.resolve(); |
|
1276 }); |
|
1277 }); |
|
1278 }; |
|
1279 control.section.bind( inject ); |
|
1280 inject( control.section.get() ); |
|
1281 }, |
|
1282 |
|
1283 /** |
|
1284 * Triggered when the control's markup has been injected into the DOM. |
|
1285 * |
|
1286 * @abstract |
|
1287 */ |
85 ready: function() {}, |
1288 ready: function() {}, |
86 |
1289 |
|
1290 /** |
|
1291 * Normal controls do not expand, so just expand its parent |
|
1292 * |
|
1293 * @param {Object} [params] |
|
1294 */ |
|
1295 expand: function ( params ) { |
|
1296 api.section( this.section() ).expand( params ); |
|
1297 }, |
|
1298 |
|
1299 /** |
|
1300 * Bring the containing section and panel into view and then |
|
1301 * this control into view, focusing on the first input. |
|
1302 */ |
|
1303 focus: focus, |
|
1304 |
|
1305 /** |
|
1306 * Update UI in response to a change in the control's active state. |
|
1307 * This does not change the active state, it merely handles the behavior |
|
1308 * for when it does change. |
|
1309 * |
|
1310 * @since 4.1.0 |
|
1311 * |
|
1312 * @param {Boolean} active |
|
1313 * @param {Object} args |
|
1314 * @param {Number} args.duration |
|
1315 * @param {Callback} args.completeCallback |
|
1316 */ |
|
1317 onChangeActive: function ( active, args ) { |
|
1318 if ( ! $.contains( document, this.container ) ) { |
|
1319 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM |
|
1320 this.container.toggle( active ); |
|
1321 if ( args.completeCallback ) { |
|
1322 args.completeCallback(); |
|
1323 } |
|
1324 } else if ( active ) { |
|
1325 this.container.slideDown( args.duration, args.completeCallback ); |
|
1326 } else { |
|
1327 this.container.slideUp( args.duration, args.completeCallback ); |
|
1328 } |
|
1329 }, |
|
1330 |
|
1331 /** |
|
1332 * @deprecated 4.1.0 Use this.onChangeActive() instead. |
|
1333 */ |
|
1334 toggle: function ( active ) { |
|
1335 return this.onChangeActive( active, this.defaultActiveArguments ); |
|
1336 }, |
|
1337 |
|
1338 /** |
|
1339 * Shorthand way to enable the active state. |
|
1340 * |
|
1341 * @since 4.1.0 |
|
1342 * |
|
1343 * @param {Object} [params] |
|
1344 * @returns {Boolean} false if already active |
|
1345 */ |
|
1346 activate: Container.prototype.activate, |
|
1347 |
|
1348 /** |
|
1349 * Shorthand way to disable the active state. |
|
1350 * |
|
1351 * @since 4.1.0 |
|
1352 * |
|
1353 * @param {Object} [params] |
|
1354 * @returns {Boolean} false if already inactive |
|
1355 */ |
|
1356 deactivate: Container.prototype.deactivate, |
|
1357 |
|
1358 /** |
|
1359 * Re-use _toggleActive from Container class. |
|
1360 * |
|
1361 * @access private |
|
1362 */ |
|
1363 _toggleActive: Container.prototype._toggleActive, |
|
1364 |
87 dropdownInit: function() { |
1365 dropdownInit: function() { |
88 var control = this, |
1366 var control = this, |
89 statuses = this.container.find('.dropdown-status'), |
1367 statuses = this.container.find('.dropdown-status'), |
90 params = this.params, |
1368 params = this.params, |
91 update = function( to ) { |
1369 toggleFreeze = false, |
92 if ( typeof to === 'string' && params.statuses && params.statuses[ to ] ) |
1370 update = function( to ) { |
|
1371 if ( typeof to === 'string' && params.statuses && params.statuses[ to ] ) |
93 statuses.html( params.statuses[ to ] ).show(); |
1372 statuses.html( params.statuses[ to ] ).show(); |
94 else |
1373 else |
95 statuses.hide(); |
1374 statuses.hide(); |
96 }; |
1375 }; |
97 |
1376 |
98 var toggleFreeze = false; |
|
99 |
|
100 // Support the .dropdown class to open/close complex elements |
1377 // Support the .dropdown class to open/close complex elements |
101 this.container.on( 'click keydown', '.dropdown', function( event ) { |
1378 this.container.on( 'click keydown', '.dropdown', function( event ) { |
102 if ( event.type === 'keydown' && 13 !== event.which ) // enter |
1379 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
103 return; |
1380 return; |
|
1381 } |
104 |
1382 |
105 event.preventDefault(); |
1383 event.preventDefault(); |
106 |
1384 |
107 if (!toggleFreeze) |
1385 if (!toggleFreeze) |
108 control.container.toggleClass('open'); |
1386 control.container.toggleClass('open'); |
117 }, 400); |
1395 }, 400); |
118 }); |
1396 }); |
119 |
1397 |
120 this.setting.bind( update ); |
1398 this.setting.bind( update ); |
121 update( this.setting() ); |
1399 update( this.setting() ); |
|
1400 }, |
|
1401 |
|
1402 /** |
|
1403 * Render the control from its JS template, if it exists. |
|
1404 * |
|
1405 * The control's container must already exist in the DOM. |
|
1406 * |
|
1407 * @since 4.1.0 |
|
1408 */ |
|
1409 renderContent: function () { |
|
1410 var template, |
|
1411 control = this; |
|
1412 |
|
1413 // Replace the container element's content with the control. |
|
1414 if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) { |
|
1415 template = wp.template( control.templateSelector ); |
|
1416 if ( template && control.container ) { |
|
1417 control.container.html( template( control.params ) ); |
|
1418 } |
|
1419 } |
122 } |
1420 } |
123 }); |
1421 }); |
124 |
1422 |
|
1423 /** |
|
1424 * A colorpicker control. |
|
1425 * |
|
1426 * @class |
|
1427 * @augments wp.customize.Control |
|
1428 * @augments wp.customize.Class |
|
1429 */ |
125 api.ColorControl = api.Control.extend({ |
1430 api.ColorControl = api.Control.extend({ |
126 ready: function() { |
1431 ready: function() { |
127 var control = this, |
1432 var control = this, |
128 picker = this.container.find('.color-picker-hex'); |
1433 picker = this.container.find('.color-picker-hex'); |
129 |
1434 |
130 picker.val( control.setting() ).wpColorPicker({ |
1435 picker.val( control.setting() ).wpColorPicker({ |
131 change: function( event, options ) { |
1436 change: function() { |
132 control.setting.set( picker.wpColorPicker('color') ); |
1437 control.setting.set( picker.wpColorPicker('color') ); |
133 }, |
1438 }, |
134 clear: function() { |
1439 clear: function() { |
135 control.setting.set( false ); |
1440 control.setting.set( false ); |
136 } |
1441 } |
|
1442 }); |
|
1443 |
|
1444 this.setting.bind( function ( value ) { |
|
1445 picker.val( value ); |
|
1446 picker.wpColorPicker( 'color', value ); |
137 }); |
1447 }); |
138 } |
1448 } |
139 }); |
1449 }); |
140 |
1450 |
141 api.UploadControl = api.Control.extend({ |
1451 /** |
|
1452 * A control that implements the media modal. |
|
1453 * |
|
1454 * @class |
|
1455 * @augments wp.customize.Control |
|
1456 * @augments wp.customize.Class |
|
1457 */ |
|
1458 api.MediaControl = api.Control.extend({ |
|
1459 |
|
1460 /** |
|
1461 * When the control's DOM structure is ready, |
|
1462 * set up internal event bindings. |
|
1463 */ |
142 ready: function() { |
1464 ready: function() { |
143 var control = this; |
1465 var control = this; |
144 |
1466 // Shortcut so that we don't have to use _.bind every time we add a callback. |
145 this.params.removed = this.params.removed || ''; |
1467 _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' ); |
146 |
1468 |
147 this.success = $.proxy( this.success, this ); |
1469 // Bind events, with delegation to facilitate re-rendering. |
148 |
1470 control.container.on( 'click keydown', '.upload-button', control.openFrame ); |
149 this.uploader = $.extend({ |
1471 control.container.on( 'click keydown', '.upload-button', control.pausePlayer ); |
150 container: this.container, |
1472 control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame ); |
151 browser: this.container.find('.upload'), |
1473 control.container.on( 'click keydown', '.default-button', control.restoreDefault ); |
152 dropzone: this.container.find('.upload-dropzone'), |
1474 control.container.on( 'click keydown', '.remove-button', control.pausePlayer ); |
153 success: this.success, |
1475 control.container.on( 'click keydown', '.remove-button', control.removeFile ); |
154 plupload: {}, |
1476 control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer ); |
155 params: {} |
1477 |
156 }, this.uploader || {} ); |
1478 // Resize the player controls when it becomes visible (ie when section is expanded) |
157 |
1479 api.section( control.section() ).container |
158 if ( control.params.extensions ) { |
1480 .on( 'expanded', function() { |
159 control.uploader.plupload.filters = [{ |
1481 if ( control.player ) { |
160 title: api.l10n.allowedFiles, |
1482 control.player.setControlsSize(); |
161 extensions: control.params.extensions |
1483 } |
162 }]; |
1484 }) |
163 } |
1485 .on( 'collapsed', function() { |
164 |
1486 control.pausePlayer(); |
165 if ( control.params.context ) |
1487 }); |
166 control.uploader.params['post_data[context]'] = this.params.context; |
1488 |
167 |
1489 // Re-render whenever the control's setting changes. |
168 if ( api.settings.theme.stylesheet ) |
1490 control.setting.bind( function () { control.renderContent(); } ); |
169 control.uploader.params['post_data[theme]'] = api.settings.theme.stylesheet; |
1491 }, |
170 |
1492 |
171 this.uploader = new wp.Uploader( this.uploader ); |
1493 pausePlayer: function () { |
172 |
1494 this.player && this.player.pause(); |
173 this.remover = this.container.find('.remove'); |
1495 }, |
174 this.remover.on( 'click keydown', function( event ) { |
1496 |
175 if ( event.type === 'keydown' && 13 !== event.which ) // enter |
1497 cleanupPlayer: function () { |
176 return; |
1498 this.player && wp.media.mixin.removePlayer( this.player ); |
177 |
1499 }, |
178 control.setting.set( control.params.removed ); |
1500 |
179 event.preventDefault(); |
1501 /** |
180 }); |
1502 * Open the media modal. |
181 |
1503 */ |
182 this.removerVisibility = $.proxy( this.removerVisibility, this ); |
1504 openFrame: function( event ) { |
183 this.setting.bind( this.removerVisibility ); |
1505 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
184 this.removerVisibility( this.setting.get() ); |
1506 return; |
185 }, |
1507 } |
186 success: function( attachment ) { |
1508 |
187 this.setting.set( attachment.get('url') ); |
1509 event.preventDefault(); |
188 }, |
1510 |
189 removerVisibility: function( to ) { |
1511 if ( ! this.frame ) { |
190 this.remover.toggle( to != this.params.removed ); |
1512 this.initFrame(); |
|
1513 } |
|
1514 |
|
1515 this.frame.open(); |
|
1516 }, |
|
1517 |
|
1518 /** |
|
1519 * Create a media modal select frame, and store it so the instance can be reused when needed. |
|
1520 */ |
|
1521 initFrame: function() { |
|
1522 this.frame = wp.media({ |
|
1523 button: { |
|
1524 text: this.params.button_labels.frame_button |
|
1525 }, |
|
1526 states: [ |
|
1527 new wp.media.controller.Library({ |
|
1528 title: this.params.button_labels.frame_title, |
|
1529 library: wp.media.query({ type: this.params.mime_type }), |
|
1530 multiple: false, |
|
1531 date: false |
|
1532 }) |
|
1533 ] |
|
1534 }); |
|
1535 |
|
1536 // When a file is selected, run a callback. |
|
1537 this.frame.on( 'select', this.select ); |
|
1538 }, |
|
1539 |
|
1540 /** |
|
1541 * Callback handler for when an attachment is selected in the media modal. |
|
1542 * Gets the selected image information, and sets it within the control. |
|
1543 */ |
|
1544 select: function() { |
|
1545 // Get the attachment from the modal frame. |
|
1546 var node, |
|
1547 attachment = this.frame.state().get( 'selection' ).first().toJSON(), |
|
1548 mejsSettings = window._wpmejsSettings || {}; |
|
1549 |
|
1550 this.params.attachment = attachment; |
|
1551 |
|
1552 // Set the Customizer setting; the callback takes care of rendering. |
|
1553 this.setting( attachment.id ); |
|
1554 node = this.container.find( 'audio, video' ).get(0); |
|
1555 |
|
1556 // Initialize audio/video previews. |
|
1557 if ( node ) { |
|
1558 this.player = new MediaElementPlayer( node, mejsSettings ); |
|
1559 } else { |
|
1560 this.cleanupPlayer(); |
|
1561 } |
|
1562 }, |
|
1563 |
|
1564 /** |
|
1565 * Reset the setting to the default value. |
|
1566 */ |
|
1567 restoreDefault: function( event ) { |
|
1568 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
|
1569 return; |
|
1570 } |
|
1571 event.preventDefault(); |
|
1572 |
|
1573 this.params.attachment = this.params.defaultAttachment; |
|
1574 this.setting( this.params.defaultAttachment.url ); |
|
1575 }, |
|
1576 |
|
1577 /** |
|
1578 * Called when the "Remove" link is clicked. Empties the setting. |
|
1579 * |
|
1580 * @param {object} event jQuery Event object |
|
1581 */ |
|
1582 removeFile: function( event ) { |
|
1583 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
|
1584 return; |
|
1585 } |
|
1586 event.preventDefault(); |
|
1587 |
|
1588 this.params.attachment = {}; |
|
1589 this.setting( '' ); |
|
1590 this.renderContent(); // Not bound to setting change when emptying. |
191 } |
1591 } |
192 }); |
1592 }); |
193 |
1593 |
|
1594 /** |
|
1595 * An upload control, which utilizes the media modal. |
|
1596 * |
|
1597 * @class |
|
1598 * @augments wp.customize.MediaControl |
|
1599 * @augments wp.customize.Control |
|
1600 * @augments wp.customize.Class |
|
1601 */ |
|
1602 api.UploadControl = api.MediaControl.extend({ |
|
1603 |
|
1604 /** |
|
1605 * Callback handler for when an attachment is selected in the media modal. |
|
1606 * Gets the selected image information, and sets it within the control. |
|
1607 */ |
|
1608 select: function() { |
|
1609 // Get the attachment from the modal frame. |
|
1610 var node, |
|
1611 attachment = this.frame.state().get( 'selection' ).first().toJSON(), |
|
1612 mejsSettings = window._wpmejsSettings || {}; |
|
1613 |
|
1614 this.params.attachment = attachment; |
|
1615 |
|
1616 // Set the Customizer setting; the callback takes care of rendering. |
|
1617 this.setting( attachment.url ); |
|
1618 node = this.container.find( 'audio, video' ).get(0); |
|
1619 |
|
1620 // Initialize audio/video previews. |
|
1621 if ( node ) { |
|
1622 this.player = new MediaElementPlayer( node, mejsSettings ); |
|
1623 } else { |
|
1624 this.cleanupPlayer(); |
|
1625 } |
|
1626 }, |
|
1627 |
|
1628 // @deprecated |
|
1629 success: function() {}, |
|
1630 |
|
1631 // @deprecated |
|
1632 removerVisibility: function() {} |
|
1633 }); |
|
1634 |
|
1635 /** |
|
1636 * A control for uploading images. |
|
1637 * |
|
1638 * This control no longer needs to do anything more |
|
1639 * than what the upload control does in JS. |
|
1640 * |
|
1641 * @class |
|
1642 * @augments wp.customize.UploadControl |
|
1643 * @augments wp.customize.MediaControl |
|
1644 * @augments wp.customize.Control |
|
1645 * @augments wp.customize.Class |
|
1646 */ |
194 api.ImageControl = api.UploadControl.extend({ |
1647 api.ImageControl = api.UploadControl.extend({ |
|
1648 // @deprecated |
|
1649 thumbnailSrc: function() {} |
|
1650 }); |
|
1651 |
|
1652 /** |
|
1653 * A control for uploading background images. |
|
1654 * |
|
1655 * @class |
|
1656 * @augments wp.customize.UploadControl |
|
1657 * @augments wp.customize.MediaControl |
|
1658 * @augments wp.customize.Control |
|
1659 * @augments wp.customize.Class |
|
1660 */ |
|
1661 api.BackgroundControl = api.UploadControl.extend({ |
|
1662 |
|
1663 /** |
|
1664 * When the control's DOM structure is ready, |
|
1665 * set up internal event bindings. |
|
1666 */ |
195 ready: function() { |
1667 ready: function() { |
196 var control = this, |
1668 api.UploadControl.prototype.ready.apply( this, arguments ); |
197 panels; |
1669 }, |
198 |
1670 |
199 this.uploader = { |
1671 /** |
200 init: function( up ) { |
1672 * Callback handler for when an attachment is selected in the media modal. |
201 var fallback, button; |
1673 * Does an additional AJAX request for setting the background context. |
202 |
1674 */ |
203 if ( this.supports.dragdrop ) |
1675 select: function() { |
204 return; |
1676 api.UploadControl.prototype.select.apply( this, arguments ); |
205 |
1677 |
206 // Maintain references while wrapping the fallback button. |
1678 wp.ajax.post( 'custom-background-add', { |
207 fallback = control.container.find( '.upload-fallback' ); |
1679 nonce: _wpCustomizeBackground.nonces.add, |
208 button = fallback.children().detach(); |
1680 wp_customize: 'on', |
209 |
1681 theme: api.settings.theme.stylesheet, |
210 this.browser.detach().empty().append( button ); |
1682 attachment_id: this.params.attachment.id |
211 fallback.append( this.browser ).show(); |
1683 } ); |
212 } |
|
213 }; |
|
214 |
|
215 api.UploadControl.prototype.ready.call( this ); |
|
216 |
|
217 this.thumbnail = this.container.find('.preview-thumbnail img'); |
|
218 this.thumbnailSrc = $.proxy( this.thumbnailSrc, this ); |
|
219 this.setting.bind( this.thumbnailSrc ); |
|
220 |
|
221 this.library = this.container.find('.library'); |
|
222 |
|
223 // Generate tab objects |
|
224 this.tabs = {}; |
|
225 panels = this.library.find('.library-content'); |
|
226 |
|
227 this.library.children('ul').children('li').each( function() { |
|
228 var link = $(this), |
|
229 id = link.data('customizeTab'), |
|
230 panel = panels.filter('[data-customize-tab="' + id + '"]'); |
|
231 |
|
232 control.tabs[ id ] = { |
|
233 both: link.add( panel ), |
|
234 link: link, |
|
235 panel: panel |
|
236 }; |
|
237 }); |
|
238 |
|
239 // Bind tab switch events |
|
240 this.library.children('ul').on( 'click keydown', 'li', function( event ) { |
|
241 if ( event.type === 'keydown' && 13 !== event.which ) // enter |
|
242 return; |
|
243 |
|
244 var id = $(this).data('customizeTab'), |
|
245 tab = control.tabs[ id ]; |
|
246 |
|
247 event.preventDefault(); |
|
248 |
|
249 if ( tab.link.hasClass('library-selected') ) |
|
250 return; |
|
251 |
|
252 control.selected.both.removeClass('library-selected'); |
|
253 control.selected = tab; |
|
254 control.selected.both.addClass('library-selected'); |
|
255 }); |
|
256 |
|
257 // Bind events to switch image urls. |
|
258 this.library.on( 'click keydown', 'a', function( event ) { |
|
259 if ( event.type === 'keydown' && 13 !== event.which ) // enter |
|
260 return; |
|
261 |
|
262 var value = $(this).data('customizeImageValue'); |
|
263 |
|
264 if ( value ) { |
|
265 control.setting.set( value ); |
|
266 event.preventDefault(); |
|
267 } |
|
268 }); |
|
269 |
|
270 if ( this.tabs.uploaded ) { |
|
271 this.tabs.uploaded.target = this.library.find('.uploaded-target'); |
|
272 if ( ! this.tabs.uploaded.panel.find('.thumbnail').length ) |
|
273 this.tabs.uploaded.both.addClass('hidden'); |
|
274 } |
|
275 |
|
276 // Select a tab |
|
277 panels.each( function() { |
|
278 var tab = control.tabs[ $(this).data('customizeTab') ]; |
|
279 |
|
280 // Select the first visible tab. |
|
281 if ( ! tab.link.hasClass('hidden') ) { |
|
282 control.selected = tab; |
|
283 tab.both.addClass('library-selected'); |
|
284 return false; |
|
285 } |
|
286 }); |
|
287 |
|
288 this.dropdownInit(); |
|
289 }, |
|
290 success: function( attachment ) { |
|
291 api.UploadControl.prototype.success.call( this, attachment ); |
|
292 |
|
293 // Add the uploaded image to the uploaded tab. |
|
294 if ( this.tabs.uploaded && this.tabs.uploaded.target.length ) { |
|
295 this.tabs.uploaded.both.removeClass('hidden'); |
|
296 |
|
297 // @todo: Do NOT store this on the attachment model. That is bad. |
|
298 attachment.element = $( '<a href="#" class="thumbnail"></a>' ) |
|
299 .data( 'customizeImageValue', attachment.get('url') ) |
|
300 .append( '<img src="' + attachment.get('url')+ '" />' ) |
|
301 .appendTo( this.tabs.uploaded.target ); |
|
302 } |
|
303 }, |
|
304 thumbnailSrc: function( to ) { |
|
305 if ( /^(https?:)?\/\//.test( to ) ) |
|
306 this.thumbnail.prop( 'src', to ).show(); |
|
307 else |
|
308 this.thumbnail.hide(); |
|
309 } |
1684 } |
310 }); |
1685 }); |
311 |
1686 |
|
1687 /** |
|
1688 * @class |
|
1689 * @augments wp.customize.Control |
|
1690 * @augments wp.customize.Class |
|
1691 */ |
|
1692 api.HeaderControl = api.Control.extend({ |
|
1693 ready: function() { |
|
1694 this.btnRemove = $('#customize-control-header_image .actions .remove'); |
|
1695 this.btnNew = $('#customize-control-header_image .actions .new'); |
|
1696 |
|
1697 _.bindAll(this, 'openMedia', 'removeImage'); |
|
1698 |
|
1699 this.btnNew.on( 'click', this.openMedia ); |
|
1700 this.btnRemove.on( 'click', this.removeImage ); |
|
1701 |
|
1702 api.HeaderTool.currentHeader = this.getInitialHeaderImage(); |
|
1703 |
|
1704 new api.HeaderTool.CurrentView({ |
|
1705 model: api.HeaderTool.currentHeader, |
|
1706 el: '#customize-control-header_image .current .container' |
|
1707 }); |
|
1708 |
|
1709 new api.HeaderTool.ChoiceListView({ |
|
1710 collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(), |
|
1711 el: '#customize-control-header_image .choices .uploaded .list' |
|
1712 }); |
|
1713 |
|
1714 new api.HeaderTool.ChoiceListView({ |
|
1715 collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(), |
|
1716 el: '#customize-control-header_image .choices .default .list' |
|
1717 }); |
|
1718 |
|
1719 api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([ |
|
1720 api.HeaderTool.UploadsList, |
|
1721 api.HeaderTool.DefaultsList |
|
1722 ]); |
|
1723 }, |
|
1724 |
|
1725 /** |
|
1726 * Returns a new instance of api.HeaderTool.ImageModel based on the currently |
|
1727 * saved header image (if any). |
|
1728 * |
|
1729 * @since 4.2.0 |
|
1730 * |
|
1731 * @returns {Object} Options |
|
1732 */ |
|
1733 getInitialHeaderImage: function() { |
|
1734 if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) { |
|
1735 return new api.HeaderTool.ImageModel(); |
|
1736 } |
|
1737 |
|
1738 // Get the matching uploaded image object. |
|
1739 var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) { |
|
1740 return ( imageObj.attachment_id === api.get().header_image_data.attachment_id ); |
|
1741 } ); |
|
1742 // Fall back to raw current header image. |
|
1743 if ( ! currentHeaderObject ) { |
|
1744 currentHeaderObject = { |
|
1745 url: api.get().header_image, |
|
1746 thumbnail_url: api.get().header_image, |
|
1747 attachment_id: api.get().header_image_data.attachment_id |
|
1748 }; |
|
1749 } |
|
1750 |
|
1751 return new api.HeaderTool.ImageModel({ |
|
1752 header: currentHeaderObject, |
|
1753 choice: currentHeaderObject.url.split( '/' ).pop() |
|
1754 }); |
|
1755 }, |
|
1756 |
|
1757 /** |
|
1758 * Returns a set of options, computed from the attached image data and |
|
1759 * theme-specific data, to be fed to the imgAreaSelect plugin in |
|
1760 * wp.media.view.Cropper. |
|
1761 * |
|
1762 * @param {wp.media.model.Attachment} attachment |
|
1763 * @param {wp.media.controller.Cropper} controller |
|
1764 * @returns {Object} Options |
|
1765 */ |
|
1766 calculateImageSelectOptions: function(attachment, controller) { |
|
1767 var xInit = parseInt(_wpCustomizeHeader.data.width, 10), |
|
1768 yInit = parseInt(_wpCustomizeHeader.data.height, 10), |
|
1769 flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10), |
|
1770 flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10), |
|
1771 ratio, xImg, yImg, realHeight, realWidth, |
|
1772 imgSelectOptions; |
|
1773 |
|
1774 realWidth = attachment.get('width'); |
|
1775 realHeight = attachment.get('height'); |
|
1776 |
|
1777 this.headerImage = new api.HeaderTool.ImageModel(); |
|
1778 this.headerImage.set({ |
|
1779 themeWidth: xInit, |
|
1780 themeHeight: yInit, |
|
1781 themeFlexWidth: flexWidth, |
|
1782 themeFlexHeight: flexHeight, |
|
1783 imageWidth: realWidth, |
|
1784 imageHeight: realHeight |
|
1785 }); |
|
1786 |
|
1787 controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() ); |
|
1788 |
|
1789 ratio = xInit / yInit; |
|
1790 xImg = realWidth; |
|
1791 yImg = realHeight; |
|
1792 |
|
1793 if ( xImg / yImg > ratio ) { |
|
1794 yInit = yImg; |
|
1795 xInit = yInit * ratio; |
|
1796 } else { |
|
1797 xInit = xImg; |
|
1798 yInit = xInit / ratio; |
|
1799 } |
|
1800 |
|
1801 imgSelectOptions = { |
|
1802 handles: true, |
|
1803 keys: true, |
|
1804 instance: true, |
|
1805 persistent: true, |
|
1806 imageWidth: realWidth, |
|
1807 imageHeight: realHeight, |
|
1808 x1: 0, |
|
1809 y1: 0, |
|
1810 x2: xInit, |
|
1811 y2: yInit |
|
1812 }; |
|
1813 |
|
1814 if (flexHeight === false && flexWidth === false) { |
|
1815 imgSelectOptions.aspectRatio = xInit + ':' + yInit; |
|
1816 } |
|
1817 if (flexHeight === false ) { |
|
1818 imgSelectOptions.maxHeight = yInit; |
|
1819 } |
|
1820 if (flexWidth === false ) { |
|
1821 imgSelectOptions.maxWidth = xInit; |
|
1822 } |
|
1823 |
|
1824 return imgSelectOptions; |
|
1825 }, |
|
1826 |
|
1827 /** |
|
1828 * Sets up and opens the Media Manager in order to select an image. |
|
1829 * Depending on both the size of the image and the properties of the |
|
1830 * current theme, a cropping step after selection may be required or |
|
1831 * skippable. |
|
1832 * |
|
1833 * @param {event} event |
|
1834 */ |
|
1835 openMedia: function(event) { |
|
1836 var l10n = _wpMediaViewsL10n; |
|
1837 |
|
1838 event.preventDefault(); |
|
1839 |
|
1840 this.frame = wp.media({ |
|
1841 button: { |
|
1842 text: l10n.selectAndCrop, |
|
1843 close: false |
|
1844 }, |
|
1845 states: [ |
|
1846 new wp.media.controller.Library({ |
|
1847 title: l10n.chooseImage, |
|
1848 library: wp.media.query({ type: 'image' }), |
|
1849 multiple: false, |
|
1850 date: false, |
|
1851 priority: 20, |
|
1852 suggestedWidth: _wpCustomizeHeader.data.width, |
|
1853 suggestedHeight: _wpCustomizeHeader.data.height |
|
1854 }), |
|
1855 new wp.media.controller.Cropper({ |
|
1856 imgSelectOptions: this.calculateImageSelectOptions |
|
1857 }) |
|
1858 ] |
|
1859 }); |
|
1860 |
|
1861 this.frame.on('select', this.onSelect, this); |
|
1862 this.frame.on('cropped', this.onCropped, this); |
|
1863 this.frame.on('skippedcrop', this.onSkippedCrop, this); |
|
1864 |
|
1865 this.frame.open(); |
|
1866 }, |
|
1867 |
|
1868 /** |
|
1869 * After an image is selected in the media modal, |
|
1870 * switch to the cropper state. |
|
1871 */ |
|
1872 onSelect: function() { |
|
1873 this.frame.setState('cropper'); |
|
1874 }, |
|
1875 |
|
1876 /** |
|
1877 * After the image has been cropped, apply the cropped image data to the setting. |
|
1878 * |
|
1879 * @param {object} croppedImage Cropped attachment data. |
|
1880 */ |
|
1881 onCropped: function(croppedImage) { |
|
1882 var url = croppedImage.post_content, |
|
1883 attachmentId = croppedImage.attachment_id, |
|
1884 w = croppedImage.width, |
|
1885 h = croppedImage.height; |
|
1886 this.setImageFromURL(url, attachmentId, w, h); |
|
1887 }, |
|
1888 |
|
1889 /** |
|
1890 * If cropping was skipped, apply the image data directly to the setting. |
|
1891 * |
|
1892 * @param {object} selection |
|
1893 */ |
|
1894 onSkippedCrop: function(selection) { |
|
1895 var url = selection.get('url'), |
|
1896 w = selection.get('width'), |
|
1897 h = selection.get('height'); |
|
1898 this.setImageFromURL(url, selection.id, w, h); |
|
1899 }, |
|
1900 |
|
1901 /** |
|
1902 * Creates a new wp.customize.HeaderTool.ImageModel from provided |
|
1903 * header image data and inserts it into the user-uploaded headers |
|
1904 * collection. |
|
1905 * |
|
1906 * @param {String} url |
|
1907 * @param {Number} attachmentId |
|
1908 * @param {Number} width |
|
1909 * @param {Number} height |
|
1910 */ |
|
1911 setImageFromURL: function(url, attachmentId, width, height) { |
|
1912 var choice, data = {}; |
|
1913 |
|
1914 data.url = url; |
|
1915 data.thumbnail_url = url; |
|
1916 data.timestamp = _.now(); |
|
1917 |
|
1918 if (attachmentId) { |
|
1919 data.attachment_id = attachmentId; |
|
1920 } |
|
1921 |
|
1922 if (width) { |
|
1923 data.width = width; |
|
1924 } |
|
1925 |
|
1926 if (height) { |
|
1927 data.height = height; |
|
1928 } |
|
1929 |
|
1930 choice = new api.HeaderTool.ImageModel({ |
|
1931 header: data, |
|
1932 choice: url.split('/').pop() |
|
1933 }); |
|
1934 api.HeaderTool.UploadsList.add(choice); |
|
1935 api.HeaderTool.currentHeader.set(choice.toJSON()); |
|
1936 choice.save(); |
|
1937 choice.importImage(); |
|
1938 }, |
|
1939 |
|
1940 /** |
|
1941 * Triggers the necessary events to deselect an image which was set as |
|
1942 * the currently selected one. |
|
1943 */ |
|
1944 removeImage: function() { |
|
1945 api.HeaderTool.currentHeader.trigger('hide'); |
|
1946 api.HeaderTool.CombinedList.trigger('control:removeImage'); |
|
1947 } |
|
1948 |
|
1949 }); |
|
1950 |
|
1951 /** |
|
1952 * wp.customize.ThemeControl |
|
1953 * |
|
1954 * @constructor |
|
1955 * @augments wp.customize.Control |
|
1956 * @augments wp.customize.Class |
|
1957 */ |
|
1958 api.ThemeControl = api.Control.extend({ |
|
1959 |
|
1960 touchDrag: false, |
|
1961 isRendered: false, |
|
1962 |
|
1963 /** |
|
1964 * Defer rendering the theme control until the section is displayed. |
|
1965 * |
|
1966 * @since 4.2.0 |
|
1967 */ |
|
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() { |
|
1991 var control = this; |
|
1992 |
|
1993 control.container.on( 'touchmove', '.theme', function() { |
|
1994 control.touchDrag = true; |
|
1995 }); |
|
1996 |
|
1997 // Bind details view trigger. |
|
1998 control.container.on( 'click keydown touchend', '.theme', function( event ) { |
|
1999 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
|
2000 return; |
|
2001 } |
|
2002 |
|
2003 // Bail if the user scrolled on a touch device. |
|
2004 if ( control.touchDrag === true ) { |
|
2005 return control.touchDrag = false; |
|
2006 } |
|
2007 |
|
2008 // Prevent the modal from showing when the user clicks the action button. |
|
2009 if ( $( event.target ).is( '.theme-actions .button' ) ) { |
|
2010 return; |
|
2011 } |
|
2012 |
|
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 |
|
2026 |
|
2027 api.section( control.section() ).showDetails( control.params.theme ); |
|
2028 }); |
|
2029 |
|
2030 control.container.on( 'render-screenshot', function() { |
|
2031 var $screenshot = $( this ).find( 'img' ), |
|
2032 source = $screenshot.data( 'src' ); |
|
2033 |
|
2034 if ( source ) { |
|
2035 $screenshot.attr( 'src', source ); |
|
2036 } |
|
2037 }); |
|
2038 }, |
|
2039 |
|
2040 /** |
|
2041 * Show or hide the theme based on the presence of the term in the title, description, and author. |
|
2042 * |
|
2043 * @since 4.2.0 |
|
2044 */ |
|
2045 filter: function( term ) { |
|
2046 var control = this, |
|
2047 haystack = control.params.theme.name + ' ' + |
|
2048 control.params.theme.description + ' ' + |
|
2049 control.params.theme.tags + ' ' + |
|
2050 control.params.theme.author; |
|
2051 haystack = haystack.toLowerCase().replace( '-', ' ' ); |
|
2052 if ( -1 !== haystack.search( term ) ) { |
|
2053 control.activate(); |
|
2054 } else { |
|
2055 control.deactivate(); |
|
2056 } |
|
2057 } |
|
2058 }); |
|
2059 |
312 // Change objects contained within the main customize object to Settings. |
2060 // Change objects contained within the main customize object to Settings. |
313 api.defaultConstructor = api.Setting; |
2061 api.defaultConstructor = api.Setting; |
314 |
2062 |
315 // Create the collection of Control objects. |
2063 // Create the collections for Controls, Sections and Panels. |
316 api.control = new api.Values({ defaultConstructor: api.Control }); |
2064 api.control = new api.Values({ defaultConstructor: api.Control }); |
317 |
2065 api.section = new api.Values({ defaultConstructor: api.Section }); |
|
2066 api.panel = new api.Values({ defaultConstructor: api.Panel }); |
|
2067 |
|
2068 /** |
|
2069 * @class |
|
2070 * @augments wp.customize.Messenger |
|
2071 * @augments wp.customize.Class |
|
2072 * @mixes wp.customize.Events |
|
2073 */ |
318 api.PreviewFrame = api.Messenger.extend({ |
2074 api.PreviewFrame = api.Messenger.extend({ |
319 sensitivity: 2000, |
2075 sensitivity: 2000, |
320 |
2076 |
321 initialize: function( params, options ) { |
2077 initialize: function( params, options ) { |
322 var deferred = $.Deferred(), |
2078 var deferred = $.Deferred(); |
323 self = this; |
|
324 |
2079 |
325 // This is the promise object. |
2080 // This is the promise object. |
326 deferred.promise( this ); |
2081 deferred.promise( this ); |
327 |
2082 |
328 this.container = params.container; |
2083 this.container = params.container; |
663 messenger = new api.Messenger({ |
2488 messenger = new api.Messenger({ |
664 channel: 'login', |
2489 channel: 'login', |
665 url: api.settings.url.login |
2490 url: api.settings.url.login |
666 }); |
2491 }); |
667 |
2492 |
668 iframe = $('<iframe src="' + api.settings.url.login + '" />').appendTo( this.container ); |
2493 iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container ); |
669 |
2494 |
670 messenger.targetWindow( iframe[0].contentWindow ); |
2495 messenger.targetWindow( iframe[0].contentWindow ); |
671 |
2496 |
672 messenger.bind( 'login', function() { |
2497 messenger.bind( 'login', function () { |
673 iframe.remove(); |
2498 var refreshNonces = previewer.refreshNonces(); |
674 messenger.destroy(); |
2499 |
675 delete previewer._login; |
2500 refreshNonces.always( function() { |
676 deferred.resolve(); |
2501 iframe.remove(); |
|
2502 messenger.destroy(); |
|
2503 delete previewer._login; |
|
2504 }); |
|
2505 |
|
2506 refreshNonces.done( function() { |
|
2507 deferred.resolve(); |
|
2508 }); |
|
2509 |
|
2510 refreshNonces.fail( function() { |
|
2511 previewer.cheatin(); |
|
2512 deferred.reject(); |
|
2513 }); |
677 }); |
2514 }); |
678 |
2515 |
679 return this._login; |
2516 return this._login; |
680 }, |
2517 }, |
681 |
2518 |
682 cheatin: function() { |
2519 cheatin: function() { |
683 $( document.body ).empty().addClass('cheatin').append( '<p>' + api.l10n.cheatin + '</p>' ); |
2520 $( document.body ).empty().addClass('cheatin').append( '<p>' + api.l10n.cheatin + '</p>' ); |
|
2521 }, |
|
2522 |
|
2523 refreshNonces: function() { |
|
2524 var request, deferred = $.Deferred(); |
|
2525 |
|
2526 deferred.promise(); |
|
2527 |
|
2528 request = wp.ajax.post( 'customize_refresh_nonces', { |
|
2529 wp_customize: 'on', |
|
2530 theme: api.settings.theme.stylesheet |
|
2531 }); |
|
2532 |
|
2533 request.done( function( response ) { |
|
2534 api.trigger( 'nonce-refresh', response ); |
|
2535 deferred.resolve(); |
|
2536 }); |
|
2537 |
|
2538 request.fail( function() { |
|
2539 deferred.reject(); |
|
2540 }); |
|
2541 |
|
2542 return deferred; |
684 } |
2543 } |
685 }); |
2544 }); |
686 |
2545 |
687 /* ===================================================================== |
|
688 * Ready. |
|
689 * ===================================================================== */ |
|
690 |
|
691 api.controlConstructor = { |
2546 api.controlConstructor = { |
692 color: api.ColorControl, |
2547 color: api.ColorControl, |
693 upload: api.UploadControl, |
2548 media: api.MediaControl, |
694 image: api.ImageControl |
2549 upload: api.UploadControl, |
|
2550 image: api.ImageControl, |
|
2551 header: api.HeaderControl, |
|
2552 background: api.BackgroundControl, |
|
2553 theme: api.ThemeControl |
|
2554 }; |
|
2555 api.panelConstructor = {}; |
|
2556 api.sectionConstructor = { |
|
2557 themes: api.ThemesSection |
695 }; |
2558 }; |
696 |
2559 |
697 $( function() { |
2560 $( function() { |
698 api.settings = window._wpCustomizeSettings; |
2561 api.settings = window._wpCustomizeSettings; |
699 api.l10n = window._wpCustomizeControlsL10n; |
2562 api.l10n = window._wpCustomizeControlsL10n; |
700 |
2563 |
701 // Check if we can run the customizer. |
2564 // Check if we can run the Customizer. |
702 if ( ! api.settings ) |
2565 if ( ! api.settings ) { |
703 return; |
2566 return; |
|
2567 } |
704 |
2568 |
705 // Redirect to the fallback preview if any incompatibilities are found. |
2569 // Redirect to the fallback preview if any incompatibilities are found. |
706 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) |
2570 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) |
707 return window.location = api.settings.url.fallback; |
2571 return window.location = api.settings.url.fallback; |
708 |
2572 |
709 var body = $( document.body ), |
2573 var parent, topFocus, |
710 overlay = body.children('.wp-full-overlay'), |
2574 body = $( document.body ), |
711 query, previewer, parent; |
2575 overlay = body.children( '.wp-full-overlay' ), |
712 |
2576 title = $( '#customize-info .theme-name.site-title' ), |
713 // Prevent the form from saving when enter is pressed. |
2577 closeBtn = $( '.customize-controls-close' ), |
|
2578 saveBtn = $( '#save' ); |
|
2579 |
|
2580 // Prevent the form from saving when enter is pressed on an input or select element. |
714 $('#customize-controls').on( 'keydown', function( e ) { |
2581 $('#customize-controls').on( 'keydown', function( e ) { |
715 if ( $( e.target ).is('textarea') ) |
2582 var isEnter = ( 13 === e.which ), |
|
2583 $el = $( e.target ); |
|
2584 |
|
2585 if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) { |
|
2586 e.preventDefault(); |
|
2587 } |
|
2588 }); |
|
2589 |
|
2590 // Expand/Collapse the main customizer customize info. |
|
2591 $( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( event ) { |
|
2592 if ( api.utils.isKeydownButNotEnterEvent( event ) ) { |
716 return; |
2593 return; |
717 |
2594 } |
718 if ( 13 === e.which ) // Enter |
2595 event.preventDefault(); // Keep this AFTER the key filter above |
719 e.preventDefault(); |
2596 |
|
2597 var section = $( this ).parent(), |
|
2598 content = section.find( '.accordion-section-content:first' ); |
|
2599 |
|
2600 if ( section.hasClass( 'cannot-expand' ) ) { |
|
2601 return; |
|
2602 } |
|
2603 |
|
2604 if ( section.hasClass( 'open' ) ) { |
|
2605 section.toggleClass( 'open' ); |
|
2606 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration ); |
|
2607 } else { |
|
2608 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration ); |
|
2609 section.toggleClass( 'open' ); |
|
2610 } |
720 }); |
2611 }); |
721 |
2612 |
722 // Initialize Previewer |
2613 // Initialize Previewer |
723 previewer = new api.Previewer({ |
2614 api.previewer = new api.Previewer({ |
724 container: '#customize-preview', |
2615 container: '#customize-preview', |
725 form: '#customize-controls', |
2616 form: '#customize-controls', |
726 previewUrl: api.settings.url.preview, |
2617 previewUrl: api.settings.url.preview, |
727 allowedUrls: api.settings.url.allowed, |
2618 allowedUrls: api.settings.url.allowed, |
728 signature: 'WP_CUSTOMIZER_SIGNATURE' |
2619 signature: 'WP_CUSTOMIZER_SIGNATURE' |
729 }, { |
2620 }, { |
730 |
2621 |
731 nonce: api.settings.nonce, |
2622 nonce: api.settings.nonce, |
732 |
2623 |
733 query: function() { |
2624 query: function() { |
|
2625 var dirtyCustomized = {}; |
|
2626 api.each( function ( value, key ) { |
|
2627 if ( value._dirty ) { |
|
2628 dirtyCustomized[ key ] = value(); |
|
2629 } |
|
2630 } ); |
|
2631 |
734 return { |
2632 return { |
735 wp_customize: 'on', |
2633 wp_customize: 'on', |
736 theme: api.settings.theme.stylesheet, |
2634 theme: api.settings.theme.stylesheet, |
737 customized: JSON.stringify( api.get() ), |
2635 customized: JSON.stringify( dirtyCustomized ), |
738 nonce: this.nonce.preview |
2636 nonce: this.nonce.preview |
739 }; |
2637 }; |
740 }, |
2638 }, |
741 |
2639 |
742 save: function() { |
2640 save: function() { |
743 var self = this, |
2641 var self = this, |
744 query = $.extend( this.query(), { |
2642 processing = api.state( 'processing' ), |
745 action: 'customize_save', |
2643 submitWhenDoneProcessing, |
746 nonce: this.nonce.save |
2644 submit; |
747 }), |
2645 |
748 request = $.post( api.settings.url.ajax, query ); |
2646 body.addClass( 'saving' ); |
749 |
2647 |
750 api.trigger( 'save', request ); |
2648 submit = function () { |
751 |
2649 var request, query; |
752 body.addClass('saving'); |
2650 query = $.extend( self.query(), { |
753 |
2651 nonce: self.nonce.save |
754 request.always( function() { |
2652 } ); |
755 body.removeClass('saving'); |
2653 request = wp.ajax.post( 'customize_save', query ); |
756 }); |
2654 |
757 |
2655 api.trigger( 'save', request ); |
758 request.done( function( response ) { |
2656 |
759 // Check if the user is logged out. |
2657 request.always( function () { |
760 if ( '0' === response ) { |
2658 body.removeClass( 'saving' ); |
761 self.preview.iframe.hide(); |
2659 } ); |
762 self.login().done( function() { |
2660 |
763 self.save(); |
2661 request.fail( function ( response ) { |
764 self.preview.iframe.show(); |
2662 if ( '0' === response ) { |
765 }); |
2663 response = 'not_logged_in'; |
766 return; |
2664 } else if ( '-1' === response ) { |
767 } |
2665 // Back-compat in case any other check_ajax_referer() call is dying |
768 |
2666 response = 'invalid_nonce'; |
769 // Check for cheaters. |
2667 } |
770 if ( '-1' === response ) { |
2668 |
771 self.cheatin(); |
2669 if ( 'invalid_nonce' === response ) { |
772 return; |
2670 self.cheatin(); |
773 } |
2671 } else if ( 'not_logged_in' === response ) { |
774 |
2672 self.preview.iframe.hide(); |
775 api.trigger( 'saved' ); |
2673 self.login().done( function() { |
776 }); |
2674 self.save(); |
|
2675 self.preview.iframe.show(); |
|
2676 } ); |
|
2677 } |
|
2678 api.trigger( 'error', response ); |
|
2679 } ); |
|
2680 |
|
2681 request.done( function( response ) { |
|
2682 // Clear setting dirty states |
|
2683 api.each( function ( value ) { |
|
2684 value._dirty = false; |
|
2685 } ); |
|
2686 |
|
2687 api.trigger( 'saved', response ); |
|
2688 } ); |
|
2689 }; |
|
2690 |
|
2691 if ( 0 === processing() ) { |
|
2692 submit(); |
|
2693 } else { |
|
2694 submitWhenDoneProcessing = function () { |
|
2695 if ( 0 === processing() ) { |
|
2696 api.state.unbind( 'change', submitWhenDoneProcessing ); |
|
2697 submit(); |
|
2698 } |
|
2699 }; |
|
2700 api.state.bind( 'change', submitWhenDoneProcessing ); |
|
2701 } |
|
2702 |
777 } |
2703 } |
778 }); |
2704 }); |
779 |
2705 |
780 // Refresh the nonces if the preview sends updated nonces over. |
2706 // Refresh the nonces if the preview sends updated nonces over. |
781 previewer.bind( 'nonce', function( nonce ) { |
2707 api.previewer.bind( 'nonce', function( nonce ) { |
782 $.extend( this.nonce, nonce ); |
2708 $.extend( this.nonce, nonce ); |
783 }); |
2709 }); |
784 |
2710 |
|
2711 // Refresh the nonces if login sends updated nonces over. |
|
2712 api.bind( 'nonce-refresh', function( nonce ) { |
|
2713 $.extend( api.settings.nonce, nonce ); |
|
2714 $.extend( api.previewer.nonce, nonce ); |
|
2715 }); |
|
2716 |
|
2717 // Create Settings |
785 $.each( api.settings.settings, function( id, data ) { |
2718 $.each( api.settings.settings, function( id, data ) { |
786 api.create( id, id, data.value, { |
2719 api.create( id, id, data.value, { |
787 transport: data.transport, |
2720 transport: data.transport, |
788 previewer: previewer |
2721 previewer: api.previewer, |
|
2722 dirty: !! data.dirty |
789 } ); |
2723 } ); |
790 }); |
2724 }); |
791 |
2725 |
|
2726 // Create Panels |
|
2727 $.each( api.settings.panels, function ( id, data ) { |
|
2728 var constructor = api.panelConstructor[ data.type ] || api.Panel, |
|
2729 panel; |
|
2730 |
|
2731 panel = new constructor( id, { |
|
2732 params: data |
|
2733 } ); |
|
2734 api.panel.add( id, panel ); |
|
2735 }); |
|
2736 |
|
2737 // Create Sections |
|
2738 $.each( api.settings.sections, function ( id, data ) { |
|
2739 var constructor = api.sectionConstructor[ data.type ] || api.Section, |
|
2740 section; |
|
2741 |
|
2742 section = new constructor( id, { |
|
2743 params: data |
|
2744 } ); |
|
2745 api.section.add( id, section ); |
|
2746 }); |
|
2747 |
|
2748 // Create Controls |
792 $.each( api.settings.controls, function( id, data ) { |
2749 $.each( api.settings.controls, function( id, data ) { |
793 var constructor = api.controlConstructor[ data.type ] || api.Control, |
2750 var constructor = api.controlConstructor[ data.type ] || api.Control, |
794 control; |
2751 control; |
795 |
2752 |
796 control = api.control.add( id, new constructor( id, { |
2753 control = new constructor( id, { |
797 params: data, |
2754 params: data, |
798 previewer: previewer |
2755 previewer: api.previewer |
799 } ) ); |
2756 } ); |
|
2757 api.control.add( id, control ); |
800 }); |
2758 }); |
801 |
2759 |
|
2760 // Focus the autofocused element |
|
2761 _.each( [ 'panel', 'section', 'control' ], function ( type ) { |
|
2762 var instance, id = api.settings.autofocus[ type ]; |
|
2763 if ( id && api[ type ]( id ) ) { |
|
2764 instance = api[ type ]( id ); |
|
2765 // Wait until the element is embedded in the DOM |
|
2766 instance.deferred.embedded.done( function () { |
|
2767 // Wait until the preview has activated and so active panels, sections, controls have been set |
|
2768 api.previewer.deferred.active.done( function () { |
|
2769 instance.focus(); |
|
2770 }); |
|
2771 }); |
|
2772 } |
|
2773 }); |
|
2774 |
|
2775 /** |
|
2776 * Sort panels, sections, controls by priorities. Hide empty sections and panels. |
|
2777 * |
|
2778 * @since 4.1.0 |
|
2779 */ |
|
2780 api.reflowPaneContents = _.bind( function () { |
|
2781 |
|
2782 var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false; |
|
2783 |
|
2784 if ( document.activeElement ) { |
|
2785 activeElement = $( document.activeElement ); |
|
2786 } |
|
2787 |
|
2788 // Sort the sections within each panel |
|
2789 api.panel.each( function ( panel ) { |
|
2790 var sections = panel.sections(), |
|
2791 sectionContainers = _.pluck( sections, 'container' ); |
|
2792 rootNodes.push( panel ); |
|
2793 appendContainer = panel.container.find( 'ul:first' ); |
|
2794 if ( ! api.utils.areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) { |
|
2795 _( sections ).each( function ( section ) { |
|
2796 appendContainer.append( section.container ); |
|
2797 } ); |
|
2798 wasReflowed = true; |
|
2799 } |
|
2800 } ); |
|
2801 |
|
2802 // Sort the controls within each section |
|
2803 api.section.each( function ( section ) { |
|
2804 var controls = section.controls(), |
|
2805 controlContainers = _.pluck( controls, 'container' ); |
|
2806 if ( ! section.panel() ) { |
|
2807 rootNodes.push( section ); |
|
2808 } |
|
2809 appendContainer = section.container.find( 'ul:first' ); |
|
2810 if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) { |
|
2811 _( controls ).each( function ( control ) { |
|
2812 appendContainer.append( control.container ); |
|
2813 } ); |
|
2814 wasReflowed = true; |
|
2815 } |
|
2816 } ); |
|
2817 |
|
2818 // Sort the root panels and sections |
|
2819 rootNodes.sort( api.utils.prioritySort ); |
|
2820 rootContainers = _.pluck( rootNodes, 'container' ); |
|
2821 appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable |
|
2822 if ( ! api.utils.areElementListsEqual( rootContainers, appendContainer.children() ) ) { |
|
2823 _( rootNodes ).each( function ( rootNode ) { |
|
2824 appendContainer.append( rootNode.container ); |
|
2825 } ); |
|
2826 wasReflowed = true; |
|
2827 } |
|
2828 |
|
2829 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered |
|
2830 api.panel.each( function ( panel ) { |
|
2831 var value = panel.active(); |
|
2832 panel.active.callbacks.fireWith( panel.active, [ value, value ] ); |
|
2833 } ); |
|
2834 api.section.each( function ( section ) { |
|
2835 var value = section.active(); |
|
2836 section.active.callbacks.fireWith( section.active, [ value, value ] ); |
|
2837 } ); |
|
2838 |
|
2839 // Restore focus if there was a reflow and there was an active (focused) element |
|
2840 if ( wasReflowed && activeElement ) { |
|
2841 activeElement.focus(); |
|
2842 } |
|
2843 }, api ); |
|
2844 api.bind( 'ready', api.reflowPaneContents ); |
|
2845 api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 ); |
|
2846 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) { |
|
2847 values.bind( 'add', api.reflowPaneContents ); |
|
2848 values.bind( 'change', api.reflowPaneContents ); |
|
2849 values.bind( 'remove', api.reflowPaneContents ); |
|
2850 } ); |
|
2851 |
802 // Check if preview url is valid and load the preview frame. |
2852 // Check if preview url is valid and load the preview frame. |
803 if ( previewer.previewUrl() ) |
2853 if ( api.previewer.previewUrl() ) { |
804 previewer.refresh(); |
2854 api.previewer.refresh(); |
805 else |
2855 } else { |
806 previewer.previewUrl( api.settings.url.home ); |
2856 api.previewer.previewUrl( api.settings.url.home ); |
|
2857 } |
807 |
2858 |
808 // Save and activated states |
2859 // Save and activated states |
809 (function() { |
2860 (function() { |
810 var state = new api.Values(), |
2861 var state = new api.Values(), |
811 saved = state.create('saved'), |
2862 saved = state.create( 'saved' ), |
812 activated = state.create('activated'); |
2863 activated = state.create( 'activated' ), |
|
2864 processing = state.create( 'processing' ); |
813 |
2865 |
814 state.bind( 'change', function() { |
2866 state.bind( 'change', function() { |
815 var save = $('#save'), |
|
816 back = $('.back'); |
|
817 |
|
818 if ( ! activated() ) { |
2867 if ( ! activated() ) { |
819 save.val( api.l10n.activate ).prop( 'disabled', false ); |
2868 saveBtn.val( api.l10n.activate ).prop( 'disabled', false ); |
820 back.text( api.l10n.cancel ); |
2869 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); |
821 |
2870 |
822 } else if ( saved() ) { |
2871 } else if ( saved() ) { |
823 save.val( api.l10n.saved ).prop( 'disabled', true ); |
2872 saveBtn.val( api.l10n.saved ).prop( 'disabled', true ); |
824 back.text( api.l10n.close ); |
2873 closeBtn.find( '.screen-reader-text' ).text( api.l10n.close ); |
825 |
2874 |
826 } else { |
2875 } else { |
827 save.val( api.l10n.save ).prop( 'disabled', false ); |
2876 saveBtn.val( api.l10n.save ).prop( 'disabled', false ); |
828 back.text( api.l10n.cancel ); |
2877 closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); |
829 } |
2878 } |
830 }); |
2879 }); |
831 |
2880 |
832 // Set default states. |
2881 // Set default states. |
833 saved( true ); |
2882 saved( true ); |
834 activated( api.settings.theme.active ); |
2883 activated( api.settings.theme.active ); |
|
2884 processing( 0 ); |
835 |
2885 |
836 api.bind( 'change', function() { |
2886 api.bind( 'change', function() { |
837 state('saved').set( false ); |
2887 state('saved').set( false ); |
838 }); |
2888 }); |
839 |
2889 |