~canonical-sysadmins/wordpress/4.8.3

« back to all changes in this revision

Viewing changes to wp-admin/js/customize-controls.js

  • Committer: Ryan Finnie
  • Date: 2015-08-31 16:09:47 UTC
  • mfrom: (1.1.9 upstream)
  • Revision ID: ryan.finnie@canonical.com-20150831160947-1h6rfxby9z1ec62u
Merge WP4.3 from upstream

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
/* globals _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer */
 
1
/* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer */
2
2
(function( exports, $ ){
3
3
        var Container, focus, api = wp.customize;
4
4
 
68
68
                params = params || {};
69
69
                focus = function () {
70
70
                        var focusContainer;
71
 
                        if ( construct.extended( api.Panel ) && construct.expanded() ) {
72
 
                                focusContainer = construct.container.find( '.control-panel-content:first' );
 
71
                        if ( construct.expanded && construct.expanded() ) {
 
72
                                focusContainer = construct.container.find( 'ul:first' );
73
73
                        } else {
74
74
                                focusContainer = construct.container;
75
75
                        }
76
 
                        focusContainer.find( ':focusable:first' ).focus();
77
 
                        focusContainer[0].scrollIntoView( true );
 
76
 
 
77
                        // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
 
78
                        focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first().focus();
78
79
                };
79
80
                if ( params.completeCallback ) {
80
81
                        completeCallback = params.completeCallback;
156
157
        Container = api.Class.extend({
157
158
                defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
158
159
                defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
 
160
                containerType: 'container',
 
161
                defaults: {
 
162
                        title: '',
 
163
                        description: '',
 
164
                        priority: 100,
 
165
                        type: 'default',
 
166
                        content: null,
 
167
                        active: true,
 
168
                        instanceNumber: null
 
169
                },
159
170
 
160
171
                /**
161
172
                 * @since 4.1.0
162
173
                 *
163
 
                 * @param {String} id
164
 
                 * @param {Object} options
 
174
                 * @param {string}         id - The ID for the container.
 
175
                 * @param {object}         options - Object containing one property: params.
 
176
                 * @param {object}         options.params - Object containing the following properties.
 
177
                 * @param {string}         options.params.title - Title shown when panel is collapsed and expanded.
 
178
                 * @param {string=}        [options.params.description] - Description shown at the top of the panel.
 
179
                 * @param {number=100}     [options.params.priority] - The sort priority for the panel.
 
180
                 * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
 
181
                 * @param {string=}        [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
 
182
                 * @param {boolean=true}   [options.params.active] - Whether the panel is active or not.
165
183
                 */
166
184
                initialize: function ( id, options ) {
167
185
                        var container = this;
168
186
                        container.id = id;
169
 
                        container.params = {};
170
 
                        $.extend( container, options || {} );
 
187
                        options = options || {};
 
188
 
 
189
                        options.params = _.defaults(
 
190
                                options.params || {},
 
191
                                container.defaults
 
192
                        );
 
193
 
 
194
                        $.extend( container, options );
 
195
                        container.templateSelector = 'customize-' + container.containerType + '-' + container.params.type;
171
196
                        container.container = $( container.params.content );
 
197
                        if ( 0 === container.container.length ) {
 
198
                                container.container = $( container.getContainer() );
 
199
                        }
172
200
 
173
201
                        container.deferred = {
174
202
                                embedded: new $.Deferred()
191
219
                                container.onChangeExpanded( expanded, args );
192
220
                        });
193
221
 
194
 
                        container.attachEvents();
 
222
                        container.deferred.embedded.done( function () {
 
223
                                container.attachEvents();
 
224
                        });
195
225
 
196
226
                        api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
197
227
 
198
 
                        container.priority.set( isNaN( container.params.priority ) ? 100 : container.params.priority );
 
228
                        container.priority.set( container.params.priority );
199
229
                        container.active.set( container.params.active );
200
230
                        container.expanded.set( false );
201
231
                },
255
285
                 * @param {Object}  args.completeCallback
256
286
                 */
257
287
                onChangeActive: function ( active, args ) {
258
 
                        var duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
259
 
                        if ( ! $.contains( document, this.container ) ) {
 
288
                        var duration, construct = this;
 
289
                        duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
 
290
                        if ( ! $.contains( document, construct.container[0] ) ) {
260
291
                                // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
261
 
                                this.container.toggle( active );
 
292
                                construct.container.toggle( active );
262
293
                                if ( args.completeCallback ) {
263
294
                                        args.completeCallback();
264
295
                                }
265
296
                        } else if ( active ) {
266
 
                                this.container.stop( true, true ).slideDown( duration, args.completeCallback );
 
297
                                construct.container.stop( true, true ).slideDown( duration, args.completeCallback );
267
298
                        } else {
268
 
                                this.container.stop( true, true ).slideUp( duration, args.completeCallback );
 
299
                                if ( construct.expanded() ) {
 
300
                                        construct.collapse({
 
301
                                                duration: duration,
 
302
                                                completeCallback: function() {
 
303
                                                        construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
 
304
                                                }
 
305
                                        });
 
306
                                } else {
 
307
                                        construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
 
308
                                }
269
309
                        }
270
310
                },
271
311
 
366
406
                 * Bring the container into view and then expand this and bring it into view
367
407
                 * @param {Object} [params]
368
408
                 */
369
 
                focus: focus
 
409
                focus: focus,
 
410
 
 
411
                /**
 
412
                 * Return the container html, generated from its JS template, if it exists.
 
413
                 *
 
414
                 * @since 4.3.0
 
415
                 */
 
416
                getContainer: function () {
 
417
                        var template,
 
418
                                container = this;
 
419
 
 
420
                        if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) {
 
421
                                template = wp.template( container.templateSelector );
 
422
                        } else {
 
423
                                template = wp.template( 'customize-' + container.containerType + '-default' );
 
424
                        }
 
425
                        if ( template && container.container ) {
 
426
                                return $.trim( template( container.params ) );
 
427
                        }
 
428
 
 
429
                        return '<li></li>';
 
430
                }
370
431
        });
371
432
 
372
433
        /**
376
437
         * @augments wp.customize.Class
377
438
         */
378
439
        api.Section = Container.extend({
 
440
                containerType: 'section',
 
441
                defaults: {
 
442
                        title: '',
 
443
                        description: '',
 
444
                        priority: 100,
 
445
                        type: 'default',
 
446
                        content: null,
 
447
                        active: true,
 
448
                        instanceNumber: null,
 
449
                        panel: null,
 
450
                        customizeAction: ''
 
451
                },
379
452
 
380
453
                /**
381
454
                 * @since 4.1.0
382
455
                 *
383
 
                 * @param {String} id
384
 
                 * @param {Array}  options
 
456
                 * @param {string}         id - The ID for the section.
 
457
                 * @param {object}         options - Object containing one property: params.
 
458
                 * @param {object}         options.params - Object containing the following properties.
 
459
                 * @param {string}         options.params.title - Title shown when section is collapsed and expanded.
 
460
                 * @param {string=}        [options.params.description] - Description shown at the top of the section.
 
461
                 * @param {number=100}     [options.params.priority] - The sort priority for the section.
 
462
                 * @param {string=default} [options.params.type] - The type of the section. See wp.customize.sectionConstructor.
 
463
                 * @param {string=}        [options.params.content] - The markup to be used for the section container. If empty, a JS template is used.
 
464
                 * @param {boolean=true}   [options.params.active] - Whether the section is active or not.
 
465
                 * @param {string}         options.params.panel - The ID for the panel this section is associated with.
 
466
                 * @param {string=}        [options.params.customizeAction] - Additional context information shown before the section title when expanded.
385
467
                 */
386
468
                initialize: function ( id, options ) {
387
469
                        var section = this;
446
528
                        var section = this;
447
529
 
448
530
                        // Expand/Collapse accordion sections on click.
449
 
                        section.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
 
531
                        section.container.find( '.accordion-section-title, .customize-section-back' ).on( 'click keydown', function( event ) {
450
532
                                if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
451
533
                                        return;
452
534
                                }
500
582
                 */
501
583
                onChangeExpanded: function ( expanded, args ) {
502
584
                        var section = this,
 
585
                                container = section.container.closest( '.wp-full-overlay-sidebar-content' ),
503
586
                                content = section.container.find( '.accordion-section-content' ),
504
 
                                expand;
 
587
                                overlay = section.container.closest( '.wp-full-overlay' ),
 
588
                                backBtn = section.container.find( '.customize-section-back' ),
 
589
                                sectionTitle = section.container.find( '.accordion-section-title' ).first(),
 
590
                                headerActionsHeight = $( '#customize-header-actions' ).height(),
 
591
                                resizeContentHeight, expand, position, scroll;
505
592
 
506
 
                        if ( expanded ) {
 
593
                        if ( expanded && ! section.container.hasClass( 'open' ) ) {
507
594
 
508
595
                                if ( args.unchanged ) {
509
596
                                        expand = args.completeCallback;
510
597
                                } else {
511
 
                                        expand = function () {
512
 
                                                content.stop().slideDown( args.duration, args.completeCallback );
 
598
                                        container.scrollTop( 0 );
 
599
                                        resizeContentHeight = function() {
 
600
                                                var matchMedia, offset;
 
601
                                                matchMedia = window.matchMedia || window.msMatchMedia;
 
602
                                                offset = 90; // 45px for customize header actions + 45px for footer actions.
 
603
 
 
604
                                                // No footer on small screens.
 
605
                                                if ( matchMedia && matchMedia( '(max-width: 640px)' ).matches ) {
 
606
                                                        offset = 45;
 
607
                                                }
 
608
                                                content.css( 'height', ( window.innerHeight - offset ) );
 
609
                                        };
 
610
                                        expand = function() {
513
611
                                                section.container.addClass( 'open' );
 
612
                                                overlay.addClass( 'section-open' );
 
613
                                                position = content.offset().top;
 
614
                                                scroll = container.scrollTop();
 
615
                                                content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
 
616
                                                resizeContentHeight();
 
617
                                                sectionTitle.attr( 'tabindex', '-1' );
 
618
                                                backBtn.attr( 'tabindex', '0' );
 
619
                                                backBtn.focus();
 
620
                                                if ( args.completeCallback ) {
 
621
                                                        args.completeCallback();
 
622
                                                }
 
623
 
 
624
                                                // Fix the height after browser resize.
 
625
                                                $( window ).on( 'resize.customizer-section', _.debounce( resizeContentHeight, 100 ) );
 
626
 
 
627
                                                // Fix the top margin after reflow.
 
628
                                                api.bind( 'pane-contents-reflowed', _.debounce( function() {
 
629
                                                        var offset = ( content.offset().top - headerActionsHeight );
 
630
                                                        if ( 0 < offset ) {
 
631
                                                                content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - offset ) );
 
632
                                                        }
 
633
                                                }, 100 ) );
514
634
                                        };
515
635
                                }
516
636
 
531
651
                                        expand();
532
652
                                }
533
653
 
534
 
                        } else {
 
654
                        } else if ( ! expanded && section.container.hasClass( 'open' ) ) {
535
655
                                section.container.removeClass( 'open' );
536
 
                                content.slideUp( args.duration, args.completeCallback );
 
656
                                overlay.removeClass( 'section-open' );
 
657
                                content.css( 'margin-top', '' );
 
658
                                container.scrollTop( 0 );
 
659
                                backBtn.attr( 'tabindex', '-1' );
 
660
                                sectionTitle.attr( 'tabindex', '0' );
 
661
                                sectionTitle.focus();
 
662
                                if ( args.completeCallback ) {
 
663
                                        args.completeCallback();
 
664
                                }
 
665
                                $( window ).off( 'resize.customizer-section' );
 
666
                        } else {
 
667
                                if ( args.completeCallback ) {
 
668
                                        args.completeCallback();
 
669
                                }
537
670
                        }
538
671
                }
539
672
        });
718
851
                                overlay = section.closest( '.wp-full-overlay' ),
719
852
                                container = section.closest( '.wp-full-overlay-sidebar-content' ),
720
853
                                siblings = container.find( '.open' ),
721
 
                                topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
722
854
                                customizeBtn = section.find( '.customize-theme' ),
723
855
                                changeBtn = section.find( '.change-theme' ),
724
856
                                content = section.find( '.control-panel-content' );
748
880
                                                args.completeCallback();
749
881
                                        }
750
882
                                } );
751
 
                                topPanel.attr( 'tabindex', '-1' );
752
 
                                changeBtn.attr( 'tabindex', '-1' );
753
883
                                customizeBtn.focus();
754
884
                        } else {
755
885
                                siblings.removeClass( 'open' );
762
892
                                                args.completeCallback();
763
893
                                        }
764
894
                                } );
765
 
                                topPanel.attr( 'tabindex', '0' );
766
895
                                customizeBtn.attr( 'tabindex', '0' );
767
896
                                changeBtn.focus();
768
897
                                container.scrollTop( 0 );
964
1093
         * @augments wp.customize.Class
965
1094
         */
966
1095
        api.Panel = Container.extend({
 
1096
                containerType: 'panel',
 
1097
 
967
1098
                /**
968
1099
                 * @since 4.1.0
969
1100
                 *
970
 
                 * @param  {String} id
971
 
                 * @param  {Object} options
 
1101
                 * @param {string}         id - The ID for the panel.
 
1102
                 * @param {object}         options - Object containing one property: params.
 
1103
                 * @param {object}         options.params - Object containing the following properties.
 
1104
                 * @param {string}         options.params.title - Title shown when panel is collapsed and expanded.
 
1105
                 * @param {string=}        [options.params.description] - Description shown at the top of the panel.
 
1106
                 * @param {number=100}     [options.params.priority] - The sort priority for the panel.
 
1107
                 * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
 
1108
                 * @param {string=}        [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
 
1109
                 * @param {boolean=true}   [options.params.active] - Whether the panel is active or not.
972
1110
                 */
973
1111
                initialize: function ( id, options ) {
974
1112
                        var panel = this;
990
1128
 
991
1129
                        if ( ! panel.container.parent().is( parentContainer ) ) {
992
1130
                                parentContainer.append( panel.container );
 
1131
                                panel.renderContent();
993
1132
                        }
994
1133
                        panel.deferred.embedded.resolve();
995
1134
                },
1012
1151
                                }
1013
1152
                        });
1014
1153
 
 
1154
                        // Close panel.
 
1155
                        panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) {
 
1156
                                if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
 
1157
                                        return;
 
1158
                                }
 
1159
                                event.preventDefault(); // Keep this AFTER the key filter above
 
1160
 
 
1161
                                if ( panel.expanded() ) {
 
1162
                                        panel.collapse();
 
1163
                                }
 
1164
                        });
 
1165
 
1015
1166
                        meta = panel.container.find( '.panel-meta:first' );
1016
1167
 
1017
 
                        meta.find( '> .accordion-section-title' ).on( 'click keydown', function( event ) {
 
1168
                        meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
1018
1169
                                if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1019
1170
                                        return;
1020
1171
                                }
1021
1172
                                event.preventDefault(); // Keep this AFTER the key filter above
1022
1173
 
 
1174
                                meta = panel.container.find( '.panel-meta' );
1023
1175
                                if ( meta.hasClass( 'cannot-expand' ) ) {
1024
1176
                                        return;
1025
1177
                                }
1026
1178
 
1027
 
                                var content = meta.find( '.accordion-section-content:first' );
 
1179
                                var content = meta.find( '.customize-panel-description:first' );
1028
1180
                                if ( meta.hasClass( 'open' ) ) {
1029
1181
                                        meta.toggleClass( 'open' );
1030
1182
                                        content.slideUp( panel.defaultExpandedArguments.duration );
 
1183
                                        $( this ).attr( 'aria-expanded', false );
1031
1184
                                } else {
1032
1185
                                        content.slideDown( panel.defaultExpandedArguments.duration );
1033
1186
                                        meta.toggleClass( 'open' );
 
1187
                                        $( this ).attr( 'aria-expanded', true );
1034
1188
                                }
1035
1189
                        });
1036
1190
 
1089
1243
                        // Note: there is a second argument 'args' passed
1090
1244
                        var position, scroll,
1091
1245
                                panel = this,
1092
 
                                section = panel.container.closest( '.accordion-section' ),
 
1246
                                section = panel.container.closest( '.accordion-section' ), // This is actually the panel.
1093
1247
                                overlay = section.closest( '.wp-full-overlay' ),
1094
1248
                                container = section.closest( '.wp-full-overlay-sidebar-content' ),
1095
1249
                                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' ),
 
1250
                                topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ),
 
1251
                                backBtn = section.find( '.customize-panel-back' ),
1098
1252
                                panelTitle = section.find( '.accordion-section-title' ).first(),
1099
 
                                content = section.find( '.control-panel-content' );
 
1253
                                content = section.find( '.control-panel-content' ),
 
1254
                                headerActionsHeight = $( '#customize-header-actions' ).height();
1100
1255
 
1101
1256
                        if ( expanded ) {
1102
1257
 
1116
1271
                                        content.parent().show();
1117
1272
                                        position = content.offset().top;
1118
1273
                                        scroll = container.scrollTop();
1119
 
                                        content.css( 'margin-top', ( $( '#customize-header-actions' ).height() - position - scroll ) );
 
1274
                                        content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
1120
1275
                                        section.addClass( 'current-panel' );
1121
1276
                                        overlay.addClass( 'in-sub-panel' );
1122
1277
                                        container.scrollTop( 0 );
1127
1282
                                topPanel.attr( 'tabindex', '-1' );
1128
1283
                                backBtn.attr( 'tabindex', '0' );
1129
1284
                                backBtn.focus();
 
1285
 
 
1286
                                // Fix the top margin after reflow.
 
1287
                                api.bind( 'pane-contents-reflowed', _.debounce( function() {
 
1288
                                        content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - ( content.offset().top - headerActionsHeight ) ) );
 
1289
                                }, 100 ) );
1130
1290
                        } else {
1131
1291
                                siblings.removeClass( 'open' );
1132
1292
                                section.removeClass( 'current-panel' );
1142
1302
                                panelTitle.focus();
1143
1303
                                container.scrollTop( 0 );
1144
1304
                        }
 
1305
                },
 
1306
 
 
1307
                /**
 
1308
                 * Render the panel from its JS template, if it exists.
 
1309
                 *
 
1310
                 * The panel's container must already exist in the DOM.
 
1311
                 *
 
1312
                 * @since 4.3.0
 
1313
                 */
 
1314
                renderContent: function () {
 
1315
                        var template,
 
1316
                                panel = this;
 
1317
 
 
1318
                        // Add the content to the container.
 
1319
                        if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) {
 
1320
                                template = wp.template( panel.templateSelector + '-content' );
 
1321
                        } else {
 
1322
                                template = wp.template( 'customize-panel-default-content' );
 
1323
                        }
 
1324
                        if ( template && panel.container ) {
 
1325
                                panel.container.find( '.accordion-sub-container' ).html( template( panel.params ) );
 
1326
                        }
1145
1327
                }
1146
1328
        });
1147
1329
 
1685
1867
        });
1686
1868
 
1687
1869
        /**
 
1870
         * A control for selecting and cropping an image.
 
1871
         *
 
1872
         * @class
 
1873
         * @augments wp.customize.MediaControl
 
1874
         * @augments wp.customize.Control
 
1875
         * @augments wp.customize.Class
 
1876
         */
 
1877
        api.CroppedImageControl = api.MediaControl.extend({
 
1878
 
 
1879
                /**
 
1880
                 * Open the media modal to the library state.
 
1881
                 */
 
1882
                openFrame: function( event ) {
 
1883
                        if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
 
1884
                                return;
 
1885
                        }
 
1886
 
 
1887
                        this.initFrame();
 
1888
                        this.frame.setState( 'library' ).open();
 
1889
                },
 
1890
 
 
1891
                /**
 
1892
                 * Create a media modal select frame, and store it so the instance can be reused when needed.
 
1893
                 */
 
1894
                initFrame: function() {
 
1895
                        var l10n = _wpMediaViewsL10n;
 
1896
 
 
1897
                        this.frame = wp.media({
 
1898
                                button: {
 
1899
                                        text: l10n.select,
 
1900
                                        close: false
 
1901
                                },
 
1902
                                states: [
 
1903
                                        new wp.media.controller.Library({
 
1904
                                                title: this.params.button_labels.frame_title,
 
1905
                                                library: wp.media.query({ type: 'image' }),
 
1906
                                                multiple: false,
 
1907
                                                date: false,
 
1908
                                                priority: 20,
 
1909
                                                suggestedWidth: this.params.width,
 
1910
                                                suggestedHeight: this.params.height
 
1911
                                        }),
 
1912
                                        new wp.media.controller.CustomizeImageCropper({
 
1913
                                                imgSelectOptions: this.calculateImageSelectOptions,
 
1914
                                                control: this
 
1915
                                        })
 
1916
                                ]
 
1917
                        });
 
1918
 
 
1919
                        this.frame.on( 'select', this.onSelect, this );
 
1920
                        this.frame.on( 'cropped', this.onCropped, this );
 
1921
                        this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
 
1922
                },
 
1923
 
 
1924
                /**
 
1925
                 * After an image is selected in the media modal, switch to the cropper
 
1926
                 * state if the image isn't the right size.
 
1927
                 */
 
1928
                onSelect: function() {
 
1929
                        var attachment = this.frame.state().get( 'selection' ).first().toJSON();
 
1930
 
 
1931
                        if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
 
1932
                                this.setImageFromAttachment( attachment );
 
1933
                                this.frame.close();
 
1934
                        } else {
 
1935
                                this.frame.setState( 'cropper' );
 
1936
                        }
 
1937
                },
 
1938
 
 
1939
                /**
 
1940
                 * After the image has been cropped, apply the cropped image data to the setting.
 
1941
                 *
 
1942
                 * @param {object} croppedImage Cropped attachment data.
 
1943
                 */
 
1944
                onCropped: function( croppedImage ) {
 
1945
                        this.setImageFromAttachment( croppedImage );
 
1946
                },
 
1947
 
 
1948
                /**
 
1949
                 * Returns a set of options, computed from the attached image data and
 
1950
                 * control-specific data, to be fed to the imgAreaSelect plugin in
 
1951
                 * wp.media.view.Cropper.
 
1952
                 *
 
1953
                 * @param {wp.media.model.Attachment} attachment
 
1954
                 * @param {wp.media.controller.Cropper} controller
 
1955
                 * @returns {Object} Options
 
1956
                 */
 
1957
                calculateImageSelectOptions: function( attachment, controller ) {
 
1958
                        var control    = controller.get( 'control' ),
 
1959
                                flexWidth  = !! parseInt( control.params.flex_width, 10 ),
 
1960
                                flexHeight = !! parseInt( control.params.flex_height, 10 ),
 
1961
                                realWidth  = attachment.get( 'width' ),
 
1962
                                realHeight = attachment.get( 'height' ),
 
1963
                                xInit = parseInt( control.params.width, 10 ),
 
1964
                                yInit = parseInt( control.params.height, 10 ),
 
1965
                                ratio = xInit / yInit,
 
1966
                                xImg  = realWidth,
 
1967
                                yImg  = realHeight,
 
1968
                                x1, y1, imgSelectOptions;
 
1969
 
 
1970
                        controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
 
1971
 
 
1972
                        if ( xImg / yImg > ratio ) {
 
1973
                                yInit = yImg;
 
1974
                                xInit = yInit * ratio;
 
1975
                        } else {
 
1976
                                xInit = xImg;
 
1977
                                yInit = xInit / ratio;
 
1978
                        }
 
1979
 
 
1980
                        x1 = ( xImg - xInit ) / 2;
 
1981
                        y1 = ( yImg - yInit ) / 2;
 
1982
 
 
1983
                        imgSelectOptions = {
 
1984
                                handles: true,
 
1985
                                keys: true,
 
1986
                                instance: true,
 
1987
                                persistent: true,
 
1988
                                imageWidth: realWidth,
 
1989
                                imageHeight: realHeight,
 
1990
                                x1: x1,
 
1991
                                y1: y1,
 
1992
                                x2: xInit + x1,
 
1993
                                y2: yInit + y1
 
1994
                        };
 
1995
 
 
1996
                        if ( flexHeight === false && flexWidth === false ) {
 
1997
                                imgSelectOptions.aspectRatio = xInit + ':' + yInit;
 
1998
                        }
 
1999
                        if ( flexHeight === false ) {
 
2000
                                imgSelectOptions.maxHeight = yInit;
 
2001
                        }
 
2002
                        if ( flexWidth === false ) {
 
2003
                                imgSelectOptions.maxWidth = xInit;
 
2004
                        }
 
2005
 
 
2006
                        return imgSelectOptions;
 
2007
                },
 
2008
 
 
2009
                /**
 
2010
                 * Return whether the image must be cropped, based on required dimensions.
 
2011
                 *
 
2012
                 * @param {bool} flexW
 
2013
                 * @param {bool} flexH
 
2014
                 * @param {int}  dstW
 
2015
                 * @param {int}  dstH
 
2016
                 * @param {int}  imgW
 
2017
                 * @param {int}  imgH
 
2018
                 * @return {bool}
 
2019
                 */
 
2020
                mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
 
2021
                        if ( true === flexW && true === flexH ) {
 
2022
                                return false;
 
2023
                        }
 
2024
 
 
2025
                        if ( true === flexW && dstH === imgH ) {
 
2026
                                return false;
 
2027
                        }
 
2028
 
 
2029
                        if ( true === flexH && dstW === imgW ) {
 
2030
                                return false;
 
2031
                        }
 
2032
 
 
2033
                        if ( dstW === imgW && dstH === imgH ) {
 
2034
                                return false;
 
2035
                        }
 
2036
 
 
2037
                        if ( imgW <= dstW ) {
 
2038
                                return false;
 
2039
                        }
 
2040
 
 
2041
                        return true;
 
2042
                },
 
2043
 
 
2044
                /**
 
2045
                 * If cropping was skipped, apply the image data directly to the setting.
 
2046
                 */
 
2047
                onSkippedCrop: function() {
 
2048
                        var attachment = this.frame.state().get( 'selection' ).first().toJSON();
 
2049
                        this.setImageFromAttachment( attachment );
 
2050
                },
 
2051
 
 
2052
                /**
 
2053
                 * Updates the setting and re-renders the control UI.
 
2054
                 *
 
2055
                 * @param {object} attachment
 
2056
                 */
 
2057
                setImageFromAttachment: function( attachment ) {
 
2058
                        this.params.attachment = attachment;
 
2059
 
 
2060
                        // Set the Customizer setting; the callback takes care of rendering.
 
2061
                        this.setting( attachment.id );
 
2062
                }
 
2063
        });
 
2064
 
 
2065
        /**
 
2066
         * A control for selecting and cropping Site Icons.
 
2067
         *
 
2068
         * @class
 
2069
         * @augments wp.customize.CroppedImageControl
 
2070
         * @augments wp.customize.MediaControl
 
2071
         * @augments wp.customize.Control
 
2072
         * @augments wp.customize.Class
 
2073
         */
 
2074
        api.SiteIconControl = api.CroppedImageControl.extend({
 
2075
 
 
2076
                /**
 
2077
                 * Create a media modal select frame, and store it so the instance can be reused when needed.
 
2078
                 */
 
2079
                initFrame: function() {
 
2080
                        var l10n = _wpMediaViewsL10n;
 
2081
 
 
2082
                        this.frame = wp.media({
 
2083
                                button: {
 
2084
                                        text: l10n.select,
 
2085
                                        close: false
 
2086
                                },
 
2087
                                states: [
 
2088
                                        new wp.media.controller.Library({
 
2089
                                                title: this.params.button_labels.frame_title,
 
2090
                                                library: wp.media.query({ type: 'image' }),
 
2091
                                                multiple: false,
 
2092
                                                date: false,
 
2093
                                                priority: 20,
 
2094
                                                suggestedWidth: this.params.width,
 
2095
                                                suggestedHeight: this.params.height
 
2096
                                        }),
 
2097
                                        new wp.media.controller.SiteIconCropper({
 
2098
                                                imgSelectOptions: this.calculateImageSelectOptions,
 
2099
                                                control: this
 
2100
                                        })
 
2101
                                ]
 
2102
                        });
 
2103
 
 
2104
                        this.frame.on( 'select', this.onSelect, this );
 
2105
                        this.frame.on( 'cropped', this.onCropped, this );
 
2106
                        this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
 
2107
                },
 
2108
 
 
2109
                /**
 
2110
                 * After an image is selected in the media modal, switch to the cropper
 
2111
                 * state if the image isn't the right size.
 
2112
                 */
 
2113
                onSelect: function() {
 
2114
                        var attachment = this.frame.state().get( 'selection' ).first().toJSON(),
 
2115
                                controller = this;
 
2116
 
 
2117
                        if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
 
2118
                                wp.ajax.post( 'crop-image', {
 
2119
                                        nonce: attachment.nonces.edit,
 
2120
                                        id: attachment.id,
 
2121
                                        context: 'site-icon',
 
2122
                                        cropDetails: {
 
2123
                                                x1: 0,
 
2124
                                                y1: 0,
 
2125
                                                width: this.params.width,
 
2126
                                                height: this.params.height,
 
2127
                                                dst_width: this.params.width,
 
2128
                                                dst_height: this.params.height
 
2129
                                        }
 
2130
                                } ).done( function( croppedImage ) {
 
2131
                                        controller.setImageFromAttachment( croppedImage );
 
2132
                                        controller.frame.close();
 
2133
                                } ).fail( function() {
 
2134
                                        controller.trigger('content:error:crop');
 
2135
                                } );
 
2136
                        } else {
 
2137
                                this.frame.setState( 'cropper' );
 
2138
                        }
 
2139
                },
 
2140
 
 
2141
                /**
 
2142
                 * Updates the setting and re-renders the control UI.
 
2143
                 *
 
2144
                 * @param {object} attachment
 
2145
                 */
 
2146
                setImageFromAttachment: function( attachment ) {
 
2147
                        var icon = typeof attachment.sizes['site_icon-32'] !== 'undefined' ? attachment.sizes['site_icon-32'] : attachment.sizes.thumbnail;
 
2148
 
 
2149
                        this.params.attachment = attachment;
 
2150
 
 
2151
                        // Set the Customizer setting; the callback takes care of rendering.
 
2152
                        this.setting( attachment.id );
 
2153
 
 
2154
 
 
2155
                        // Update the icon in-browser.
 
2156
                        $( 'link[sizes="32x32"]' ).attr( 'href', icon.url );
 
2157
                },
 
2158
 
 
2159
                /**
 
2160
                 * Called when the "Remove" link is clicked. Empties the setting.
 
2161
                 *
 
2162
                 * @param {object} event jQuery Event object
 
2163
                 */
 
2164
                removeFile: function( event ) {
 
2165
                        if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
 
2166
                                return;
 
2167
                        }
 
2168
                        event.preventDefault();
 
2169
 
 
2170
                        this.params.attachment = {};
 
2171
                        this.setting( '' );
 
2172
                        this.renderContent(); // Not bound to setting change when emptying.
 
2173
                        $( 'link[rel="icon"]' ).attr( 'href', '' );
 
2174
                }
 
2175
        });
 
2176
 
 
2177
        /**
1688
2178
         * @class
1689
2179
         * @augments wp.customize.Control
1690
2180
         * @augments wp.customize.Class
2134
2624
                                _( constructs ).each( function ( activeConstructs, type ) {
2135
2625
                                        api[ type ].each( function ( construct, id ) {
2136
2626
                                                var active = !! ( activeConstructs && activeConstructs[ id ] );
2137
 
                                                construct.active( active );
 
2627
                                                if ( active ) {
 
2628
                                                        construct.activate();
 
2629
                                                } else {
 
2630
                                                        construct.deactivate();
 
2631
                                                }
2138
2632
                                        } );
2139
2633
                                } );
2140
2634
                        } );
2544
3038
        });
2545
3039
 
2546
3040
        api.controlConstructor = {
2547
 
                color:      api.ColorControl,
2548
 
                media:      api.MediaControl,
2549
 
                upload:     api.UploadControl,
2550
 
                image:      api.ImageControl,
2551
 
                header:     api.HeaderControl,
2552
 
                background: api.BackgroundControl,
2553
 
                theme:      api.ThemeControl
 
3041
                color:         api.ColorControl,
 
3042
                media:         api.MediaControl,
 
3043
                upload:        api.UploadControl,
 
3044
                image:         api.ImageControl,
 
3045
                cropped_image: api.CroppedImageControl,
 
3046
                site_icon:     api.SiteIconControl,
 
3047
                header:        api.HeaderControl,
 
3048
                background:    api.BackgroundControl,
 
3049
                theme:         api.ThemeControl
2554
3050
        };
2555
3051
        api.panelConstructor = {};
2556
3052
        api.sectionConstructor = {
2566
3062
                        return;
2567
3063
                }
2568
3064
 
2569
 
                // Redirect to the fallback preview if any incompatibilities are found.
2570
 
                if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) )
2571
 
                        return window.location = api.settings.url.fallback;
 
3065
                // Bail if any incompatibilities are found.
 
3066
                if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) {
 
3067
                        return;
 
3068
                }
2572
3069
 
2573
3070
                var parent, topFocus,
2574
3071
                        body = $( document.body ),
2575
3072
                        overlay = body.children( '.wp-full-overlay' ),
2576
 
                        title = $( '#customize-info .theme-name.site-title' ),
 
3073
                        title = $( '#customize-info .panel-title.site-title' ),
2577
3074
                        closeBtn = $( '.customize-controls-close' ),
2578
3075
                        saveBtn = $( '#save' );
2579
3076
 
2588
3085
                });
2589
3086
 
2590
3087
                // Expand/Collapse the main customizer customize info.
2591
 
                $( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( event ) {
 
3088
                $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
2592
3089
                        if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2593
3090
                                return;
2594
3091
                        }
2595
3092
                        event.preventDefault(); // Keep this AFTER the key filter above
2596
3093
 
2597
 
                        var section = $( this ).parent(),
2598
 
                                content = section.find( '.accordion-section-content:first' );
 
3094
                        var section = $( this ).closest( '.accordion-section' ),
 
3095
                                content = section.find( '.customize-panel-description:first' );
2599
3096
 
2600
3097
                        if ( section.hasClass( 'cannot-expand' ) ) {
2601
3098
                                return;
2604
3101
                        if ( section.hasClass( 'open' ) ) {
2605
3102
                                section.toggleClass( 'open' );
2606
3103
                                content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration );
 
3104
                                $( this ).attr( 'aria-expanded', false );
2607
3105
                        } else {
2608
3106
                                content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration );
2609
3107
                                section.toggleClass( 'open' );
 
3108
                                $( this ).attr( 'aria-expanded', true );
2610
3109
                        }
2611
3110
                });
2612
3111
 
2840
3339
                        if ( wasReflowed && activeElement ) {
2841
3340
                                activeElement.focus();
2842
3341
                        }
 
3342
                        api.trigger( 'pane-contents-reflowed' );
2843
3343
                }, api );
2844
3344
                api.bind( 'ready', api.reflowPaneContents );
2845
3345
                api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
2913
3413
                        event.preventDefault();
2914
3414
                });
2915
3415
 
2916
 
                // Go back to the top-level Customizer accordion.
2917
 
                $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( event ) {
2918
 
                        if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2919
 
                                return;
2920
 
                        }
2921
 
 
2922
 
                        event.preventDefault(); // Keep this AFTER the key filter above
2923
 
                        api.panel.each( function ( panel ) {
2924
 
                                panel.collapse();
2925
 
                        });
2926
 
                });
2927
 
 
2928
3416
                closeBtn.keydown( function( event ) {
2929
3417
                        if ( 9 === event.which ) // tab
2930
3418
                                return;
2933
3421
                        event.preventDefault();
2934
3422
                });
2935
3423
 
2936
 
                $('.collapse-sidebar').on( 'click keydown', function( event ) {
2937
 
                        if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2938
 
                                return;
 
3424
                $( '.collapse-sidebar' ).on( 'click', function() {
 
3425
                        if ( 'true' === $( this ).attr( 'aria-expanded' ) ) {
 
3426
                                $( this ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar });
 
3427
                        } else {
 
3428
                                $( this ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar });
2939
3429
                        }
2940
3430
 
2941
3431
                        overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
2942
 
                        event.preventDefault();
2943
3432
                });
2944
3433
 
2945
3434
                $( '.customize-controls-preview-toggle' ).on( 'click keydown', function( event ) {
3057
3546
                        });
3058
3547
                });
3059
3548
 
 
3549
                // Change previewed URL to the homepage when changing the page_on_front.
 
3550
                api( 'show_on_front', 'page_on_front', function( showOnFront, pageOnFront ) {
 
3551
                        var updatePreviewUrl = function() {
 
3552
                                if ( showOnFront() === 'page' && parseInt( pageOnFront(), 10 ) > 0 ) {
 
3553
                                        api.previewer.previewUrl.set( api.settings.url.home );
 
3554
                                }
 
3555
                        };
 
3556
                        showOnFront.bind( updatePreviewUrl );
 
3557
                        pageOnFront.bind( updatePreviewUrl );
 
3558
                });
 
3559
 
 
3560
                // Change the previewed URL to the selected page when changing the page_for_posts.
 
3561
                api( 'page_for_posts', function( setting ) {
 
3562
                        setting.bind(function( pageId ) {
 
3563
                                pageId = parseInt( pageId, 10 );
 
3564
                                if ( pageId > 0 ) {
 
3565
                                        api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageId );
 
3566
                                }
 
3567
                        });
 
3568
                });
 
3569
 
3060
3570
                api.trigger( 'ready' );
3061
3571
 
3062
3572
                // Make sure left column gets focus