Make WordPress Core

Ticket #21785: 21785-customize-header.2.diff

File 21785-customize-header.2.diff, 48.2 KB (added by ehg, 11 years ago)
  • src/wp-admin/css/customize-controls.css

    diff --git src/wp-admin/css/customize-controls.css src/wp-admin/css/customize-controls.css
    index c76a6e5..482f002 100644
    body { 
    455455        -webkit-overflow-scrolling: touch;
    456456}
    457457
     458/** Header control **/
     459
     460#customize-control-header_image .current {
     461        margin-bottom: 8px;
     462}
     463
     464#customize-control-header_image .uploaded {
     465        margin-bottom: 18px;
     466}
     467
     468/* Header control: current image container */
     469
     470#customize-control-header_image .current .container {
     471        overflow: hidden;
     472        border-radius: 2px;
     473}
     474
     475#customize-control-header_image .placeholder {
     476        width: 100%;
     477        position: relative;
     478        background: #262626;
     479        text-align: center;
     480        cursor: default;
     481}
     482
     483#customize-control-header_image .inner {
     484        display: none;
     485        position: absolute;
     486        width: 100%;
     487        height: 18px;
     488        margin-top: -9px;
     489        top: 50%;
     490        color: #eee;
     491}
     492
     493/* Header control: overlay "close" button */
     494
     495#customize-control-header_image .header-view {
     496        position: relative;
     497}
     498
     499#customize-control-header_image .uploaded .header-view .close {
     500        font-size: 2em;
     501        color: grey;
     502        position: absolute;
     503        visibility: hidden;
     504        top: 10px;
     505        right: 10px;
     506        z-index: 1;
     507        width: 20px;
     508        height: 20px;
     509        cursor: pointer;
     510}
     511
     512#customize-control-header_image .uploaded .header-view .close:hover {
     513 color: black;
     514 text-shadow:
     515    -1px -1px 0 #fff,
     516    1px -1px 0 #fff,
     517    -1px 1px 0 #fff,
     518    1px 1px 0 #fff;
     519}
     520
     521#customize-control-header_image .header-view:hover .close {
     522        visibility: visible;
     523}
     524
     525/* Header control: randomiz(s)er */
     526
     527#customize-control-header_image .random.placeholder {
     528        cursor: pointer;
     529        border-radius: 2px;
     530        height: 40px;
     531}
     532
     533#customize-control-header_image .random .inner {
     534        display: block;
     535}
     536
     537#customize-control-header_image .dice {
     538        font-size: 16px;
     539        vertical-align: -1px;
     540}
     541
     542#customize-control-header_image .placeholder:hover .dice {
     543        -webkit-animation: dice-color-change 3s infinite;
     544        -moz-animation: dice-color-change 3s infinite;
     545        -ms-animation: dice-color-change 3s infinite;
     546        animation: dice-color-change 3s infinite;
     547}
     548
     549@-webkit-keyframes dice-color-change {
     550        0% { color: #d4b146; }
     551        50% { color: #ef54b0; }
     552        75% { color: #7190d3; }
     553        100% { color: #d4b146; }
     554}
     555
     556@-moz-keyframes dice-color-change {
     557        0% { color: #d4b146; }
     558        50% { color: #ef54b0; }
     559        75% { color: #7190d3; }
     560        100% { color: #d4b146; }
     561}
     562
     563@-ms-keyframes dice-color-change {
     564        0% { color: #d4b146; }
     565        50% { color: #ef54b0; }
     566        75% { color: #7190d3; }
     567        100% { color: #d4b146; }
     568}
     569
     570@keyframes dice-color-change {
     571        0% { color: #d4b146; }
     572        50% { color: #ef54b0; }
     573        75% { color: #7190d3; }
     574        100% { color: #d4b146; }
     575}
     576
     577/* Header control: actions and choices */
     578
     579#customize-control-header_image .actions {
     580        margin-bottom: 32px;
     581}
     582
     583#customize-control-header_image .choice {
     584        position: relative;
     585        display: block;
     586        margin-bottom: 9px;
     587}
     588
     589#customize-control-header_image .choice.random:before {
     590        position: absolute;
     591        content: attr(data-label);
     592        left: 0;
     593        top: 0;
     594}
     595
     596#customize-control-header_image .uploaded div:last-child > .choice {
     597        margin-bottom: 0;
     598}
     599
     600#customize-control-header_image .choices hr {
     601        visibility: hidden;
     602}
     603
     604#customize-control-header_image img {
     605        width: 100%;
     606        border-radius: 2px;
     607}
     608
     609#customize-control-header_image .remove {
     610        float: left;
     611        margin-right: 3px;
     612}
     613
     614#customize-control-header_image .new {
     615        float: right;
     616}
     617
     618
    458619/** Handle cheaters. */
    459620body.cheatin {
    460621        min-width: 0;
  • src/wp-admin/custom-header.php

    diff --git src/wp-admin/custom-header.php src/wp-admin/custom-header.php
    index 633dc65..a48caad 100644
    class Custom_Image_Header { 
    4343        var $default_headers = array();
    4444
    4545        /**
    46          * Holds custom headers uploaded by the user
     46         * Holds custom headers uploaded by the user.
    4747         *
    4848         * @var array
    4949         * @since 3.2.0
    class Custom_Image_Header { 
    7272                $this->admin_header_callback = $admin_header_callback;
    7373                $this->admin_image_div_callback = $admin_image_div_callback;
    7474
     75                if ( current_theme_supports( 'custom-header' ) ) {
     76                        add_action( 'customize_save_after', array( $this, 'set_last_used' ) );
     77                        add_action( 'wp_ajax_header_crop', array( $this, 'ajax_header_crop' ) );
     78                        add_action( 'wp_ajax_header_add', array( $this, 'ajax_header_add' ) );
     79                        add_action( 'wp_ajax_header_remove', array( $this, 'ajax_header_remove' ) );
     80                }
     81
    7582                add_action( 'admin_menu', array( $this, 'init' ) );
    7683        }
    7784
    class Custom_Image_Header { 
    93100                add_action("admin_head-$page", array($this, 'js'), 50);
    94101                if ( $this->admin_header_callback )
    95102                        add_action("admin_head-$page", $this->admin_header_callback, 51);
     103
    96104        }
    97105
    98106        /**
    wp_nonce_field( 'custom-header-options', '_wpnonce-custom-header-options' ); ?> 
    819827                $attachment_id = absint( $_POST['attachment_id'] );
    820828                $original = get_attached_file($attachment_id);
    821829
    822 
    823                 $max_width = 0;
    824                 // For flex, limit size of image displayed to 1500px unless theme says otherwise
    825                 if ( current_theme_supports( 'custom-header', 'flex-width' ) )
    826                         $max_width = 1500;
    827 
    828                 if ( current_theme_supports( 'custom-header', 'max-width' ) )
    829                         $max_width = max( $max_width, get_theme_support( 'custom-header', 'max-width' ) );
    830                 $max_width = max( $max_width, get_theme_support( 'custom-header', 'width' ) );
    831 
    832                 if ( ( current_theme_supports( 'custom-header', 'flex-height' ) && ! current_theme_supports( 'custom-header', 'flex-width' ) ) || $_POST['width'] > $max_width )
    833                         $dst_height = absint( $_POST['height'] * ( $max_width / $_POST['width'] ) );
    834                 elseif ( current_theme_supports( 'custom-header', 'flex-height' ) && current_theme_supports( 'custom-header', 'flex-width' ) )
    835                         $dst_height = absint( $_POST['height'] );
    836                 else
    837                         $dst_height = get_theme_support( 'custom-header', 'height' );
    838 
    839                 if ( ( current_theme_supports( 'custom-header', 'flex-width' ) && ! current_theme_supports( 'custom-header', 'flex-height' ) ) || $_POST['width'] > $max_width )
    840                         $dst_width = absint( $_POST['width'] * ( $max_width / $_POST['width'] ) );
    841                 elseif ( current_theme_supports( 'custom-header', 'flex-width' ) && current_theme_supports( 'custom-header', 'flex-height' ) )
    842                         $dst_width = absint( $_POST['width'] );
    843                 else
    844                         $dst_width = get_theme_support( 'custom-header', 'width' );
     830                extract( $this->get_header_dimensions( array(
     831                        'width' => $_POST['width'],
     832                        'height' => $_POST['height'],
     833                ) ) );
    845834
    846835                if ( empty( $_POST['skip-cropping'] ) )
    847836                        $cropped = wp_crop_image( $attachment_id, (int) $_POST['x1'], (int) $_POST['y1'], (int) $_POST['width'], (int) $_POST['height'], $dst_width, $dst_height );
    wp_nonce_field( 'custom-header-options', '_wpnonce-custom-header-options' ); ?> 
    856845                /** This filter is documented in wp-admin/custom-header.php */
    857846                $cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication
    858847
    859                 $parent = get_post($attachment_id);
    860                 $parent_url = $parent->guid;
    861                 $url = str_replace( basename( $parent_url ), basename( $cropped ), $parent_url );
    862 
    863                 $size = @getimagesize( $cropped );
    864                 $image_type = ( $size ) ? $size['mime'] : 'image/jpeg';
     848                $object = $this->create_attachment_object( $cropped, $attachment_id );
    865849
    866                 // Construct the object array
    867                 $object = array(
    868                         'ID' => $attachment_id,
    869                         'post_title' => basename($cropped),
    870                         'post_content' => $url,
    871                         'post_mime_type' => $image_type,
    872                         'guid' => $url,
    873                         'context' => 'custom-header'
    874                 );
    875850                if ( ! empty( $_POST['create-new-attachment'] ) )
    876851                        unset( $object['ID'] );
    877852
    878853                // Update the attachment
    879                 $attachment_id = wp_insert_attachment( $object, $cropped );
    880                 wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $cropped ) );
     854                $attachment_id = $this->insert_attachment( $object, $cropped );
    881855
    882856                $width = $dst_width;
    883857                $height = $dst_height;
     858                $url = $object['guid'];
    884859                $this->set_header_image( compact( 'url', 'attachment_id', 'width', 'height' ) );
    885860
    886861                // cleanup
    wp_nonce_field( 'custom-header-options', '_wpnonce-custom-header-options' ); ?> 
    10411016                set_theme_mod( 'header_image', $default );
    10421017                set_theme_mod( 'header_image_data', (object) $default_data );
    10431018        }
     1019
     1020        /**
     1021         * Calculate dst_width and dst_height based on what the currently selected theme supports.
     1022         *
     1023         * @return array dst_height and dst_width of header image.
     1024         */
     1025        final public function get_header_dimensions( $dimensions ) {
     1026                $max_width = 0;
     1027                $width = absint( $dimensions['width'] );
     1028                $height = absint( $dimensions['height'] );
     1029                $theme_height = get_theme_support( 'custom-header', 'height' );
     1030                $theme_width = get_theme_support( 'custom-header', 'width' );
     1031                $has_flex_width = current_theme_supports( 'custom-header', 'flex-width' );
     1032                $has_flex_height = current_theme_supports( 'custom-header', 'flex-height' );
     1033                $has_max_width = current_theme_supports( 'custom-header', 'max-width' ) ;
     1034                $dst = array();
     1035
     1036                // For flex, limit size of image displayed to 1500px unless theme says otherwise
     1037                if ( $has_flex_width )
     1038                        $max_width = 1500;
     1039
     1040                if ( $has_max_width )
     1041                        $max_width = max( $max_width, get_theme_support( 'custom-header', 'max-width' ) );
     1042                $max_width = max( $max_width, $theme_width );
     1043
     1044                if ( $has_flex_height && ( ! $has_flex_width || $width > $max_width ) )
     1045                        $dst['dst_height'] = absint( $height * ( $max_width / $width ) );
     1046                elseif ( $has_flex_height && $has_flex_width )
     1047                        $dst['dst_height'] = $height;
     1048                else
     1049                        $dst['dst_height'] = $theme_height;
     1050
     1051                if ( $has_flex_width && ( ! $has_flex_height || $width > $max_width ) )
     1052                        $dst['dst_width'] = absint( $width * ( $max_width / $width ) );
     1053                elseif ( $has_flex_width && $has_flex_height )
     1054                        $dst['dst_width'] = $width;
     1055                else
     1056                        $dst['dst_width'] = $theme_width;
     1057
     1058                return $dst;
     1059        }
     1060
     1061        /**
     1062         * Create an attachment 'object'.
     1063         *
     1064         * @param string $cropped Cropped image URL.
     1065         * @param int $parent_attachment_id Attachment ID of parent image.
     1066         *
     1067         * @return array Attachment object.
     1068         */
     1069        final public function create_attachment_object( $cropped, $parent_attachment_id ) {
     1070                $parent = get_post( $parent_attachment_id );
     1071                $parent_url = $parent->guid;
     1072                $url = str_replace( basename( $parent_url ), basename( $cropped ), $parent_url );
     1073
     1074                $size = @getimagesize( $cropped );
     1075                $image_type = ( $size ) ? $size['mime'] : 'image/jpeg';
     1076
     1077                $object = array(
     1078                        'ID' => $parent_attachment_id,
     1079                        'post_title' => basename($cropped),
     1080                        'post_content' => $url,
     1081                        'post_mime_type' => $image_type,
     1082                        'guid' => $url,
     1083                        'context' => 'custom-header'
     1084                );
     1085
     1086                return $object;
     1087        }
     1088
     1089        /**
     1090         * Insert an attachment & its metadata.
     1091         *
     1092         * @param array $object Attachment object.
     1093         * @param string $cropped Cropped image URL.
     1094         *
     1095         * @return int Attachment ID.
     1096         */
     1097        final public function insert_attachment( $object, $cropped ) {
     1098                $attachment_id = wp_insert_attachment( $object, $cropped );
     1099                $metadata = wp_generate_attachment_metadata( $attachment_id, $cropped );
     1100                /**
     1101                 * Allows us to insert custom meta data for an attachment.
     1102                 *
     1103                 */
     1104                $metadata = apply_filters( 'wp_header_image_attachment_metadata', $metadata );
     1105                wp_update_attachment_metadata( $attachment_id, $metadata );
     1106                return $attachment_id;
     1107        }
     1108
     1109        function ajax_check_permission( $name, $nonce, $attachment_id = null ) {
     1110                $name = $attachment_id === null ? $name : "${name}_${attachment_id}";
     1111                if ( ! isset( $nonce ) || ! wp_verify_nonce( $nonce, $name ) ||
     1112                                        ! current_user_can('edit_theme_options') ) {
     1113                        wp_die( __( 'Cheatin’ uh?' ) );
     1114                }
     1115        }
     1116
     1117        /**
     1118         * Gets attachment uploaded by Media Manager, crops it, then saves it as a
     1119         * new object. Returns JSON-encoded object details.
     1120         */
     1121        function ajax_header_crop() {
     1122                $data = $_POST['data'];
     1123                $this->ajax_check_permission( 'crop-image', $data['nonces']['crop'], $data['id'] );
     1124                if ( ! current_theme_supports( 'custom-header', 'uploads' ) )
     1125                        wp_die( __( 'Cheatin’ uh?' ) );
     1126
     1127                if ( ! empty( $data['skip-cropping'] ) && ! ( current_theme_supports( 'custom-header', 'flex-height' ) || current_theme_supports( 'custom-header', 'flex-width' ) ) )
     1128                        wp_die( __( 'Cheatin’ uh?' ) );
     1129
     1130                $crop_details = $data['cropDetails'];
     1131
     1132                $dimensions = $this->get_header_dimensions( array(
     1133                        'width' => $crop_details['width'],
     1134                        'height' => $crop_details['height'],
     1135                ) );
     1136
     1137                $attachment_id = absint( $data['id'] );
     1138
     1139                $cropped = wp_crop_image( $attachment_id, (int) $crop_details['x1'], (int) $crop_details['y1'], (int) $crop_details['width'], (int) $crop_details['height'], (int) $dimensions['dst_width'], (int) $dimensions['dst_height'] );
     1140
     1141                if ( ! $cropped || is_wp_error( $cropped ) )
     1142                        wp_die( __( 'Image could not be processed. Please go back and try again.' ), __( 'Image Processing Error' ) );
     1143
     1144                $cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication
     1145
     1146                $object = $this->create_attachment_object( $cropped, $attachment_id );
     1147
     1148                unset( $object['ID'] );
     1149
     1150                $new_attachment_id = $this->insert_attachment( $object, $cropped );
     1151
     1152                $object['attachment_id'] = $new_attachment_id;
     1153                $object['width']         = $dimensions['dst_width'];
     1154                $object['height']        = $dimensions['dst_height'];
     1155
     1156                echo json_encode($object);
     1157                wp_die();
     1158        }
     1159
     1160        /**
     1161         * Given an attachment ID for a header image, updates its "last used"
     1162         * timestamp to now.
     1163         *
     1164         * Triggered when the user tries adds a new header image from the
     1165         * Media Manager, even if s/he doesn't save that change.
     1166         */
     1167        function ajax_header_add() {
     1168                $data = $_POST['data'];
     1169                $this->ajax_check_permission( 'header-add', $_REQUEST['nonce'] );
     1170
     1171                $attachment_id = absint( $data['attachment_id'] );
     1172                if ( $attachment_id < 1 )
     1173                        return;
     1174
     1175                $key = '_wp_attachment_custom_header_last_used_' . get_stylesheet();
     1176                update_post_meta( $attachment_id, $key, time() );
     1177                update_post_meta( $attachment_id, '_wp_attachment_is_custom_header', get_stylesheet() );
     1178
     1179                wp_die();
     1180        }
     1181
     1182        /**
     1183         * Given an attachment ID for a header image, unsets it as a user-uploaded
     1184         * header image for the current theme.
     1185         *
     1186         * Triggered when the user clicks the overlay "X" button next to each image
     1187         * choice in the Customizer's Header tool.
     1188         */
     1189        function ajax_header_remove() {
     1190                $data = $_POST['data'];
     1191                $this->ajax_check_permission( 'header-remove', $_REQUEST['nonce'] );
     1192
     1193                $attachment_id = absint( $data['attachment_id'] );
     1194                if ( $attachment_id < 1 )
     1195                        return;
     1196
     1197                $key = '_wp_attachment_custom_header_last_used_' . get_stylesheet();
     1198                delete_post_meta( $attachment_id, $key );
     1199                delete_post_meta( $attachment_id, '_wp_attachment_is_custom_header', get_stylesheet() );
     1200
     1201                wp_die();
     1202        }
     1203
     1204        function set_last_used( $manager ) {
     1205                $data = $manager->get_setting( 'header_image_data' )->post_value();
     1206
     1207                if ( !isset( $data['attachment_id'] ) )
     1208                        return;
     1209
     1210                $attachment_id = $data['attachment_id'];
     1211                $key = '_wp_attachment_custom_header_last_used_' . get_stylesheet();
     1212                update_post_meta( $attachment_id, $key, time() );
     1213        }
    10441214}
  • src/wp-admin/js/customize-controls.js

    diff --git src/wp-admin/js/customize-controls.js src/wp-admin/js/customize-controls.js
    index 3a05ad4..aed0f6d 100644
     
     1/* globals _wpCustomizeHeader, _wpMediaViewsL10n */
    12(function( exports, $ ){
    23        var api = wp.customize;
    34
     
    306307                }
    307308        });
    308309
     310        api.HeaderControl = api.Control.extend({
     311                ready: function() {
     312                        this.btnRemove        = $('.actions .remove');
     313                        this.btnNew           = $('.actions .new');
     314
     315                        _.bindAll(this, 'openMM', 'removeImage');
     316
     317                        this.btnNew.on( 'click', this.openMM );
     318                        this.btnRemove.on( 'click', this.removeImage );
     319
     320                        api.HeaderTool.currentHeader = new api.HeaderTool.ImageModel();
     321
     322                        new api.HeaderTool.CurrentView({
     323                                model: api.HeaderTool.currentHeader,
     324                                el: '.current .container'
     325                        });
     326
     327                        new api.HeaderTool.ChoiceListView({
     328                                collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
     329                                el: '.choices .uploaded .list'
     330                        });
     331
     332                        new api.HeaderTool.ChoiceListView({
     333                                collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
     334                                el: '.choices .default .list'
     335                        });
     336
     337                        api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
     338                                api.HeaderTool.UploadsList,
     339                                api.HeaderTool.DefaultsList
     340                        ]);
     341                },
     342
     343                /**
     344                 * Returns a set of options, computed from the attached image data and
     345                 * theme-specific data, to be fed to the imgAreaSelect plugin in
     346                 * wp.media.view.Cropper.
     347                 *
     348                 * @param {wp.media.model.Attachment} attachment
     349                 * @param {wp.media.controller.Cropper} controller
     350                 * @returns {Object} Options
     351                 */
     352                calculateImageSelectOptions: function(attachment, controller) {
     353                        var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
     354                                yInit = parseInt(_wpCustomizeHeader.data.height, 10),
     355                                flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
     356                                flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
     357                                ratio, xImg, yImg, realHeight, realWidth,
     358                                imgSelectOptions;
     359
     360                        realWidth = attachment.get('width');
     361                        realHeight = attachment.get('height');
     362
     363                        this.headerImage = new api.HeaderTool.ImageModel();
     364                        this.headerImage.set({
     365                                themeWidth: xInit,
     366                                themeHeight: yInit,
     367                                themeFlexWidth: flexWidth,
     368                                themeFlexHeight: flexHeight,
     369                                imageWidth: realWidth,
     370                                imageHeight: realHeight
     371                        });
     372
     373                        controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
     374
     375                        ratio = xInit / yInit;
     376                        xImg = realWidth;
     377                        yImg = realHeight;
     378
     379                        if ( xImg / yImg > ratio ) {
     380                                yInit = yImg;
     381                                xInit = yInit * ratio;
     382                        } else {
     383                                xInit = xImg;
     384                                yInit = xInit / ratio;
     385                        }
     386
     387                        imgSelectOptions = {
     388                                handles: true,
     389                                keys: true,
     390                                instance: true,
     391                                persistent: true,
     392                                parent: this.$el,
     393                                imageWidth: realWidth,
     394                                imageHeight: realHeight,
     395                                x1: 0,
     396                                y1: 0,
     397                                x2: xInit,
     398                                y2: yInit
     399                        };
     400
     401                        if (flexHeight === false && flexWidth === false) {
     402                                imgSelectOptions.aspectRatio = xInit + ':' + yInit;
     403                        }
     404                        if (flexHeight === false ) {
     405                                imgSelectOptions.maxHeight = yInit;
     406                        }
     407                        if (flexWidth === false ) {
     408                                imgSelectOptions.maxWidth = xInit;
     409                        }
     410
     411                        return imgSelectOptions;
     412                },
     413
     414                /**
     415                 * Sets up and opens the Media Manager in order to select an image.
     416                 * Depending on both the size of the image and the properties of the
     417                 * current theme, a cropping step after selection may be required or
     418                 * skippable.
     419                 *
     420                 * @param {event} event
     421                 */
     422                openMM: function(event) {
     423                        var title, suggestedWidth, suggestedHeight,
     424                                l10n = _wpMediaViewsL10n;
     425
     426                        event.preventDefault();
     427
     428                        suggestedWidth = l10n.suggestedWidth.replace('%d', _wpCustomizeHeader.data.width);
     429                        suggestedHeight = l10n.suggestedHeight.replace('%d', _wpCustomizeHeader.data.height);
     430
     431                        title = {
     432                                html: l10n.chooseImage + ' <span class="suggested-dimensions">' +
     433                                                        suggestedWidth + ' ' + suggestedHeight +'</span>',
     434                                text: l10n.chooseImage
     435                        };
     436
     437                        this.frame = wp.media({
     438                                title: title,
     439                                library: {
     440                                        type: 'image'
     441                                },
     442                                button: {
     443                                        text: l10n.selectAndCrop,
     444                                        close: false
     445                                },
     446                                multiple: false,
     447                                imgSelectOptions: this.calculateImageSelectOptions
     448                        });
     449
     450                        this.frame.states.add([new wp.media.controller.Cropper()]);
     451
     452                        this.frame.on('select', this.onSelect, this);
     453                        this.frame.on('cropped', this.onCropped, this);
     454                        this.frame.on('skippedcrop', this.onSkippedCrop, this);
     455
     456                        this.frame.open();
     457                },
     458
     459                onSelect: function() {
     460                        this.frame.setState('cropper');
     461                },
     462                onCropped: function(croppedImage) {
     463                        var url = croppedImage.post_content,
     464                                attachmentId = croppedImage.attachment_id,
     465                                w = croppedImage.width,
     466                                h = croppedImage.height;
     467                        this.setImageFromURL(url, attachmentId, w, h);
     468                },
     469                onSkippedCrop: function(selection) {
     470                        var url = selection.get('url'),
     471                                w = selection.get('width'),
     472                                h = selection.get('height');
     473                        this.setImageFromURL(url, selection.id, w, h);
     474                },
     475
     476                /**
     477                 * Creates a new wp.customize.HeaderTool.ImageModel from provided
     478                 * header image data and inserts it into the user-uploaded headers
     479                 * collection.
     480                 *
     481                 * @param {String} url
     482                 * @param {Number} attachmentId
     483                 * @param {Number} width
     484                 * @param {Number} height
     485                 */
     486                setImageFromURL: function(url, attachmentId, width, height) {
     487                        var choice, data = {};
     488
     489                        data.url = url;
     490                        data.thumbnail_url = url;
     491
     492                        if (attachmentId) {
     493                                data.attachment_id = attachmentId;
     494                        }
     495
     496                        if (width) {
     497                                data.width = width;
     498                        }
     499
     500                        if (height) {
     501                                data.height = height;
     502                        }
     503
     504                        choice = new api.HeaderTool.ImageModel({
     505                                header: data,
     506                                choice: url.split('/').pop()
     507                        });
     508                        api.HeaderTool.UploadsList.add(choice);
     509                        api.HeaderTool.currentHeader.set(choice.toJSON());
     510                        choice.save();
     511                        choice.importImage();
     512                },
     513
     514                /**
     515                 * Triggers the necessary events to deselect an image which was set as
     516                 * the currently selected one.
     517                 */
     518                removeImage: function() {
     519                        api.HeaderTool.currentHeader.trigger('hide');
     520                        api.HeaderTool.CombinedList.trigger('control:removeImage');
     521                }
     522
     523        });
     524
    309525        // Change objects contained within the main customize object to Settings.
    310526        api.defaultConstructor = api.Setting;
    311527
     
    686902        api.controlConstructor = {
    687903                color:  api.ColorControl,
    688904                upload: api.UploadControl,
    689                 image:  api.ImageControl
     905                image:  api.ImageControl,
     906                header: api.HeaderControl
    690907        };
    691908
    692909        $( function() {
     
    9611178                        });
    9621179                });
    9631180
    964                 // Handle header image data
    965                 api.control( 'header_image', function( control ) {
    966                         control.setting.bind( function( to ) {
    967                                 if ( to === control.params.removed )
    968                                         control.settings.data.set( false );
    969                         });
    970 
    971                         control.library.on( 'click', 'a', function() {
    972                                 control.settings.data.set( $(this).data('customizeHeaderImageData') );
    973                         });
    974 
    975                         control.uploader.success = function( attachment ) {
    976                                 var data;
    977 
    978                                 api.ImageControl.prototype.success.call( control, attachment );
    979 
    980                                 data = {
    981                                         attachment_id: attachment.get('id'),
    982                                         url:           attachment.get('url'),
    983                                         thumbnail_url: attachment.get('url'),
    984                                         height:        attachment.get('height'),
    985                                         width:         attachment.get('width')
    986                                 };
    987 
    988                                 attachment.element.data( 'customizeHeaderImageData', data );
    989                                 control.settings.data.set( data );
    990                         };
    991                 });
    992 
    9931181                api.trigger( 'ready' );
    9941182
    9951183                // Make sure left column gets focus
  • src/wp-includes/class-wp-customize-control.php

    diff --git src/wp-includes/class-wp-customize-control.php src/wp-includes/class-wp-customize-control.php
    index fde8561..aed26fc 100644
    class WP_Customize_Background_Image_Control extends WP_Customize_Image_Control { 
    691691        }
    692692}
    693693
    694 /**
    695  * Customize Header Image Control Class
    696  *
    697  * @package WordPress
    698  * @subpackage Customize
    699  * @since 3.4.0
    700  */
    701 class WP_Customize_Header_Image_Control extends WP_Customize_Image_Control {
    702         /**
    703          * The processed default headers.
    704          * @since 3.4.2
    705          * @var array
    706          */
    707         protected $default_headers;
    708 
    709         /**
    710          * The uploaded headers.
    711          * @since 3.4.2
    712          * @var array
    713          */
    714         protected $uploaded_headers;
     694class WP_Customize_Header_Image_Control extends WP_Customize_Control {
     695        public $type = 'header';
    715696
    716         /**
    717          * Constructor.
    718          *
    719          * @since 3.4.0
    720          * @uses WP_Customize_Image_Control::__construct()
    721          * @uses WP_Customize_Image_Control::add_tab()
    722          *
    723          * @param WP_Customize_Manager $manager
    724          */
    725697        public function __construct( $manager ) {
    726698                parent::__construct( $manager, 'header_image', array(
    727699                        'label'    => __( 'Header Image' ),
    class WP_Customize_Header_Image_Control extends WP_Customize_Image_Control { 
    733705                        'context'  => 'custom-header',
    734706                        'removed'  => 'remove-header',
    735707                        'get_url'  => 'get_header_image',
    736                         'statuses' => array(
    737                                 ''                      => __('Default'),
    738                                 'remove-header'         => __('No Image'),
    739                                 'random-default-image'  => __('Random Default Image'),
    740                                 'random-uploaded-image' => __('Random Uploaded Image'),
     708                ) );
     709
     710        }
     711
     712        public function to_json() {
     713                parent::to_json();
     714        }
     715
     716        public function enqueue() {
     717                wp_enqueue_media();
     718                wp_enqueue_script( 'customize-header-views' );
     719
     720                $this->prepare_control();
     721
     722                wp_localize_script( 'customize-header-views', '_wpCustomizeHeader', array(
     723                        'data' => array(
     724                                'width' => absint( get_theme_support( 'custom-header', 'width' ) ),
     725                                'height' => absint( get_theme_support( 'custom-header', 'height' ) ),
     726                                'flex-width' => absint( get_theme_support( 'custom-header', 'flex-width' ) ),
     727                                'flex-height' => absint( get_theme_support( 'custom-header', 'flex-height' ) ),
     728                                'currentImgSrc' => $this->get_current_image_src(),
     729                        ),
     730                        'nonces' => array(
     731                                'add' => wp_create_nonce( 'header-add' ),
     732                                'remove' => wp_create_nonce( 'header-remove' ),
     733                        ),
     734                        'l10n' => array(
     735                                /* translators: header images uploaded by user */
     736                                'uploaded' => __( 'uploaded' ),
     737                                /* translators: header images suggested by the current theme */
     738                                'default' => __( 'suggested' )
     739                        ),
     740                        'uploads' => $this->uploaded_headers,
     741                        'defaults' => $this->default_headers
     742                ) );
     743
     744                parent::enqueue();
     745        }
     746
     747        public function get_default_header_images() {
     748                global $custom_image_header;
     749
     750                // Get *the* default image if there is one
     751                $default = get_theme_support( 'custom-header', 'default-image' );
     752
     753                if ( ! $default ) { // If not,
     754                        return $custom_image_header->default_headers; // easy peasy.
     755                }
     756
     757                $default = sprintf( $default,
     758                        get_template_directory_uri(),
     759                        get_stylesheet_directory_uri() );
     760
     761                $header_images = array();
     762                $already_has_default = false;
     763
     764                // Get the whole set of default images
     765                $default_header_images = $custom_image_header->default_headers;
     766                foreach ( $default_header_images as $k => $h ) {
     767                        if ( $h['url'] == $default ) {
     768                                $already_has_default = true;
     769                                break;
     770                        }
     771                }
     772
     773                // If *the one true image* isn't included in the default set, add it in
     774                // first position
     775                if ( ! $already_has_default ) {
     776                        $header_images['default'] = array(
     777                                'url' => $default,
     778                                'thumbnail_url' => $default,
     779                                'description' => 'Default'
     780                        );
     781                }
     782
     783                // The rest of the set comes after
     784                $header_images = array_merge( $header_images, $default_header_images );
     785
     786                return $header_images;
     787        }
     788
     789        public function get_uploaded_header_images() {
     790                $key = '_wp_attachment_custom_header_last_used_' . get_stylesheet();
     791                $header_images = array();
     792
     793                $headers_not_dated = get_posts( array(
     794                        'post_type' => 'attachment',
     795                        'meta_key' => '_wp_attachment_is_custom_header',
     796                        'meta_value' => get_option('stylesheet'),
     797                        'orderby' => 'none',
     798                        'nopaging' => true,
     799                        'meta_query' => array(
     800                                array(
     801                                        'key' => '_wp_attachment_is_custom_header',
     802                                        'value' => get_option( 'stylesheet' ),
     803                                        'compare' => 'LIKE'
     804                                ),
     805                                array(
     806                                        'key' => $key,
     807                                        'value' => 'this string must not be empty',
     808                                        'compare' => 'NOT EXISTS'
     809                                ),
    741810                        )
    742811                ) );
    743812
    744                 // Remove the upload tab.
    745                 $this->remove_tab( 'upload-new' );
     813                $headers_dated = get_posts( array(
     814                        'post_type' => 'attachment',
     815                        'meta_key' => $key,
     816                        'orderby' => 'meta_value_num',
     817                        'order' => 'DESC',
     818                        'nopaging' => true,
     819                        'meta_query' => array(
     820                                array(
     821                                        'key' => '_wp_attachment_is_custom_header',
     822                                        'value' => get_option( 'stylesheet' ),
     823                                        'compare' => 'LIKE'
     824                                ),
     825                        ),
     826                ) );
     827
     828                $limit = apply_filters( 'custom_header_uploaded_limit', 15 );
     829                $headers = array_merge( $headers_dated, $headers_not_dated );
     830                $headers = array_slice( $headers, 0, $limit );
     831
     832                foreach ( (array) $headers as $header ) {
     833                        $url = esc_url_raw( $header->guid );
     834                        $header_data = wp_get_attachment_metadata( $header->ID );
     835                        $timestamp = get_post_meta( $header->ID,
     836                                '_wp_attachment_custom_header_last_used_' . get_stylesheet(),
     837                                true );
     838
     839                        $h = array(
     840                                'attachment_id' => $header->ID,
     841                                'url'           => $url,
     842                                'thumbnail_url' => $url,
     843                                'timestamp'     => $timestamp ? $timestamp : 0,
     844                        );
     845
     846                        if ( isset( $header_data['width'] ) ) {
     847                                $h['width'] = $header_data['width'];
     848                        }
     849                        if ( isset( $header_data['height'] ) ) {
     850                                $h['height'] = $header_data['height'];
     851                        }
     852
     853                        $header_images[] = $h;
     854                }
     855
     856                return $header_images;
    746857        }
    747858
    748         /**
    749          * Prepares the control.
    750          *
    751          * If no tabs exist, removes the control from the manager.
    752          *
    753          * @since 3.4.2
    754          */
    755859        public function prepare_control() {
    756860                global $custom_image_header;
    757                 if ( empty( $custom_image_header ) )
    758                         return parent::prepare_control();
     861                if ( empty( $custom_image_header ) ) {
     862                        return;
     863                }
    759864
    760865                // Process default headers and uploaded headers.
    761866                $custom_image_header->process_default_headers();
    762                 $this->default_headers = $custom_image_header->default_headers;
    763                 $this->uploaded_headers = get_uploaded_header_images();
     867                $this->default_headers = $this->get_default_header_images();
     868                $this->uploaded_headers = $this->get_uploaded_header_images();
     869        }
    764870
    765                 if ( $this->default_headers )
    766                         $this->add_tab( 'default',  __('Default'),  array( $this, 'tab_default_headers' ) );
     871        function print_header_image_template() {
     872                ?>
     873                <script type="text/template" id="tmpl-header-choice">
     874                        <% if (random) { %>
     875
     876                        <div class="placeholder random">
     877                                <div class="inner">
     878                                        <span><span class="dice">&#9860;</span>
     879                                                <?php /* translators: "nImages" is a number, "type" is either "uploaded" or "suggested" */ ?>
     880                                                <?php _e( 'Randomize <%- nImages %> <%- type %> headers' ); ?>
     881                                        </span>
     882                                </div>
     883                        </div>
    767884
    768                 if ( ! $this->uploaded_headers )
    769                         $this->remove_tab( 'uploaded' );
     885                        <% } else { %>
    770886
    771                 return parent::prepare_control();
    772         }
     887                        <% if (type == 'uploaded') { %>
     888                        <div class="dashicons dashicons-no close"></div>
     889                        <% } %>
    773890
    774         /**
    775          * @since 3.4.0
    776          *
    777          * @param mixed $choice Which header image to select. (@see Custom_Image_Header::get_header_image() )
    778          * @param array $header
    779          */
    780         public function print_header_image( $choice, $header ) {
    781                 $header['url']           = set_url_scheme( $header['url'] );
    782                 $header['thumbnail_url'] = set_url_scheme( $header['thumbnail_url'] );
     891                        <a href="#" class="choice thumbnail %>"
     892                                data-customize-image-value="<%- header.url %>"
     893                                data-customize-header-image-data="<%- JSON.stringify(header) %>">
     894                                <img src="<%- header.thumbnail_url %>">
     895                        </a>
    783896
    784                 $header_image_data = array( 'choice' => $choice );
    785                 foreach ( array( 'attachment_id', 'width', 'height', 'url', 'thumbnail_url' ) as $key ) {
    786                         if ( isset( $header[ $key ] ) )
    787                                 $header_image_data[ $key ] = $header[ $key ];
    788                 }
     897                        <% } %>
     898                </script>
    789899
     900                <script type="text/template" id="tmpl-header-current">
     901                        <% if (choice) { %>
     902                                <% if (random) { %>
    790903
    791                 ?>
    792                 <a href="#" class="thumbnail"
    793                         data-customize-image-value="<?php echo esc_url( $header['url'] ); ?>"
    794                         data-customize-header-image-data="<?php echo esc_attr( json_encode( $header_image_data ) ); ?>">
    795                         <img src="<?php echo esc_url( $header['thumbnail_url'] ); ?>" />
    796                 </a>
     904                        <div class="placeholder">
     905                                <div class="inner">
     906                                        <span><span class="dice">&#9860;</span>
     907                                                <?php /* translators: "nImages" is a number, "type" is either "uploaded" or "suggested" */ ?>
     908                                                <?php _e( 'Randomizing <%- nImages %> <%- type %> headers' ); ?>
     909                                        </span>
     910                                </div>
     911                        </div>
     912
     913                                <% } else { %>
     914
     915                        <img src="<%- header.thumbnail_url %>" />
     916
     917                                <% } %>
     918                        <% } else { %>
     919
     920                        <div class="placeholder">
     921                                <div class="inner">
     922                                        <span>
     923                                                <?php _e( 'No image set' ); ?>
     924                                        </span>
     925                                </div>
     926                        </div>
     927
     928                        <% } %>
     929                </script>
    797930                <?php
    798931        }
    799932
    800         /**
    801          * @since 3.4.0
    802          */
    803         public function tab_uploaded() {
    804                 ?><div class="uploaded-target"></div><?php
    805 
    806                 foreach ( $this->uploaded_headers as $choice => $header )
    807                         $this->print_header_image( $choice, $header );
     933        public function get_current_image_src() {
     934                $src = $this->value();
     935                if ( isset( $this->get_url ) ) {
     936                        $src = call_user_func( $this->get_url, $src );
     937                        return $src;
     938                }
     939                return null;
    808940        }
    809941
    810         /**
    811          * @since 3.4.0
    812          */
    813         public function tab_default_headers() {
    814                 foreach ( $this->default_headers as $choice => $header )
    815                         $this->print_header_image( $choice, $header );
     942        public function render_content() {
     943                $this->print_header_image_template();
     944                $visibility = $this->get_current_image_src() ? '' : ' style="display:none" ';
     945                $width = absint( get_theme_support( 'custom-header', 'width' ) );
     946                $height = absint( get_theme_support( 'custom-header', 'height' ) );
     947                ?>
     948
     949
     950                <div class="customize-control-content">
     951                        <p class="customizer-section-intro">
     952                                <?php _e( 'Personalize your blog with your own header image.' ); ?>
     953                                <?php
     954                                if ( $width && $height ) {
     955                                        printf( __( 'While you can crop images to your liking after clicking <strong>%s</strong>, your theme recommends a header size of <strong>%dx%d</strong> pixels.' ),
     956                                                _x( 'Add new', 'new image', 'custom-header' ), $width, $height );
     957                                } else {
     958                                        if ( $width ) {
     959                                                printf( __( 'While you can crop images to your liking after clicking <strong>%s</strong>, your theme recommends a header width of <strong>%d</strong> pixels.' ),
     960                                                        _x( 'Add new', 'new image', 'custom-header' ), $width );
     961                                        }
     962                                        if ( $height ) {
     963                                                printf( __( 'While you can crop images to your liking after clicking <strong>%s</strong>, your theme recommends a header height of <strong>%d</strong> pixels.' ),
     964                                                        _x( 'Add new', 'new image', 'custom-header' ), $height );
     965                                        }
     966                                }
     967                                ?>
     968                        </p>
     969                        <div class="current">
     970                                <span class="customize-control-title">
     971                                        <?php _e( 'Current header', 'custom-header' ); ?>
     972                                </span>
     973                                <div class="container">
     974                                </div>
     975                        </div>
     976                        <div class="actions">
     977                                <?php /* translators: Hide as in hide header image via the Customizer */ ?>
     978                                <a href="#" <?php echo $visibility ?> class="button remove"><?php _e( 'Hide', 'custom-header' ); ?></a>
     979                                <?php /* translators: New as in add new header image via the Customizer */ ?>
     980                                <a href="#" class="button new"><?php _ex( 'Add new', 'new image', 'custom-header' ); ?></a>
     981                                <div style="clear:both"></div>
     982                        </div>
     983                        <div class="choices">
     984                                <span class="customize-control-title header-previously-uploaded">
     985                                        <?php _e( 'Previously uploaded', 'custom-header' ); ?>
     986                                </span>
     987                                <div class="uploaded">
     988                                        <div class="list">
     989                                        </div>
     990                                </div>
     991                                <span class="customize-control-title header-default">
     992                                        <?php _e( 'Suggested', 'custom-header' ); ?>
     993                                </span>
     994                                <div class="default">
     995                                        <div class="list">
     996                                        </div>
     997                                </div>
     998                        </div>
     999                </div>
     1000                <?php
    8161001        }
    817 }
    818  No newline at end of file
     1002
     1003}
  • new file src/wp-includes/js/customize-header-models.js

    diff --git src/wp-includes/js/customize-header-models.js src/wp-includes/js/customize-header-models.js
    new file mode 100644
    index 0000000..475ca4c
    - +  
     1/* globals jQuery, _wpCustomizeHeader, _wpCustomizeSettings */
     2;( function( $, wp ) {
     3        var api = wp.customize;
     4        api.HeaderTool = {};
     5
     6
     7        /**
     8         * wp.customize.HeaderTool.ImageModel
     9         *
     10         * A header image. This is where saves via the Customizer API are
     11         * abstracted away, plus our own AJAX calls to add images to and remove
     12         * images from the user's recently uploaded images setting on the server.
     13         * These calls are made regardless of whether the user actually saves new
     14         * Customizer settings.
     15         *
     16         * @constructor
     17         * @augments Backbone.Model
     18         */
     19        api.HeaderTool.ImageModel = Backbone.Model.extend({
     20                defaults: function() {
     21                        return {
     22                                header: {
     23                                        attachment_id: 0,
     24                                        url: '',
     25                                        timestamp: Date.now(),
     26                                        thumbnail_url: ''
     27                                },
     28                                choice: '',
     29                                hidden: false,
     30                                random: false
     31                        };
     32                },
     33
     34                initialize: function() {
     35                        this.on('hide', this.hide, this);
     36                },
     37
     38                hide: function() {
     39                        this.set('choice', '');
     40                        api('header_image').set('remove-header');
     41                        api('header_image_data').set('remove-header');
     42                },
     43
     44                destroy: function() {
     45                        var data = this.get('header'),
     46                                curr = api.HeaderTool.currentHeader.get('header').attachment_id;
     47
     48                        // If the image we're removing is also the current header, unset
     49                        // the latter
     50                        if (curr && data.attachment_id === curr) {
     51                                api.HeaderTool.currentHeader.trigger('hide');
     52                        }
     53
     54                        $.post(_wpCustomizeSettings.url.ajax, {
     55                                wp_customize: 'on',
     56                                theme: api.settings.theme.stylesheet,
     57                                dataType: 'json',
     58                                action: 'header_remove',
     59                                nonce: _wpCustomizeHeader.nonces.remove,
     60                                data: data
     61                        });
     62
     63                        this.trigger('destroy', this, this.collection);
     64                },
     65
     66                save: function() {
     67                        if (this.get('random')) {
     68                                api('header_image').set(this.get('header').random);
     69                                api('header_image_data').set(this.get('header').random);
     70                        } else {
     71                                if (this.get('header').defaultName) {
     72                                        api('header_image').set(this.get('header').url);
     73                                        api('header_image_data').set(this.get('header').defaultName);
     74                                } else {
     75                                        api('header_image').set(this.get('header').url);
     76                                        api('header_image_data').set(this.get('header'));
     77                                }
     78                        }
     79
     80                        api.HeaderTool.combinedList.trigger('control:setImage', this);
     81                },
     82
     83                importImage: function() {
     84                        var data = this.get('header');
     85                        if (data.attachment_id === undefined) {
     86                                return;
     87                        }
     88
     89                        $.post(_wpCustomizeSettings.url.ajax, {
     90                                wp_customize: 'on',
     91                                theme: api.settings.theme.stylesheet,
     92                                dataType: 'json',
     93                                action: 'header_add',
     94                                nonce: _wpCustomizeHeader.nonces.add,
     95                                data: data
     96                        });
     97                },
     98
     99                shouldBeCropped: function() {
     100                        if (this.get('themeFlexWidth') === true &&
     101                                                this.get('themeFlexHeight') === true) {
     102                                return false;
     103                        }
     104
     105                        if (this.get('themeFlexWidth') === true &&
     106                                this.get('themeHeight') === this.get('imageHeight')) {
     107                                return false;
     108                        }
     109
     110                        if (this.get('themeFlexHeight') === true &&
     111                                this.get('themeWidth') === this.get('imageWidth')) {
     112                                return false;
     113                        }
     114
     115                        if (this.get('themeWidth') === this.get('imageWidth') &&
     116                                this.get('themeHeight') === this.get('imageHeight')) {
     117                                return false;
     118                        }
     119
     120                        return true;
     121                }
     122        });
     123
     124
     125        /**
     126         * wp.customize.HeaderTool.ChoiceList
     127         *
     128         * @constructor
     129         * @augments Backbone.Collection
     130         */
     131        api.HeaderTool.ChoiceList = Backbone.Collection.extend({
     132                model: api.HeaderTool.ImageModel,
     133
     134                // Ordered from most recently used to least
     135                comparator: function(model) {
     136                        return -model.get('header').timestamp;
     137                },
     138
     139                initialize: function() {
     140                        var current = api.HeaderTool.currentHeader.get('choice').replace(/^https?:\/\//, ''),
     141                                isRandom = this.isRandomChoice(api.get().header_image);
     142
     143                        // Overridable by an extending class
     144                        if (!this.type) {
     145                                this.type = 'uploaded';
     146                        }
     147
     148                        // Overridable by an extending class
     149                        if (!this.data) {
     150                                this.data = _wpCustomizeHeader.uploads;
     151                        }
     152
     153                        if (isRandom) {
     154                                // So that when adding data we don't hide regular images
     155                                current = api.get().header_image;
     156                        }
     157
     158                        this.on('control:setImage', this.setImage, this);
     159                        this.on('control:removeImage', this.removeImage, this);
     160                        this.on('add', this.maybeAddRandomChoice, this);
     161
     162                        _.each(this.data, function(elt, index) {
     163                                if (!elt.attachment_id) {
     164                                        elt.defaultName = index;
     165                                }
     166
     167                                this.add({
     168                                        header: elt,
     169                                        choice: elt.url.split('/').pop(),
     170                                        hidden: current === elt.url.replace(/^https?:\/\//, '')
     171                                }, { silent: true });
     172                        }, this);
     173
     174                        if (this.size() > 0) {
     175                                this.addRandomChoice(current);
     176                        }
     177                },
     178
     179                maybeAddRandomChoice: function() {
     180                        if (this.size() === 1) {
     181                                this.addRandomChoice();
     182                        }
     183                },
     184
     185                addRandomChoice: function(initialChoice) {
     186                        var isRandomSameType = RegExp(this.type).test(initialChoice),
     187                                randomChoice = 'random-' + this.type + '-image';
     188
     189                        this.add({
     190                                header: {
     191                                        timestamp: 0,
     192                                        random: randomChoice,
     193                                        width: 245,
     194                                        height: 41
     195                                },
     196                                choice: randomChoice,
     197                                random: true,
     198                                hidden: isRandomSameType
     199                        });
     200                },
     201
     202                isRandomChoice: function(choice) {
     203                        return (/^random-(uploaded|default)-image$/).test(choice);
     204                },
     205
     206                shouldHideTitle: function() {
     207                        return _.every(this.pluck('hidden'));
     208                },
     209
     210                setImage: function(model) {
     211                        this.each(function(m) {
     212                                m.set('hidden', false);
     213                        });
     214
     215                        if (model) {
     216                                model.set('hidden', true);
     217                                // Bump images to top except for special "Randomize" images
     218                                if (!model.get('random')) {
     219                                        model.get('header').timestamp = Date.now();
     220                                        this.sort();
     221                                }
     222                        }
     223                },
     224
     225                removeImage: function() {
     226                        this.each(function(m) {
     227                                m.set('hidden', false);
     228                        });
     229                },
     230
     231                shown: function() {
     232                        var filtered = this.where({ hidden: false });
     233                        return new api.HeaderTool.ChoiceList( filtered );
     234                }
     235        });
     236
     237
     238        /**
     239         * wp.customize.HeaderTool.DefaultsList
     240         *
     241         * @constructor
     242         * @augments wp.customize.HeaderTool.ChoiceList
     243         * @augments Backbone.Collection
     244         */
     245        api.HeaderTool.DefaultsList = api.HeaderTool.ChoiceList.extend({
     246                initialize: function() {
     247                        this.type = 'default';
     248                        this.data = _wpCustomizeHeader.defaults;
     249                        api.HeaderTool.ChoiceList.prototype.initialize.apply(this);
     250                }
     251        });
     252
     253})( jQuery, this.wp );
  • new file src/wp-includes/js/customize-header-views.js

    diff --git src/wp-includes/js/customize-header-views.js src/wp-includes/js/customize-header-views.js
    new file mode 100644
    index 0000000..06be379
    - +  
     1/* globals jQuery, _, Backbone, _wpCustomizeHeader */
     2;( function( $, wp, _ ) {
     3        if ( ! wp || ! wp.customize ) { return; }
     4        var api = wp.customize;
     5
     6
     7        /**
     8         * wp.customize.HeaderTool.CurrentView
     9         *
     10         * Displays the currently selected header image, or a placeholder in lack
     11         * thereof.
     12         *
     13         * Instantiate with model wp.customize.HeaderTool.currentHeader.
     14         *
     15         * @constructor
     16         * @augments Backbone.View
     17         */
     18        api.HeaderTool.CurrentView = Backbone.View.extend({
     19                template: _.template($('#tmpl-header-current').html()),
     20
     21                initialize: function() {
     22                        this.listenTo(this.model, 'change', this.render);
     23                        this.render();
     24                },
     25
     26                render: function() {
     27                        this.$el.html(this.template(this.model.toJSON()));
     28                        this.setPlaceholder();
     29                        this.setButtons();
     30                        return this;
     31                },
     32
     33                getHeight: function() {
     34                        var image = this.$el.find('img'),
     35                                saved = this.model.get('savedHeight'),
     36                                height = image.height() || saved,
     37                                headerImageData;
     38
     39                        if (image.length) {
     40                                this.$el.find('.inner').hide();
     41                        } else {
     42                                this.$el.find('.inner').show();
     43                        }
     44
     45                        // happens at ready
     46                        if (!height) {
     47                                headerImageData = api.get().header_image_data;
     48
     49                                if (headerImageData && headerImageData.width && headerImageData.height) {
     50                                        // hardcoded container width
     51                                        height = 260 / headerImageData.width * headerImageData.height;
     52                                }
     53                                else {
     54                                        // fallback for when no image is set
     55                                        height = 40;
     56                                }
     57                        }
     58
     59                        return height;
     60                },
     61
     62                setPlaceholder: function(_height) {
     63                        var height = _height || this.getHeight();
     64                        this.model.set('savedHeight', height);
     65                        this.$el
     66                                .add(this.$el.find('.placeholder'))
     67                                .height(height);
     68                },
     69
     70                setButtons: function() {
     71                        var elements = $('.actions .remove');
     72                        if (this.model.get('choice')) {
     73                                elements.show();
     74                        } else {
     75                                elements.hide();
     76                        }
     77                }
     78        });
     79
     80
     81        /**
     82         * wp.customize.HeaderTool.ChoiceView
     83         *
     84         * Represents a choosable header image, be it user-uploaded,
     85         * theme-suggested or a special Randomize choice.
     86         *
     87         * Takes a wp.customize.HeaderTool.ImageModel.
     88         *
     89         * Manually changes model wp.customize.HeaderTool.currentHeader via the
     90         * `select` method.
     91         *
     92         * @constructor
     93         * @augments Backbone.View
     94         */
     95        (function () { // closures FTW
     96        var lastHeight = 0;
     97        api.HeaderTool.ChoiceView = Backbone.View.extend({
     98                template: _.template($('#tmpl-header-choice').html()),
     99
     100                className: 'header-view',
     101
     102                events: {
     103                        'click .choice,.random': 'select',
     104                        'click .close': 'removeImage'
     105                },
     106
     107                initialize: function() {
     108                        var properties = [
     109                                this.model.get('header').url,
     110                                this.model.get('choice')
     111                        ];
     112
     113                        this.listenTo(this.model, 'change', this.render);
     114
     115                        if (_.contains(properties, api.get().header_image)) {
     116                                api.HeaderTool.currentHeader.set(this.extendedModel());
     117                        }
     118                },
     119
     120                render: function() {
     121                        var model = this.model;
     122
     123                        this.$el.html(this.template(this.extendedModel()));
     124
     125                        if (model.get('random')) {
     126                                this.setPlaceholder(40);
     127                        }
     128                        else {
     129                                lastHeight = this.getHeight();
     130                        }
     131
     132                        this.$el.toggleClass('hidden', model.get('hidden'));
     133                        return this;
     134                },
     135
     136                extendedModel: function() {
     137                        var c = this.model.get('collection'),
     138                                t = _wpCustomizeHeader.l10n[c.type] || '';
     139
     140                        return _.extend(this.model.toJSON(), {
     141                                // -1 to exclude the randomize button
     142                                nImages: c.size() - 1,
     143                                type: t
     144                        });
     145                },
     146
     147                getHeight: api.HeaderTool.CurrentView.prototype.getHeight,
     148
     149                setPlaceholder: api.HeaderTool.CurrentView.prototype.setPlaceholder,
     150
     151                select: function() {
     152                        this.model.save();
     153                        api.HeaderTool.currentHeader.set(this.extendedModel());
     154                },
     155
     156                removeImage: function(e) {
     157                        e.stopPropagation();
     158                        this.model.destroy();
     159                        this.remove();
     160                }
     161        });
     162        })();
     163
     164
     165        /**
     166         * wp.customize.HeaderTool.ChoiceListView
     167         *
     168         * A container for ChoiceViews. These choices should be of one same type:
     169         * user-uploaded headers or theme-defined ones.
     170         *
     171         * Takes a wp.customize.HeaderTool.ChoiceList.
     172         *
     173         * @constructor
     174         * @augments Backbone.View
     175         */
     176        api.HeaderTool.ChoiceListView = Backbone.View.extend({
     177                initialize: function() {
     178                        this.listenTo(this.collection, 'add', this.addOne);
     179                        this.listenTo(this.collection, 'remove', this.render);
     180                        this.listenTo(this.collection, 'sort', this.render);
     181                        this.listenTo(this.collection, 'change:hidden', this.toggleTitle);
     182                        this.listenTo(this.collection, 'change:hidden', this.setMaxListHeight);
     183                        this.render();
     184                },
     185
     186                render: function() {
     187                        this.$el.empty();
     188                        this.collection.each(this.addOne, this);
     189                        this.toggleTitle();
     190                },
     191
     192                addOne: function(choice) {
     193                        var view;
     194                        choice.set({ collection: this.collection });
     195                        view = new api.HeaderTool.ChoiceView({ model: choice });
     196                        this.$el.append(view.render().el);
     197                },
     198
     199                toggleTitle: function() {
     200                        var title = this.$el.parents().prev('.customize-control-title');
     201                        if (this.collection.shouldHideTitle()) {
     202                                title.hide();
     203                        } else {
     204                                title.show();
     205                        }
     206                }
     207        });
     208
     209
     210        /**
     211         * wp.customize.HeaderTool.CombinedList
     212         *
     213         * Aggregates wp.customize.HeaderTool.ChoiceList collections (or any
     214         * Backbone object, really) and acts as a bus to feed them events.
     215         *
     216         * @constructor
     217         * @augments Backbone.View
     218         */
     219        api.HeaderTool.CombinedList = Backbone.View.extend({
     220                initialize: function(collections) {
     221                        this.collections = collections;
     222                        this.on('all', this.propagate, this);
     223                },
     224                propagate: function(event, arg) {
     225                        _.each(this.collections, function(collection) {
     226                                collection.trigger(event, arg);
     227                        });
     228                }
     229        });
     230
     231})( jQuery, this.wp, _ );
  • src/wp-includes/script-loader.php

    diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
    index a098b19..97e1501 100644
    function wp_default_scripts( &$scripts ) { 
    381381                'allowedFiles' => __( 'Allowed Files' ),
    382382        ) );
    383383
     384        $scripts->add( 'customize-header-models',  "/wp-includes/js/customize-header-models.js",  array( 'underscore', 'backbone' ), false, 1 );
     385        $scripts->add( 'customize-header-views',  "/wp-includes/js/customize-header-views.js",  array( 'jquery', 'underscore', 'imgareaselect', 'customize-header-models' ), false, 1 );
     386
    384387        $scripts->add( 'accordion', "/wp-admin/js/accordion$suffix.js", array( 'jquery' ), false, 1 );
    385388
    386389        $scripts->add( 'shortcode', "/wp-includes/js/shortcode$suffix.js", array( 'underscore' ), false, 1 );
    function wp_default_styles( &$styles ) { 
    602605        $styles->add( 'login',              "/wp-admin/css/login$suffix.css", array( 'buttons', 'open-sans', 'dashicons' ) );
    603606        $styles->add( 'install',            "/wp-admin/css/install$suffix.css", array( 'buttons', 'open-sans' ) );
    604607        $styles->add( 'wp-color-picker',    "/wp-admin/css/color-picker$suffix.css" );
    605         $styles->add( 'customize-controls', "/wp-admin/css/customize-controls$suffix.css", array( 'wp-admin', 'colors', 'ie' ) );
     608        $styles->add( 'customize-controls', "/wp-admin/css/customize-controls$suffix.css", array( 'wp-admin', 'colors', 'ie', 'imgareaselect' ) );
    606609        $styles->add( 'ie',                 "/wp-admin/css/ie$suffix.css" );
    607610
    608611        $styles->add_data( 'ie', 'conditional', 'lte IE 7' );