WordPress.org

Make WordPress Core

Ticket #41914: 41914.2.diff

File 41914.2.diff, 39.2 KB (added by westonruter, 9 months ago)

https://github.com/xwp/wordpress-develop/pull/265/files/2562ac1..79890bd

  • src/wp-admin/css/widgets.css

    diff --git src/wp-admin/css/widgets.css src/wp-admin/css/widgets.css
    index f31b63c881..11bf7e5afd 100644
     
    8787.media-widget-control .placeholder {
    8888        border: 1px dashed #b4b9be;
    8989        box-sizing: border-box;
    90         cursor: default;
     90        cursor: pointer;
    9191        line-height: 20px;
    9292        padding: 9px 0;
    9393        position: relative;
     
    162162        margin: 1em 0;
    163163}
    164164
     165.media-widget-gallery-preview {
     166        display: flex;
     167        justify-content: flex-start;
     168        flex-wrap: wrap;
     169}
     170
     171.media-widget-preview.media_gallery,
     172.media-widget-preview.media_image {
     173        cursor: pointer;
     174}
     175
     176.media-widget-gallery-preview .gallery-item {
     177        box-sizing: border-box;
     178        width: 50%;
     179        margin: 0;
     180        padding: 1.79104477%;
     181}
     182
     183/*
     184 * Use targeted nth-last-child selectors to control the size of each image
     185 * based on how many gallery items are present in the grid.
     186 * See: https://alistapart.com/article/quantity-queries-for-css
     187 */
     188.media-widget-gallery-preview .gallery-item:nth-last-child(3):first-child,
     189.media-widget-gallery-preview .gallery-item:nth-last-child(3):first-child ~ .gallery-item,
     190.media-widget-gallery-preview .gallery-item:nth-last-child(n+5),
     191.media-widget-gallery-preview .gallery-item:nth-last-child(n+5) ~ .gallery-item,
     192.media-widget-gallery-preview .gallery-item:nth-last-child(n+6),
     193.media-widget-gallery-preview .gallery-item:nth-last-child(n+6) ~ .gallery-item {
     194        max-width: 33.33%;
     195}
     196
     197.media-widget-gallery-preview .gallery-item img {
     198        height: auto;
     199        vertical-align: bottom;
     200}
     201
     202.media-widget-gallery-preview .gallery-icon {
     203        position: relative;
     204}
     205
     206.media-widget-gallery-preview .gallery-icon-placeholder {
     207        position: absolute;
     208        top: 0;
     209        bottom: 0;
     210        width: 100%;
     211        box-sizing: border-box;
     212        display: flex;
     213        align-items: center;
     214        justify-content: center;
     215        background-color: rgba( 0, 0, 0, .5 );
     216}
     217
     218.media-widget-gallery-preview .gallery-icon-placeholder-text {
     219        font-weight: 600;
     220        font-size: 2em;
     221        color: white;
     222}
     223
     224
    165225/* Widget Dragging Helpers */
    166226.widget.ui-draggable-dragging {
    167227        min-width: 100%;
  • new file src/wp-admin/js/widgets/media-gallery-widget.js

    diff --git src/wp-admin/js/widgets/media-gallery-widget.js src/wp-admin/js/widgets/media-gallery-widget.js
    new file mode 100644
    index 0000000000..f569968e89
    - +  
     1/* eslint consistent-this: [ "error", "control" ] */
     2(function( component ) {
     3        'use strict';
     4
     5        var GalleryWidgetModel, GalleryWidgetControl, GalleryDetailsMediaFrame;
     6
     7        /**
     8         * Custom gallery details frame.
     9         *
     10         * @since 4.9.0
     11         * @class GalleryDetailsMediaFrame
     12         * @constructor
     13         */
     14        GalleryDetailsMediaFrame = wp.media.view.MediaFrame.Post.extend( {
     15
     16                /**
     17                 * Create the default states.
     18                 *
     19                 * @since 4.9.0
     20                 * @returns {void}
     21                 */
     22                createStates: function createStates() {
     23                        this.states.add([
     24                                new wp.media.controller.Library({
     25                                        id:         'gallery',
     26                                        title:      wp.media.view.l10n.createGalleryTitle,
     27                                        priority:   40,
     28                                        toolbar:    'main-gallery',
     29                                        filterable: 'uploaded',
     30                                        multiple:   'add',
     31                                        editable:   true,
     32
     33                                        library:  wp.media.query( _.defaults({
     34                                                type: 'image'
     35                                        }, this.options.library ) )
     36                                }),
     37
     38                                // Gallery states.
     39                                new wp.media.controller.GalleryEdit({
     40                                        library: this.options.selection,
     41                                        editing: this.options.editing,
     42                                        menu:    'gallery'
     43                                }),
     44
     45                                new wp.media.controller.GalleryAdd()
     46                        ]);
     47                }
     48        } );
     49
     50        /**
     51         * Gallery widget model.
     52         *
     53         * See WP_Widget_Gallery::enqueue_admin_scripts() for amending prototype from PHP exports.
     54         *
     55         * @since 4.9.0
     56         * @class GalleryWidgetModel
     57         * @constructor
     58         */
     59        GalleryWidgetModel = component.MediaWidgetModel.extend( {} );
     60
     61        /**
     62         * Gallery widget control.
     63         *
     64         * See WP_Widget_Gallery::enqueue_admin_scripts() for amending prototype from PHP exports.
     65         *
     66         * @since 4.9.0
     67         * @class GalleryWidgetControl
     68         * @constructor
     69         */
     70        GalleryWidgetControl = component.MediaWidgetControl.extend( {
     71
     72                /**
     73                 * View events.
     74                 *
     75                 * @since 4.9.0
     76                 * @type {object}
     77                 */
     78                events: _.extend( {}, component.MediaWidgetControl.prototype.events, {
     79                        'click .media-widget-gallery-preview': 'editMedia'
     80                } ),
     81
     82                /**
     83                 * Initialize.
     84                 *
     85                 * @since 4.9.0
     86                 * @param {Object}         options - Options.
     87                 * @param {Backbone.Model} options.model - Model.
     88                 * @param {jQuery}         options.el - Control field container element.
     89                 * @param {jQuery}         options.syncContainer - Container element where fields are synced for the server.
     90                 * @returns {void}
     91                 */
     92                initialize: function initialize( options ) {
     93                        var control = this;
     94
     95                        component.MediaWidgetControl.prototype.initialize.call( control, options );
     96
     97                        _.bindAll( control, 'updateSelectedAttachments', 'handleAttachmentDestroy' );
     98                        control.selectedAttachments = new wp.media.model.Attachments();
     99                        control.model.on( 'change:ids', control.updateSelectedAttachments );
     100                        control.selectedAttachments.on( 'change', control.renderPreview );
     101                        control.selectedAttachments.on( 'reset', control.renderPreview );
     102                        control.updateSelectedAttachments();
     103                },
     104
     105                /**
     106                 * Update the selected attachments if necessary.
     107                 *
     108                 * @since 4.9.0
     109                 * @returns {void}
     110                 */
     111                updateSelectedAttachments: function updateSelectedAttachments() {
     112                        var control = this, newIds, oldIds, removedIds, addedIds, addedQuery;
     113
     114                        newIds = control.model.get( 'ids' );
     115                        oldIds = _.pluck( control.selectedAttachments.models, 'id' );
     116
     117                        removedIds = _.difference( oldIds, newIds );
     118                        _.each( removedIds, function( removedId ) {
     119                                control.selectedAttachments.remove( control.selectedAttachments.get( removedId ) );
     120                        });
     121
     122                        addedIds = _.difference( newIds, oldIds );
     123                        if ( addedIds.length ) {
     124                                addedQuery = wp.media.query({
     125                                        order: 'ASC',
     126                                        orderby: 'post__in',
     127                                        perPage: -1,
     128                                        post__in: newIds,
     129                                        query: true,
     130                                        type: 'image'
     131                                });
     132                                addedQuery.more().done( function() {
     133                                        control.selectedAttachments.reset( addedQuery.models );
     134                                });
     135                        }
     136                },
     137
     138                /**
     139                 * Render preview.
     140                 *
     141                 * @since 4.9.0
     142                 * @returns {void}
     143                 */
     144                renderPreview: function renderPreview() {
     145                        var control = this, previewContainer, previewTemplate, data;
     146
     147                        previewContainer = control.$el.find( '.media-widget-preview' );
     148                        previewTemplate = wp.template( 'wp-media-widget-gallery-preview' );
     149
     150                        data = control.previewTemplateProps.toJSON();
     151                        data.attachments = {};
     152                        control.selectedAttachments.each( function( attachment ) {
     153                                data.attachments[ attachment.id ] = attachment.toJSON();
     154                        } );
     155
     156                        previewContainer.html( previewTemplate( data ) );
     157                },
     158
     159                /**
     160                 * Determine whether there are selected attachments.
     161                 *
     162                 * @since 4.9.0
     163                 * @returns {boolean} Selected.
     164                 */
     165                isSelected: function isSelected() {
     166                        var control = this;
     167
     168                        if ( control.model.get( 'error' ) ) {
     169                                return false;
     170                        }
     171
     172                        return control.model.get( 'ids' ).length > 0;
     173                },
     174
     175                /**
     176                 * Open the media select frame to edit images.
     177                 *
     178                 * @since 4.9.0
     179                 * @returns {void}
     180                 */
     181                editMedia: function editMedia() {
     182                        var control = this, selection, mediaFrame, mediaFrameProps;
     183
     184                        selection = new wp.media.model.Selection( control.selectedAttachments.models, {
     185                                multiple: true
     186                        });
     187
     188                        mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() );
     189                        selection.gallery = new Backbone.Model( _.pick( mediaFrameProps, 'columns', 'link', 'size', '_orderbyRandom' ) );
     190                        if ( mediaFrameProps.size ) {
     191                                control.displaySettings.set( 'size', mediaFrameProps.size );
     192                        }
     193                        mediaFrame = new GalleryDetailsMediaFrame({
     194                                frame: 'manage',
     195                                text: control.l10n.add_to_widget,
     196                                selection: selection,
     197                                mimeType: control.mime_type,
     198                                selectedDisplaySettings: control.displaySettings,
     199                                showDisplaySettings: control.showDisplaySettings,
     200                                metadata: mediaFrameProps,
     201                                editing:   true,
     202                                multiple:  true,
     203                                state: 'gallery-edit'
     204                        });
     205                        wp.media.frame = mediaFrame; // See wp.media().
     206
     207                        // Handle selection of a media item.
     208                        mediaFrame.on( 'update', function onUpdate( newSelection ) {
     209                                var state = mediaFrame.state(), resultSelection;
     210
     211                                resultSelection = newSelection || state.get( 'selection' );
     212                                if ( ! resultSelection ) {
     213                                        return;
     214                                }
     215
     216                                // Copy orderby_random from gallery state.
     217                                if ( resultSelection.gallery ) {
     218                                        control.model.set( control.mapMediaToModelProps( resultSelection.gallery.toJSON() ) );
     219                                }
     220
     221                                // Directly update selectedAttachments to prevent needing to do additional request.
     222                                control.selectedAttachments.reset( resultSelection.models );
     223
     224                                // Update models in the widget instance.
     225                                control.model.set( {
     226                                        ids: _.pluck( resultSelection.models, 'id' )
     227                                } );
     228                        } );
     229
     230                        mediaFrame.$el.addClass( 'media-widget' );
     231                        mediaFrame.open();
     232
     233                        if ( selection ) {
     234                                selection.on( 'destroy', control.handleAttachmentDestroy );
     235                        }
     236                },
     237
     238                /**
     239                 * Open the media select frame to chose an item.
     240                 *
     241                 * @since 4.9.0
     242                 * @returns {void}
     243                 */
     244                selectMedia: function selectMedia() {
     245                        var control = this, selection, mediaFrame, mediaFrameProps;
     246                        selection = new wp.media.model.Selection( control.selectedAttachments.models, {
     247                                multiple: true
     248                        });
     249
     250                        mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() );
     251                        if ( mediaFrameProps.size ) {
     252                                control.displaySettings.set( 'size', mediaFrameProps.size );
     253                        }
     254                        mediaFrame = new GalleryDetailsMediaFrame({
     255                                frame: 'select',
     256                                text: control.l10n.add_to_widget,
     257                                selection: selection,
     258                                mimeType: control.mime_type,
     259                                selectedDisplaySettings: control.displaySettings,
     260                                showDisplaySettings: control.showDisplaySettings,
     261                                metadata: mediaFrameProps,
     262                                state: 'gallery'
     263                        });
     264                        wp.media.frame = mediaFrame; // See wp.media().
     265
     266                        // Handle selection of a media item.
     267                        mediaFrame.on( 'update', function onUpdate( newSelection ) {
     268                                var state = mediaFrame.state(), resultSelection;
     269
     270                                resultSelection = newSelection || state.get( 'selection' );
     271                                if ( ! resultSelection ) {
     272                                        return;
     273                                }
     274
     275                                // Copy orderby_random from gallery state.
     276                                if ( resultSelection.gallery ) {
     277                                        control.model.set( control.mapMediaToModelProps( resultSelection.gallery.toJSON() ) );
     278                                }
     279
     280                                // Directly update selectedAttachments to prevent needing to do additional request.
     281                                control.selectedAttachments.reset( resultSelection.models );
     282
     283                                // Update widget instance.
     284                                control.model.set( {
     285                                        ids: _.pluck( resultSelection.models, 'id' )
     286                                } );
     287                        } );
     288
     289                        mediaFrame.$el.addClass( 'media-widget' );
     290                        mediaFrame.open();
     291
     292                        if ( selection ) {
     293                                selection.on( 'destroy', control.handleAttachmentDestroy );
     294                        }
     295
     296                        /*
     297                         * Make sure focus is set inside of modal so that hitting Esc will close
     298                         * the modal and not inadvertently cause the widget to collapse in the customizer.
     299                         */
     300                        mediaFrame.$el.find( ':focusable:first' ).focus();
     301                },
     302
     303                /**
     304                 * Clear the selected attachment when it is deleted in the media select frame.
     305                 *
     306                 * @since 4.9.0
     307                 * @param {wp.media.models.Attachment} attachment - Attachment.
     308                 * @returns {void}
     309                 */
     310                handleAttachmentDestroy: function handleAttachmentDestroy( attachment ) {
     311                        var control = this;
     312                        control.model.set( {
     313                                ids: _.difference(
     314                                        control.model.get( 'ids' ),
     315                                        [ attachment.id ]
     316                                )
     317                        } );
     318                }
     319        } );
     320
     321        // Exports.
     322        component.controlConstructors.media_gallery = GalleryWidgetControl;
     323        component.modelConstructors.media_gallery = GalleryWidgetModel;
     324
     325})( wp.mediaWidgets );
  • src/wp-admin/js/widgets/media-image-widget.js

    diff --git src/wp-admin/js/widgets/media-image-widget.js src/wp-admin/js/widgets/media-image-widget.js
    index ddbe6b3e24..78b257feae 100644
     
    2525        ImageWidgetControl = component.MediaWidgetControl.extend({
    2626
    2727                /**
     28                 * View events.
     29                 *
     30                 * @type {object}
     31                 */
     32                events: _.extend( {}, component.MediaWidgetControl.prototype.events, {
     33                        'click .media-widget-preview.populated': 'editMedia'
     34                } ),
     35
     36                /**
    2837                 * Render preview.
    2938                 *
    3039                 * @returns {void}
     
    3847                        previewContainer = control.$el.find( '.media-widget-preview' );
    3948                        previewTemplate = wp.template( 'wp-media-widget-image-preview' );
    4049                        previewContainer.html( previewTemplate( control.previewTemplateProps.toJSON() ) );
     50                        previewContainer.addClass( 'populated' );
    4151
    4252                        linkInput = control.$el.find( '.link' );
    4353                        if ( ! linkInput.is( document.activeElement ) ) {
  • src/wp-admin/js/widgets/media-widgets.js

    diff --git src/wp-admin/js/widgets/media-widgets.js src/wp-admin/js/widgets/media-widgets.js
    index e3bc41c480..f51379f14e 100644
    wp.mediaWidgets = ( function( $ ) { 
    429429                events: {
    430430                        'click .notice-missing-attachment a': 'handleMediaLibraryLinkClick',
    431431                        'click .select-media': 'selectMedia',
     432                        'click .placeholder': 'selectMedia',
    432433                        'click .edit-media': 'editMedia'
    433434                },
    434435
    wp.mediaWidgets = ( function( $ ) { 
    591592                syncModelToInputs: function syncModelToInputs() {
    592593                        var control = this;
    593594                        control.syncContainer.find( '.media-widget-instance-property' ).each( function() {
    594                                 var input = $( this ), value;
    595                                 value = control.model.get( input.data( 'property' ) );
     595                                var input = $( this ), value, propertyName;
     596                                propertyName = input.data( 'property' );
     597                                value = control.model.get( propertyName );
    596598                                if ( _.isUndefined( value ) ) {
    597599                                        return;
    598600                                }
    599                                 value = String( value );
    600                                 if ( input.val() === value ) {
    601                                         return;
     601
     602                                // @todo Support comma-separated ID list arrays? This will depend on WP_Widget_Media::form() being updated to support serializing array to form field.
     603                                if ( 'array' === control.model.schema[ propertyName ].type && _.isArray( value ) ) {
     604                                        value = value.join( ',' );
     605                                } else if ( 'boolean' === control.model.schema[ propertyName ].type ) {
     606                                        value = value ? '1' : ''; // Because in PHP, strval( true ) === '1' && strval( false ) === ''.
     607                                } else {
     608                                        value = String( value );
     609                                }
     610
     611                                if ( input.val() !== value ) {
     612                                        input.val( value );
     613                                        input.trigger( 'change' );
    602614                                }
    603                                 input.val( value );
    604                                 input.trigger( 'change' );
    605615                        });
    606616                },
    607617
    wp.mediaWidgets = ( function( $ ) { 
    10021012                                        return;
    10031013                                }
    10041014                                type = model.schema[ name ].type;
    1005                                 if ( 'integer' === type ) {
     1015                                if ( 'array' === type ) {
     1016                                        castedAttrs[ name ] = value;
     1017                                        if ( ! _.isArray( castedAttrs[ name ] ) ) {
     1018                                                castedAttrs[ name ] = castedAttrs[ name ].split( /,/ ); // Good enough for parsing an ID list.
     1019                                        }
     1020                                        if ( model.schema[ name ].items && 'integer' === model.schema[ name ].items.type ) {
     1021                                                castedAttrs[ name ] = _.filter(
     1022                                                        _.map( castedAttrs[ name ], function( id ) {
     1023                                                                return parseInt( id, 10 );
     1024                                                        },
     1025                                                        function( id ) {
     1026                                                                return 'number' === typeof id;
     1027                                                        }
     1028                                                ) );
     1029                                        }
     1030                                } else if ( 'integer' === type ) {
    10061031                                        castedAttrs[ name ] = parseInt( value, 10 );
    10071032                                } else if ( 'boolean' === type ) {
    10081033                                        castedAttrs[ name ] = ! ( ! value || '0' === value || 'false' === value );
  • src/wp-includes/default-widgets.php

    diff --git src/wp-includes/default-widgets.php src/wp-includes/default-widgets.php
    index 7c8a903c56..767002b642 100644
    require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-media-image.php' ); 
    3131/** WP_Widget_Media_Video class */
    3232require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-media-video.php' );
    3333
     34/** WP_Widget_Media_Gallery class */
     35require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-media-gallery.php' );
     36
    3437/** WP_Widget_Meta class */
    3538require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-meta.php' );
    3639
  • src/wp-includes/script-loader.php

    diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
    index c85822c807..fa1ada70fb 100644
    function wp_default_scripts( &$scripts ) { 
    699699
    700700                $scripts->add( 'media-audio-widget', "/wp-admin/js/widgets/media-audio-widget$suffix.js", array( 'media-widgets', 'media-audiovideo' ) );
    701701                $scripts->add( 'media-image-widget', "/wp-admin/js/widgets/media-image-widget$suffix.js", array( 'media-widgets' ) );
     702                $scripts->add( 'media-gallery-widget', "/wp-admin/js/widgets/media-gallery-widget$suffix.js", array( 'media-widgets' ) );
    702703                $scripts->add( 'media-video-widget', "/wp-admin/js/widgets/media-video-widget$suffix.js", array( 'media-widgets', 'media-audiovideo', 'wp-api-request' ) );
    703704                $scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util', 'wp-a11y' ) );
    704705                $scripts->add( 'custom-html-widgets', "/wp-admin/js/widgets/custom-html-widgets$suffix.js", array( 'code-editor', 'jquery', 'backbone', 'wp-util', 'jquery-ui-core', 'wp-a11y' ) );
  • src/wp-includes/widgets.php

    diff --git src/wp-includes/widgets.php src/wp-includes/widgets.php
    index fe0e058d9a..e4fcc527b7 100644
    function wp_widgets_init() { 
    16091609
    16101610        register_widget( 'WP_Widget_Media_Image' );
    16111611
     1612        register_widget( 'WP_Widget_Media_Gallery' );
     1613
    16121614        register_widget( 'WP_Widget_Media_Video' );
    16131615
    16141616        register_widget( 'WP_Widget_Meta' );
  • new file src/wp-includes/widgets/class-wp-widget-media-gallery.php

    diff --git src/wp-includes/widgets/class-wp-widget-media-gallery.php src/wp-includes/widgets/class-wp-widget-media-gallery.php
    new file mode 100644
    index 0000000000..a7903b6b67
    - +  
     1<?php
     2/**
     3 * Widget API: WP_Widget_Media_Gallery class
     4 *
     5 * @package WordPress
     6 * @subpackage Widgets
     7 * @since 4.9.0
     8 */
     9
     10/**
     11 * Core class that implements a gallery widget.
     12 *
     13 * @since 4.9.0
     14 *
     15 * @see WP_Widget
     16 */
     17class WP_Widget_Media_Gallery extends WP_Widget_Media {
     18
     19        /**
     20         * Constructor.
     21         *
     22         * @since 4.9.0
     23         */
     24        public function __construct() {
     25                parent::__construct( 'media_gallery', __( 'Gallery' ), array(
     26                        'description' => __( 'Displays an image gallery.' ),
     27                        'mime_type'   => 'image',
     28                ) );
     29
     30                $this->l10n = array_merge( $this->l10n, array(
     31                        'no_media_selected' => __( 'No images selected' ),
     32                        'select_media' => _x( 'Select Images', 'label for button in the gallery widget; should not be longer than ~13 characters long' ),
     33                        'replace_media' => _x( 'Replace Gallery', 'label for button in the gallery widget; should not be longer than ~13 characters long' ),
     34                        'change_media' => _x( 'Add Image', 'label for button in the gallery widget; should not be longer than ~13 characters long' ),
     35                        'edit_media' => _x( 'Edit Gallery', 'label for button in the gallery widget; should not be longer than ~13 characters long' ),
     36                ) );
     37        }
     38
     39        /**
     40         * Get schema for properties of a widget instance (item).
     41         *
     42         * @since 4.9.0
     43         *
     44         * @see WP_REST_Controller::get_item_schema()
     45         * @see WP_REST_Controller::get_additional_fields()
     46         * @link https://core.trac.wordpress.org/ticket/35574
     47         * @return array Schema for properties.
     48         */
     49        public function get_instance_schema() {
     50                return array(
     51                        'title' => array(
     52                                'type' => 'string',
     53                                'default' => '',
     54                                'sanitize_callback' => 'sanitize_text_field',
     55                                'description' => __( 'Title for the widget' ),
     56                                'should_preview_update' => false,
     57                        ),
     58                        'ids' => array(
     59                                'type' => 'array',
     60                                'items' => array(
     61                                        'type' => 'integer',
     62                                ),
     63                                'default' => array(),
     64                                'sanitize_callback' => 'wp_parse_id_list',
     65                        ),
     66                        'columns' => array(
     67                                'type' => 'integer',
     68                                'default' => 3,
     69                                'minimum' => 1,
     70                                'maximum' => 9,
     71                        ),
     72                        'size' => array(
     73                                'type' => 'string',
     74                                'enum' => array_merge( get_intermediate_image_sizes(), array( 'full', 'custom' ) ),
     75                                'default' => 'thumbnail',
     76                        ),
     77                        'link_type' => array(
     78                                'type' => 'string',
     79                                'enum' => array( 'none', 'file', 'post' ),
     80                                'default' => 'none',
     81                                'media_prop' => 'link',
     82                                'should_preview_update' => false,
     83                        ),
     84                        'orderby_random' => array(
     85                                'type'                  => 'boolean',
     86                                'default'               => false,
     87                                'media_prop'            => '_orderbyRandom',
     88                                'should_preview_update' => false,
     89                        ),
     90                );
     91        }
     92
     93        /**
     94         * Render the media on the frontend.
     95         *
     96         * @since 4.9.0
     97         *
     98         * @param array $instance Widget instance props.
     99         * @return void
     100         */
     101        public function render_media( $instance ) {
     102                $instance = array_merge( wp_list_pluck( $this->get_instance_schema(), 'default' ), $instance );
     103
     104                $shortcode_atts = array(
     105                        'ids'     => $instance['ids'],
     106                        'columns' => $instance['columns'],
     107                        'link'    => $instance['link_type'],
     108                        'size'    => $instance['size'],
     109                );
     110
     111                // @codeCoverageIgnoreStart
     112                if ( $instance['orderby_random'] ) {
     113                        $shortcode_atts['orderby'] = 'rand';
     114                }
     115
     116                // @codeCoverageIgnoreEnd
     117                echo gallery_shortcode( $shortcode_atts );
     118        }
     119
     120        /**
     121         * Loads the required media files for the media manager and scripts for media widgets.
     122         *
     123         * @since 4.9.0
     124         */
     125        public function enqueue_admin_scripts() {
     126                parent::enqueue_admin_scripts();
     127
     128                $handle = 'media-gallery-widget';
     129                wp_enqueue_script( $handle );
     130
     131                $exported_schema = array();
     132                foreach ( $this->get_instance_schema() as $field => $field_schema ) {
     133                        $exported_schema[ $field ] = wp_array_slice_assoc( $field_schema, array( 'type', 'default', 'enum', 'minimum', 'format', 'media_prop', 'should_preview_update', 'items' ) );
     134                }
     135                wp_add_inline_script(
     136                        $handle,
     137                        sprintf(
     138                                'wp.mediaWidgets.modelConstructors[ %s ].prototype.schema = %s;',
     139                                wp_json_encode( $this->id_base ),
     140                                wp_json_encode( $exported_schema )
     141                        )
     142                );
     143
     144                wp_add_inline_script(
     145                        $handle,
     146                        sprintf(
     147                                '
     148                                        wp.mediaWidgets.controlConstructors[ %1$s ].prototype.mime_type = %2$s;
     149                                        _.extend( wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n, %3$s );
     150                                ',
     151                                wp_json_encode( $this->id_base ),
     152                                wp_json_encode( $this->widget_options['mime_type'] ),
     153                                wp_json_encode( $this->l10n )
     154                        )
     155                );
     156        }
     157
     158        /**
     159         * Render form template scripts.
     160         *
     161         * @since 4.9.0
     162         */
     163        public function render_control_template_scripts() {
     164                parent::render_control_template_scripts();
     165                ?>
     166                <script type="text/html" id="tmpl-wp-media-widget-gallery-preview">
     167                        <# var describedById = 'describedBy-' + String( Math.random() ); #>
     168                        <# if ( data.ids.length ) { #>
     169                                <div class="gallery media-widget-gallery-preview">
     170                                        <# _.each( data.ids, function( id, index ) { #>
     171                                                <#
     172                                                var attachment = data.attachments[ id ];
     173                                                if ( ! attachment ) {
     174                                                        return;
     175                                                }
     176                                                #>
     177                                                <# if ( index < 6 ) { #>
     178                                                        <dl class="gallery-item">
     179                                                                <dt class="gallery-icon">
     180                                                                <# if ( attachment.sizes.thumbnail ) { #>
     181                                                                        <img src="{{ attachment.sizes.thumbnail.url }}" width="{{ attachment.sizes.thumbnail.width }}" height="{{ attachment.sizes.thumbnail.height }}" alt="" />
     182                                                                <# } else { #>
     183                                                                        <img src="{{ attachment.url }}" alt="" />
     184                                                                <# } #>
     185                                                                <# if ( index === 5 && data.ids.length > 6 ) { #>
     186                                                                        <div class="gallery-icon-placeholder">
     187                                                                                <p class="gallery-icon-placeholder-text">+{{ data.ids.length - 5 }}</p>
     188                                                                        </div>
     189                                                                <# } #>
     190                                                                </dt>
     191                                                        </dl>
     192                                                <# } #>
     193                                        <# } ); #>
     194                                </div>
     195                        <# } else { #>
     196                                <div class="attachment-media-view">
     197                                        <p class="placeholder"><?php echo esc_html( $this->l10n['no_media_selected'] ); ?></p>
     198                                </div>
     199                        <# } #>
     200                </script>
     201                <?php
     202        }
     203
     204        /**
     205         * Whether the widget has content to show.
     206         *
     207         * @since 4.9.0
     208         * @access protected
     209         *
     210         * @param array $instance Widget instance props.
     211         * @return bool Whether widget has content.
     212         */
     213        protected function has_content( $instance ) {
     214                if ( ! empty( $instance['ids'] ) ) {
     215                        $attachments = wp_parse_id_list( $instance['ids'] );
     216                        foreach ( $attachments as $attachment ) {
     217                                if ( 'attachment' !== get_post_type( $attachment ) ) {
     218                                        return false;
     219                                }
     220                        }
     221                        return true;
     222                }
     223                return false;
     224        }
     225}
  • src/wp-includes/widgets/class-wp-widget-media.php

    diff --git src/wp-includes/widgets/class-wp-widget-media.php src/wp-includes/widgets/class-wp-widget-media.php
    index 84c9b3ff92..854cf885ae 100644
    abstract class WP_Widget_Media extends WP_Widget { 
    257257                                continue;
    258258                        }
    259259                        $value = $new_instance[ $field ];
     260
     261                        // Workaround for rest_validate_value_from_schema() due to the fact that rest_is_boolean( '' ) === false, while rest_is_boolean( '1' ) is true.
     262                        if ( 'boolean' === $field_schema['type'] && '' === $value ) {
     263                                $value = false;
     264                        }
     265
    260266                        if ( true !== rest_validate_value_from_schema( $value, $field_schema, $field ) ) {
    261267                                continue;
    262268                        }
    abstract class WP_Widget_Media extends WP_Widget { 
    316322                                class="media-widget-instance-property"
    317323                                name="<?php echo esc_attr( $this->get_field_name( $name ) ); ?>"
    318324                                id="<?php echo esc_attr( $this->get_field_id( $name ) ); // Needed specifically by wpWidgets.appendTitle(). ?>"
    319                                 value="<?php echo esc_attr( strval( $value ) ); ?>"
     325                                value="<?php echo esc_attr( is_array( $value ) ? join( ',', $value ) : strval( $value ) ); ?>"
    320326                        />
    321327                <?php
    322328                endforeach;
    abstract class WP_Widget_Media extends WP_Widget { 
    388394                                <label for="{{ elementIdPrefix }}title"><?php esc_html_e( 'Title:' ); ?></label>
    389395                                <input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
    390396                        </p>
    391                         <div class="media-widget-preview">
     397                        <div class="media-widget-preview <?php echo esc_attr( $this->id_base ); ?>">
    392398                                <div class="attachment-media-view">
    393399                                        <div class="placeholder"><?php echo esc_html( $this->l10n['no_media_selected'] ); ?></div>
    394400                                </div>
  • new file tests/phpunit/tests/widgets/media-gallery-widget.php

    diff --git tests/phpunit/tests/widgets/media-gallery-widget.php tests/phpunit/tests/widgets/media-gallery-widget.php
    new file mode 100644
    index 0000000000..0a6defaee4
    - +  
     1<?php
     2/**
     3 * Unit tests covering WP_Widget_Media_Gallery functionality.
     4 *
     5 * @package    WordPress
     6 * @subpackage widgets
     7 */
     8
     9/**
     10 * Test wp-includes/widgets/class-wp-widget-gallery.php
     11 *
     12 * @group widgets
     13 */
     14class Test_WP_Widget_Media_Gallery extends WP_UnitTestCase {
     15
     16        /**
     17         * Clean up global scope.
     18         *
     19         * @global WP_Scripts $wp_scripts
     20         * @global WP_Styles $wp_styles
     21         */
     22        public function clean_up_global_scope() {
     23                global $wp_scripts, $wp_styles;
     24                parent::clean_up_global_scope();
     25                $wp_scripts = null;
     26                $wp_styles = null;
     27        }
     28
     29        /**
     30         * Test get_instance_schema method.
     31         *
     32         * @covers WP_Widget_Media_Gallery::get_instance_schema()
     33         */
     34        public function test_get_instance_schema() {
     35                $widget = new WP_Widget_Media_Gallery();
     36                $schema = $widget->get_instance_schema();
     37
     38                $this->assertEqualSets(
     39                        array(
     40                                'title',
     41                                'ids',
     42                                'columns',
     43                                'size',
     44                                'link_type',
     45                                'orderby_random',
     46                        ),
     47                        array_keys( $schema )
     48                );
     49        }
     50
     51        /**
     52         * Test update() method.
     53         *
     54         * @covers WP_Widget_Media_Gallery::render_media()
     55         */
     56        public function test_render_media() {
     57                $widget = new WP_Widget_Media_Gallery();
     58
     59                $attachments = array();
     60                foreach ( array( 'canola.jpg', 'waffles.jpg' ) as $filename ) {
     61                        $test_image = '/tmp/' . $filename;
     62                        copy( DIR_TESTDATA . '/images/canola.jpg', $test_image );
     63                        $attachment_id = self::factory()->attachment->create_object( array(
     64                                'file' => $test_image,
     65                                'post_parent' => 0,
     66                                'post_mime_type' => 'image/jpeg',
     67                                'post_title' => 'Canola',
     68                        ) );
     69                        wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $test_image ) );
     70                        $attachments[ $filename ] = $attachment_id;
     71                }
     72
     73                $instance = wp_list_pluck( $widget->get_instance_schema(), 'default' );
     74                $instance['size'] = 'thumbnail';
     75                $instance['columns'] = 3;
     76                $instance['ids'] = array_values( $attachments );
     77                ob_start();
     78                $widget->render_media( $instance );
     79                $output = ob_get_clean();
     80
     81                $this->assertContains( 'gallery-columns-3', $output );
     82                $this->assertContains( 'gallery-size-thumbnail', $output );
     83                $this->assertContains( 'canola', $output );
     84                $this->assertContains( 'waffles', $output );
     85        }
     86
     87        /**
     88         * Test enqueue_admin_scripts() method.
     89         *
     90         * @covers WP_Widget_Media_Gallery::enqueue_admin_scripts()
     91         */
     92        public function test_enqueue_admin_scripts() {
     93                set_current_screen( 'widgets.php' );
     94                $widget = new WP_Widget_Media_Gallery();
     95
     96                $this->assertFalse( wp_script_is( 'media-gallery-widget' ) );
     97
     98                $widget->enqueue_admin_scripts();
     99
     100                $this->assertTrue( wp_script_is( 'media-gallery-widget' ) );
     101
     102                $after = join( '', wp_scripts()->registered['media-gallery-widget']->extra['after'] );
     103                $this->assertContains( 'wp.mediaWidgets.modelConstructors[ "media_gallery" ].prototype', $after );
     104        }
     105
     106        /**
     107         * Test update() method.
     108         *
     109         * @covers WP_Widget_Media_Gallery::update()
     110         */
     111        public function test_update() {
     112                $widget = new WP_Widget_Media_Gallery();
     113                $schema = $widget->get_instance_schema();
     114                $instance = wp_list_pluck( $schema, 'default' );
     115
     116                // Field: title.
     117                $instance['title'] = 'Hello <b>World</b> ';
     118                $instance = $widget->update( $instance, array() );
     119                $this->assertEquals( 'Hello World', $instance['title'] );
     120
     121                // Field: ids.
     122                $instance['ids'] = '1,2,3';
     123                $instance = $widget->update( $instance, array() );
     124                $this->assertSame( array( 1, 2, 3 ), $instance['ids'] );
     125
     126                $instance['ids'] = array( 1, 2, '3' );
     127                $instance = $widget->update( $instance, array() );
     128                $this->assertSame( array( 1, 2, 3 ), $instance['ids'] );
     129
     130                $instance['ids'] = array( 'too', 'bad' );
     131                $instance = $widget->update( $instance, array( 'ids' => array( 2, 3 ) ) );
     132                $this->assertSame( array( 2, 3 ), $instance['ids'] );
     133
     134                // Field: columns.
     135                $instance['columns'] = 4;
     136                $instance = $widget->update( $instance, array() );
     137                $this->assertSame( 4, $instance['columns'] );
     138
     139                $instance['columns'] = '2';
     140                $instance = $widget->update( $instance, array() );
     141                $this->assertSame( 2, $instance['columns'] );
     142
     143                $instance['columns'] = -1; // Under min of 1.
     144                $instance = $widget->update( $instance, array( 'columns' => 3 ) );
     145                $this->assertSame( 3, $instance['columns'] );
     146
     147                $instance['columns'] = 10; // Over max of 9.
     148                $instance = $widget->update( $instance, array( 'columns' => 3 ) );
     149                $this->assertSame( 3, $instance['columns'] );
     150
     151                // Field: size.
     152                $instance['size'] = 'large';
     153                $instance = $widget->update( $instance, array() );
     154                $this->assertSame( 'large', $instance['size'] );
     155
     156                $instance['size'] = 'bad';
     157                $instance = $widget->update( $instance, array( 'size' => 'thumbnail' ) );
     158                $this->assertSame( 'thumbnail', $instance['size'] );
     159
     160                // Field: link_type.
     161                $instance['link_type'] = 'none';
     162                $instance = $widget->update( $instance, array() );
     163                $this->assertSame( 'none', $instance['link_type'] );
     164
     165                $instance['link_type'] = 'unknown';
     166                $instance = $widget->update( $instance, array( 'link_type' => 'file' ) );
     167                $this->assertSame( 'file', $instance['link_type'] );
     168
     169                // Field: orderby_random.
     170                $instance['orderby_random'] = '1';
     171                $instance = $widget->update( $instance, array() );
     172                $this->assertTrue( $instance['orderby_random'] );
     173
     174                $instance['orderby_random'] = true;
     175                $instance = $widget->update( $instance, array() );
     176                $this->assertTrue( $instance['orderby_random'] );
     177
     178                $instance['orderby_random'] = '';
     179                $instance = $widget->update( $instance, array() );
     180                $this->assertFalse( $instance['orderby_random'] );
     181
     182                $instance['orderby_random'] = false;
     183                $instance = $widget->update( $instance, array() );
     184                $this->assertFalse( $instance['orderby_random'] );
     185        }
     186
     187        /**
     188         * Test render_control_template_scripts() method.
     189         *
     190         * @covers WP_Widget_Media_Gallery::render_control_template_scripts()
     191         */
     192        public function test_render_control_template_scripts() {
     193                $widget = new WP_Widget_Media_Gallery();
     194
     195                ob_start();
     196                $widget->render_control_template_scripts();
     197                $output = ob_get_clean();
     198
     199                $this->assertContains( '<script type="text/html" id="tmpl-wp-media-widget-gallery-preview">', $output );
     200        }
     201}
  • tests/qunit/index.html

    diff --git tests/qunit/index.html tests/qunit/index.html
    index 52f8f8d789..d096aeeec7 100644
     
    119119                        wp.mediaWidgets.controlConstructors[ "media_audio" ].prototype.mime_type = "audio";
    120120                        _.extend( wp.mediaWidgets.controlConstructors[ "media_audio" ].prototype.l10n, {"no_media_selected":"No audio selected","select_media":"Select File","change_media":"Change Audio","edit_media":"Edit Audio","add_to_widget":"Add to Widget","missing_attachment":"We can&#8217;t find that audio file. Check your <a href=\"http:\/\/src.wordpress-develop.dev\/wp-admin\/upload.php\">media library<\/a> and make sure it wasn&#8217;t deleted.","media_library_state_multi":{"0":"Audio Widget (%d)","1":"Audio Widget (%d)","singular":"Audio Widget (%d)","plural":"Audio Widget (%d)","context":null,"domain":null},"media_library_state_single":"Audio Widget"} );
    121121                </script>
     122                <script type='text/javascript' src='../../src/wp-admin/js/widgets/media-gallery-widget.js'></script>
     123                <script type='text/javascript'>
     124                        wp.mediaWidgets.modelConstructors[ "media_gallery" ].prototype.schema = {"title":{"type":"string","default":"","should_preview_update":false},"ids":{"type":"string","default":""},"columns":{"type":"integer","default":3},"size":{"type":"string","default":"thumbnail","enum":["thumbnail","medium","medium_large","large","post-thumbnail","full","custom"]},"link_type":{"type":"string","default":"none","enum":["none","file","post"],"media_prop":"link","should_preview_update":false},"orderby_random":{"type":"boolean","default":false,"media_prop":"_orderbyRandom","should_preview_update":false},"attachments":{"type":"string","default":""}};
     125                        wp.mediaWidgets.controlConstructors[ "media_gallery" ].prototype.mime_type = "image";
     126
     127                        _.extend( wp.mediaWidgets.controlConstructors[ "media_gallery" ].prototype.l10n, {"no_media_selected":"No images selected","add_media":"Add Media","replace_media":"Replace Media","edit_media":"Edit Gallery","add_to_widget":"Add to Widget","missing_attachment":"We can&#8217;t find that gallery. Check your <a href=\"http:\/\/src.wordpress-develop.dev\/wp-admin\/upload.php\">media library<\/a> and make sure it wasn&#8217;t deleted.","media_library_state_multi":"","media_library_state_single":"","unsupported_file_type":"Looks like this isn&#8217;t the correct kind of file. Please link to an appropriate file instead.","select_media":"Select Images","change_media":"Add Image"} );
     128                </script>
    122129
    123130                <!-- Unit tests -->
    124131                <script src="wp-admin/js/password-strength-meter.js"></script>
     
    136143                <script src="wp-admin/js/nav-menu.js"></script>
    137144                <script src="wp-admin/js/widgets/test-media-widgets.js"></script>
    138145                <script src="wp-admin/js/widgets/test-media-image-widget.js"></script>
     146                <script src="wp-admin/js/widgets/test-media-gallery-widget.js"></script>
    139147                <script src="wp-admin/js/widgets/test-media-video-widget.js"></script>
    140148
    141149                <!-- Customizer templates for sections -->
     
    569577                                        </div><!-- #available-widgets-list -->
    570578                                </div><!-- #available-widgets -->
    571579                        </div><!-- #widgets-left -->
    572                        
     580
    573581                        <script type="text/html" id="tmpl-widget-media-media_image-control">
    574582                        <# var elementIdPrefix = 'el' + String( Math.random() ) + '_' #>
    575583                        <p>
     
    811819                <div class="media-frame-uploader"></div>
    812820        </script>
    813821
     822        <script type="text/html" id="tmpl-widget-media-media_gallery-control">
     823                <# var elementIdPrefix = 'el' + String( Math.random() ) + '_' #>
     824                <p>
     825                        <label for="{{ elementIdPrefix }}title">Title:</label>
     826                        <input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
     827                </p>
     828                <div class="media-widget-preview">
     829                        <div class="attachment-media-view">
     830                                <div class="placeholder">No images selected</div>
     831                        </div>
     832                        </div>
     833                        <p class="media-widget-buttons">
     834                                <button type="button" class="button edit-media selected">
     835                                        Edit Gallery                            </button>
     836                                <button type="button" class="button change-media select-media selected">
     837                                        Replace Media                           </button>
     838                                <button type="button" class="button select-media not-selected">
     839                                        Add Media                               </button>
     840                        </p>
     841                <div class="media-widget-fields">
     842                </div>
     843        </script>
     844                                <script type="text/html" id="tmpl-wp-media-widget-gallery-preview">
     845                        <# var describedById = 'describedBy-' + String( Math.random() ); #>
     846                        <# data.attachments = data.attachments ? JSON.parse(data.attachments) : ''; #>
     847                        <# if ( Array.isArray( data.attachments ) && data.attachments.length ) { #>
     848                                <div class="gallery gallery-columns-{{ data.columns }}">
     849                                        <# _.each( data.attachments, function( attachment, index ) { #>
     850                                                <dl class="gallery-item">
     851                                                        <dt class="gallery-icon">
     852                                                        <# if ( attachment.sizes.thumbnail ) { #>
     853                                                                <img src="{{ attachment.sizes.thumbnail.url }}" width="{{ attachment.sizes.thumbnail.width }}" height="{{ attachment.sizes.thumbnail.height }}" alt="" />
     854                                                        <# } else { #>
     855                                                                <img src="{{ attachment.url }}" alt="" />
     856                                                        <# } #>
     857                                                        </dt>
     858                                                        <# if ( attachment.caption ) { #>
     859                                                                <dd class="wp-caption-text gallery-caption">
     860                                                                        {{{ data.verifyHTML( attachment.caption ) }}}
     861                                                                </dd>
     862                                                        <# } #>
     863                                                </dl>
     864                                                <# if ( index % data.columns === data.columns - 1 ) { #>
     865                                                        <br style="clear: both;">
     866                                                <# } #>
     867                                        <# } ); #>
     868                                </div>
     869                        <# } else { #>
     870                                <div class="attachment-media-view">
     871                                        <p class="placeholder">No images selected</p>
     872                                </div>
     873                        <# } #>
     874                </script>
     875
    814876        <script type="text/html" id="tmpl-media-modal">
    815877                <div class="media-modal wp-core-ui">
    816878                        <button type="button" class="media-modal-close"><span class="media-modal-icon"><span class="screen-reader-text">Close media panel</span></span></button>
  • new file tests/qunit/wp-admin/js/widgets/test-media-gallery-widget.js

    diff --git tests/qunit/wp-admin/js/widgets/test-media-gallery-widget.js tests/qunit/wp-admin/js/widgets/test-media-gallery-widget.js
    new file mode 100644
    index 0000000000..c07dae52d8
    - +  
     1/* global wp */
     2/* jshint qunit: true */
     3/* eslint-env qunit */
     4/* eslint-disable no-magic-numbers */
     5
     6( function() {
     7        'use strict';
     8
     9        module( 'Gallery Media Widget' );
     10
     11        test( 'gallery widget control', function() {
     12                var GalleryWidgetControl;
     13                equal( typeof wp.mediaWidgets.controlConstructors.media_gallery, 'function', 'wp.mediaWidgets.controlConstructors.media_gallery is a function' );
     14                GalleryWidgetControl = wp.mediaWidgets.controlConstructors.media_gallery;
     15                ok( GalleryWidgetControl.prototype instanceof wp.mediaWidgets.MediaWidgetControl, 'wp.mediaWidgets.controlConstructors.media_gallery subclasses wp.mediaWidgets.MediaWidgetControl' );
     16        });
     17
     18        test( 'gallery media model', function() {
     19                var GalleryWidgetModel, galleryWidgetModelInstance;
     20                equal( typeof wp.mediaWidgets.modelConstructors.media_gallery, 'function', 'wp.mediaWidgets.modelConstructors.media_gallery is a function' );
     21                GalleryWidgetModel = wp.mediaWidgets.modelConstructors.media_gallery;
     22                ok( GalleryWidgetModel.prototype instanceof wp.mediaWidgets.MediaWidgetModel, 'wp.mediaWidgets.modelConstructors.media_gallery subclasses wp.mediaWidgets.MediaWidgetModel' );
     23
     24                galleryWidgetModelInstance = new GalleryWidgetModel();
     25                _.each( galleryWidgetModelInstance.attributes, function( value, key ) {
     26                        equal( value, GalleryWidgetModel.prototype.schema[ key ][ 'default' ], 'Should properly set default for ' + key );
     27                });
     28        });
     29
     30})();