WordPress.org

Make WordPress Core

Ticket #21483: 21483.diff

File 21483.diff, 18.0 KB (added by celloexpressions, 6 years ago)

First-pass, slightly more than a proof-of-concept. JS portions partially based on work from @mattwiebe. Needs back-compat for subclasses, UI for previewing media in the generic upload control, i18n, coding standards/cleanup, docs, and UI/UX feedback.

  • src/wp-admin/css/customize-controls.css

     
    1010        font-size: 14px;
    1111}
    1212
     13#customize-controls img {
     14        max-width: 100%;
     15}
     16
    1317#customize-controls .submit {
    1418        text-align: center;
    1519}
     
    414418        margin-right: 5px;
    415419}
    416420
     421.customize-control-upload .remove-file,
     422.customize-control-image .remove-file,
     423.customize-control-background_image .remove-file {
     424        margin-left: 15px;
     425        vertical-align: middle;
     426}
     427
     428.customize-control-upload .file-preview,
     429.customize-control-image .file-preview,
     430.customize-control-background_image .file-preview {
     431        margin-bottom: 6px;
     432}
     433
     434.customize-control-upload .action-container,
     435.customize-control-image .action-container,
     436.customize-control-background_image .action-container {
     437        margin-bottom: 12px;
     438}
     439
    417440#customize-preview iframe {
    418441        width: 100%;
    419442        height: 100%;
  • src/wp-admin/js/customize-controls.js

     
    179179         * @augments wp.customize.Class
    180180         */
    181181        api.UploadControl = api.Control.extend({
     182
     183                /**
     184                 * Setup control and do event bindings.
     185                 */
    182186                ready: function() {
    183                         var control = this;
     187                        // this.container is a jQuery object of the container element.
    184188
    185                         this.params.removed = this.params.removed || '';
     189                        // Set up preview, preview container, and button elements. Cache for re-use.
     190                        this.$previewContainer = $( '<div class="file-preview"></div> ' ).appendTo( this.container );
     191                        this.$preview = $( '<p></p>' ).prependTo( this.$previewContainer ); // @todo actual previews, and a better way to override in subclasses.
     192                        this.$actionContainer = $( '<div class="action-container"></div>' ).appendTo( this.container );
     193                        this.$button = $( '<a class="button upload-button" href="#">' + 'Select File' + '</a>' ).appendTo( this.$actionContainer ); // @todo i18n
     194                        this.$removeButton = $( '<a href="#" class="remove-file">' + 'Remove' + '</a>' ).appendTo( this.$actionContainer ); // @todo i18n
    186195
    187                         this.success = $.proxy( this.success, this );
     196                        // Shortcut so that we don't have to use _.bind every time we add a callback.
     197                        _.bindAll( this, 'removeFile', 'upload', 'render', 'select' );
    188198
    189                         this.uploader = $.extend({
    190                                 container: this.container,
    191                                 browser:   this.container.find('.upload'),
    192                                 dropzone:  this.container.find('.upload-dropzone'),
    193                                 success:   this.success,
    194                                 plupload:  {},
    195                                 params:    {}
    196                         }, this.uploader || {} );
     199                        // Ensure clicking "remove image" removes the image.
     200                        this.$removeButton.on( 'click', this.removeFile );
    197201
    198                         if ( control.params.extensions ) {
    199                                 control.uploader.plupload.filters = [{
    200                                         title:      api.l10n.allowedFiles,
    201                                         extensions: control.params.extensions
    202                                 }];
     202                        // Bind upload button
     203                        this.$button.on( 'click', this.upload );
     204
     205                        // Call render method whenever setting is changed.
     206                        this.setting.bind( 'change', this.render );
     207
     208                        // Do initial rendering.
     209                        this.render();
     210                },
     211
     212                /**
     213                 * Remember that _.bind was used to maintain `this` as the control
     214                 * object rather than the usual jQuery way of binding to the DOM element.
     215                 */
     216                upload: function( event ) {
     217                        event.preventDefault();
     218
     219                        if ( ! this.frame ) {
     220                                this.initFrame();
    203221                        }
    204222
    205                         if ( control.params.context )
    206                                 control.uploader.params['post_data[context]'] = this.params.context;
     223                        this.frame.open();
     224                },
    207225
    208                         if ( api.settings.theme.stylesheet )
    209                                 control.uploader.params['post_data[theme]'] = api.settings.theme.stylesheet;
     226                /**
     227                 * Set the media frame so that it can be reused and accessed when needed.
     228                 */
     229                initFrame: function() {
     230                        this.frame = wp.media({
     231                                // The title of the media modal
     232                                title: 'Select File', // @todo i18n
    210233
    211                         this.uploader = new wp.Uploader( this.uploader );
     234                                // Restrict to specified mime type.
     235                                // @todo try to map $extensions to this in PHP.
     236                                library: {
     237                                        type: this.params.mime_type
     238                                },
    212239
    213                         this.remover = this.container.find('.remove');
    214                         this.remover.on( 'click keydown', function( event ) {
    215                                 if ( event.type === 'keydown' &&  13 !== event.which ) // enter
    216                                         return;
     240                                button: {
     241                                        // Change the submit button label.
     242                                        text: 'Choose File'
     243                                },
    217244
    218                                 control.setting.set( control.params.removed );
    219                                 event.preventDefault();
     245                                multiple: false
    220246                        });
    221247
    222                         this.removerVisibility = $.proxy( this.removerVisibility, this );
    223                         this.setting.bind( this.removerVisibility );
    224                         this.removerVisibility( this.setting.get() );
     248                        // When a file is selected, run a callback.
     249                        this.frame.on( 'select', this.select );
    225250                },
    226                 success: function( attachment ) {
    227                         this.setting.set( attachment.get('url') );
     251
     252                /**
     253                 * Fired when an image is selected in the media modal. Gets the selected
     254                 * image information, and sets it within the control.
     255                 */
     256                select: function() {
     257                        // Get the attachment from the modal frame.
     258                        var attachment = this.frame.state().get( 'selection' ).first().toJSON();
     259                       
     260                        // Set the Customizer setting; the callback takes care of rendering.
     261                        this.setting( attachment.url );
    228262                },
    229                 removerVisibility: function( to ) {
    230                         this.remover.toggle( to != this.params.removed );
     263
     264                /**
     265                 * Called on init and whenever a setting is changed.
     266                 *
     267                 * @todo figure out the best way to render by type.
     268                 */
     269                render: function() {
     270                        var value = this.setting();
     271                        if ( value ) {
     272                                // Show a preview of some sort (just the plaintext URL for now).
     273                                this.$preview.text( value );
     274                                this.$previewContainer.show();
     275                                this.$button.text( 'Change File' );
     276                                this.$removeButton.show();
     277                        } else {
     278                                this.$button.text( 'Select File' );
     279                                this.$removeButton.hide();
     280                                this.$previewContainer.hide();
     281                        }
     282                },
     283
     284                // @todo handle the previewing part here instead of render, to facilitate subclasses?
     285                preview: function() {
     286                       
     287                },
     288
     289                /**
     290                 * Called when the "Remove" link is clicked. Empties the setting.
     291                 * @param {object} event jQuery Event object from click event
     292                 */
     293                removeFile: function( event ) {
     294                        event.preventDefault();
     295                        this.setting( '' );
    231296                }
    232297        });
    233298
     
    238303         * @augments wp.customize.Class
    239304         */
    240305        api.ImageControl = api.UploadControl.extend({
    241                 ready: function() {
    242                         var control = this,
    243                                 panels;
     306               
     307                /**
     308                 * Set the media frame so that it can be reused and accessed when needed.
     309                 */
     310                initFrame: function() {
     311                        this.frame = wp.media({
     312                                // The title of the media modal
     313                                title: 'Select Image', // @todo i18n
    244314
    245                         this.uploader = {
    246                                 init: function() {
    247                                         var fallback, button;
     315                                // Restrict to specified mime type.
     316                                // @todo try to map $extensions to this in PHP.
     317                                library: {
     318                                        type: this.params.mime_type
     319                                },
    248320
    249                                         if ( this.supports.dragdrop )
    250                                                 return;
     321                                button: {
     322                                        // Change the submit button label.
     323                                        text: 'Choose Image'
     324                                },
    251325
    252                                         // Maintain references while wrapping the fallback button.
    253                                         fallback = control.container.find( '.upload-fallback' );
    254                                         button   = fallback.children().detach();
    255 
    256                                         this.browser.detach().empty().append( button );
    257                                         fallback.append( this.browser ).show();
    258                                 }
    259                         };
    260 
    261                         api.UploadControl.prototype.ready.call( this );
    262 
    263                         this.thumbnail    = this.container.find('.preview-thumbnail img');
    264                         this.thumbnailSrc = $.proxy( this.thumbnailSrc, this );
    265                         this.setting.bind( this.thumbnailSrc );
    266 
    267                         this.library = this.container.find('.library');
    268 
    269                         // Generate tab objects
    270                         this.tabs = {};
    271                         panels    = this.library.find('.library-content');
    272 
    273                         this.library.children('ul').children('li').each( function() {
    274                                 var link  = $(this),
    275                                         id    = link.data('customizeTab'),
    276                                         panel = panels.filter('[data-customize-tab="' + id + '"]');
    277 
    278                                 control.tabs[ id ] = {
    279                                         both:  link.add( panel ),
    280                                         link:  link,
    281                                         panel: panel
    282                                 };
     326                                multiple: false
    283327                        });
    284328
    285                         // Bind tab switch events
    286                         this.library.children('ul').on( 'click keydown', 'li', function( event ) {
    287                                 if ( event.type === 'keydown' &&  13 !== event.which ) // enter
    288                                         return;
     329                        // When a file is selected, run a callback.
     330                        this.frame.on( 'select', this.select );
     331                },
    289332
    290                                 var id  = $(this).data('customizeTab'),
    291                                         tab = control.tabs[ id ];
    292 
    293                                 event.preventDefault();
    294 
    295                                 if ( tab.link.hasClass('library-selected') )
    296                                         return;
    297 
    298                                 control.selected.both.removeClass('library-selected');
    299                                 control.selected = tab;
    300                                 control.selected.both.addClass('library-selected');
    301                         });
    302 
    303                         // Bind events to switch image urls.
    304                         this.library.on( 'click keydown', 'a', function( event ) {
    305                                 if ( event.type === 'keydown' && 13 !== event.which ) // enter
    306                                         return;
    307 
    308                                 var value = $(this).data('customizeImageValue');
    309 
    310                                 if ( value ) {
    311                                         control.setting.set( value );
    312                                         event.preventDefault();
     333                render: function() {
     334                        var value = this.setting();
     335                        if ( value ) {
     336                                // Maybe setup preview.
     337                                if ( ! this.$img ) {
     338                                        this.$previewContainer.html( '' );
     339                                        this.$img = $( '<img class="preview-image" >' ).appendTo( this.$previewContainer );
    313340                                }
    314                         });
    315341
    316                         if ( this.tabs.uploaded ) {
    317                                 this.tabs.uploaded.target = this.library.find('.uploaded-target');
    318                                 if ( ! this.tabs.uploaded.panel.find('.thumbnail').length )
    319                                         this.tabs.uploaded.both.addClass('hidden');
     342                                // Show a preview thumbnail.
     343                                this.$img.attr( 'src', value );
     344                                this.$previewContainer.show();
     345                                this.$button.text( 'Change Image' );
     346                                this.$removeButton.show();
     347                        } else {
     348                                // Hide thumbnail, show button
     349                                this.$button.text( 'Select Image' );
     350                                this.$removeButton.hide();
     351                                this.$previewContainer.hide();
    320352                        }
    321 
    322                         // Select a tab
    323                         panels.each( function() {
    324                                 var tab = control.tabs[ $(this).data('customizeTab') ];
    325 
    326                                 // Select the first visible tab.
    327                                 if ( ! tab.link.hasClass('hidden') ) {
    328                                         control.selected = tab;
    329                                         tab.both.addClass('library-selected');
    330                                         return false;
    331                                 }
    332                         });
    333 
    334                         this.dropdownInit();
    335                 },
    336                 success: function( attachment ) {
    337                         api.UploadControl.prototype.success.call( this, attachment );
    338 
    339                         // Add the uploaded image to the uploaded tab.
    340                         if ( this.tabs.uploaded && this.tabs.uploaded.target.length ) {
    341                                 this.tabs.uploaded.both.removeClass('hidden');
    342 
    343                                 // @todo: Do NOT store this on the attachment model. That is bad.
    344                                 attachment.element = $( '<a href="#" class="thumbnail"></a>' )
    345                                         .data( 'customizeImageValue', attachment.get('url') )
    346                                         .append( '<img src="' +  attachment.get('url')+ '" />' )
    347                                         .appendTo( this.tabs.uploaded.target );
    348                         }
    349                 },
    350                 thumbnailSrc: function( to ) {
    351                         if ( /^(https?:)?\/\//.test( to ) )
    352                                 this.thumbnail.prop( 'src', to ).show();
    353                         else
    354                                 this.thumbnail.hide();
    355353                }
    356354        });
    357355
  • src/wp-includes/class-wp-customize-control.php

     
    541541 */
    542542class WP_Customize_Upload_Control extends WP_Customize_Control {
    543543        public $type    = 'upload';
    544         public $removed = '';
    545         public $context;
    546         public $extensions = array();
     544        public $mime_type = '';
     545        public $removed = ''; // unused
     546        public $context; // unused
     547        public $extensions = array(); // unused @todo auto-convert to mime_type and/or enforce in media library.
    547548
    548549        /**
    549550         * Enqueue control related scripts/styles.
     
    551552         * @since 3.4.0
    552553         */
    553554        public function enqueue() {
    554                 wp_enqueue_script( 'wp-plupload' );
     555                wp_enqueue_media();
    555556        }
    556557
    557558        /**
     
    563564        public function to_json() {
    564565                parent::to_json();
    565566
    566                 $this->json['removed'] = $this->removed;
    567 
    568                 if ( $this->context )
    569                         $this->json['context'] = $this->context;
    570 
    571                 if ( $this->extensions )
    572                         $this->json['extensions'] = implode( ',', $this->extensions );
     567                $this->json['mime_type'] = $this->mime_type;
    573568        }
    574569
    575570        /**
     
    579574         */
    580575        public function render_content() {
    581576                ?>
    582                 <label>
    583                         <?php if ( ! empty( $this->label ) ) : ?>
    584                                 <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
    585                         <?php endif;
    586                         if ( ! empty( $this->description ) ) : ?>
    587                                 <span class="description customize-control-description"><?php echo $this->description; ?></span>
    588                         <?php endif; ?>
    589                         <div>
    590                                 <a href="#" class="button-secondary upload"><?php _e( 'Upload' ); ?></a>
    591                                 <a href="#" class="remove"><?php _e( 'Remove' ); ?></a>
    592                         </div>
    593                 </label>
     577                <?php if ( ! empty( $this->label ) ) : ?>
     578                        <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
     579                <?php endif;
     580                if ( ! empty( $this->description ) ) : ?>
     581                        <span class="description customize-control-description"><?php echo $this->description; ?></span>
     582                <?php endif; ?>
     583                <?php // the rest is rendered with JS ?>
    594584                <?php
    595585        }
    596586}
     
    604594 */
    605595class WP_Customize_Image_Control extends WP_Customize_Upload_Control {
    606596        public $type = 'image';
    607         public $get_url;
    608         public $statuses;
    609         public $extensions = array( 'jpg', 'jpeg', 'gif', 'png' );
    610 
    611         protected $tabs = array();
    612 
    613         /**
    614          * Constructor.
    615          *
    616          * @since 3.4.0
    617          * @uses WP_Customize_Upload_Control::__construct()
    618          *
    619          * @param WP_Customize_Manager $manager
    620          * @param string $id
    621          * @param array $args
    622          */
    623         public function __construct( $manager, $id, $args ) {
    624                 $this->statuses = array( '' => __('No Image') );
    625 
    626                 parent::__construct( $manager, $id, $args );
    627 
    628                 $this->add_tab( 'upload-new', __('Upload New'), array( $this, 'tab_upload_new' ) );
    629                 $this->add_tab( 'uploaded',   __('Uploaded'),   array( $this, 'tab_uploaded' ) );
    630 
    631                 // Early priority to occur before $this->manager->prepare_controls();
    632                 add_action( 'customize_controls_init', array( $this, 'prepare_control' ), 5 );
    633         }
    634 
    635         /**
    636          * Prepares the control.
    637          *
    638          * If no tabs exist, removes the control from the manager.
    639          *
    640          * @since 3.4.2
    641          */
    642         public function prepare_control() {
    643                 if ( ! $this->tabs )
    644                         $this->manager->remove_control( $this->id );
    645         }
    646 
    647         /**
    648          * Refresh the parameters passed to the JavaScript via JSON.
    649          *
    650          * @since 3.4.0
    651          * @uses WP_Customize_Upload_Control::to_json()
    652          */
    653         public function to_json() {
    654                 parent::to_json();
    655                 $this->json['statuses'] = $this->statuses;
    656         }
    657 
    658         /**
    659          * Render the control's content.
    660          *
    661          * @since 3.4.0
    662          */
    663         public function render_content() {
    664                 $src = $this->value();
    665                 if ( isset( $this->get_url ) )
    666                         $src = call_user_func( $this->get_url, $src );
    667 
    668                 ?>
    669                 <div class="customize-image-picker">
    670                         <?php if ( ! empty( $this->label ) ) : ?>
    671                                 <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
    672                         <?php endif;
    673                         if ( ! empty( $this->description ) ) : ?>
    674                                 <span class="description customize-control-description"><?php echo $this->description; ?></span>
    675                         <?php endif; ?>
    676 
    677                         <div class="customize-control-content">
    678                                 <div class="dropdown preview-thumbnail" tabindex="0">
    679                                         <div class="dropdown-content">
    680                                                 <?php if ( empty( $src ) ): ?>
    681                                                         <img style="display:none;" />
    682                                                 <?php else: ?>
    683                                                         <img src="<?php echo esc_url( set_url_scheme( $src ) ); ?>" />
    684                                                 <?php endif; ?>
    685                                                 <div class="dropdown-status"></div>
    686                                         </div>
    687                                         <div class="dropdown-arrow"></div>
    688                                 </div>
    689                         </div>
    690 
    691                         <div class="library">
    692                                 <ul>
    693                                         <?php foreach ( $this->tabs as $id => $tab ): ?>
    694                                                 <li data-customize-tab='<?php echo esc_attr( $id ); ?>' tabindex='0'>
    695                                                         <?php echo esc_html( $tab['label'] ); ?>
    696                                                 </li>
    697                                         <?php endforeach; ?>
    698                                 </ul>
    699                                 <?php foreach ( $this->tabs as $id => $tab ): ?>
    700                                         <div class="library-content" data-customize-tab='<?php echo esc_attr( $id ); ?>'>
    701                                                 <?php call_user_func( $tab['callback'] ); ?>
    702                                         </div>
    703                                 <?php endforeach; ?>
    704                         </div>
    705 
    706                         <div class="actions">
    707                                 <a href="#" class="remove"><?php _e( 'Remove Image' ); ?></a>
    708                         </div>
    709                 </div>
    710                 <?php
    711         }
    712 
    713         /**
    714          * Add a tab to the control.
    715          *
    716          * @since 3.4.0
    717          *
    718          * @param string $id
    719          * @param string $label
    720          * @param mixed $callback
    721          */
    722         public function add_tab( $id, $label, $callback ) {
    723                 $this->tabs[ $id ] = array(
    724                         'label'    => $label,
    725                         'callback' => $callback,
    726                 );
    727         }
    728 
    729         /**
    730          * Remove a tab from the control.
    731          *
    732          * @since 3.4.0
    733          *
    734          * @param string $id
    735          */
    736         public function remove_tab( $id ) {
    737                 unset( $this->tabs[ $id ] );
    738         }
    739 
    740         /**
    741          * @since 3.4.0
    742          */
    743         public function tab_upload_new() {
    744                 if ( ! _device_can_upload() ) {
    745                         echo '<p>' . sprintf( __('The web browser on your device cannot be used to upload files. You may be able to use the <a href="%s">native app for your device</a> instead.'), 'http://apps.wordpress.org/' ) . '</p>';
    746                 } else {
    747                         ?>
    748                         <div class="upload-dropzone">
    749                                 <?php _e('Drop a file here or <a href="#" class="upload">select a file</a>.'); ?>
    750                         </div>
    751                         <div class="upload-fallback">
    752                                 <span class="button-secondary"><?php _e('Select File'); ?></span>
    753                         </div>
    754                         <?php
    755                 }
    756         }
    757 
    758         /**
    759          * @since 3.4.0
    760          */
    761         public function tab_uploaded() {
    762                 ?>
    763                 <div class="uploaded-target"></div>
    764                 <?php
    765         }
    766 
    767         /**
    768          * @since 3.4.0
    769          *
    770          * @param string $url
    771          * @param string $thumbnail_url
    772          */
    773         public function print_tab_image( $url, $thumbnail_url = null ) {
    774                 $url = set_url_scheme( $url );
    775                 $thumbnail_url = ( $thumbnail_url ) ? set_url_scheme( $thumbnail_url ) : $url;
    776                 ?>
    777                 <a href="#" class="thumbnail" data-customize-image-value="<?php echo esc_url( $url ); ?>">
    778                         <img src="<?php echo esc_url( $thumbnail_url ); ?>" />
    779                 </a>
    780                 <?php
    781         }
     597        public $mime_type = 'image';
    782598}
    783599
    784600/**
     
    806622                        'get_url'  => 'get_background_image',
    807623                ) );
    808624
    809                 if ( $this->setting->default )
    810                         $this->add_tab( 'default',  __('Default'),  array( $this, 'tab_default_background' ) );
     625                if ( $this->setting->default ) {
     626                        // @todo
     627                }
    811628        }
    812629
    813630        /**