WordPress.org

Make WordPress Core

Ticket #35243: 35243.0.diff

File 35243.0.diff, 19.5 KB (added by westonruter, 14 months ago)

https://github.com/xwp/wordpress-develop/pull/230

  • Gruntfile.js

    diff --git Gruntfile.js Gruntfile.js
    index 14e558f2d1..37848c1449 100644
    module.exports = function(grunt) { 
    456456                                dest: BUILD_DIR,
    457457                                ext: '.min.js',
    458458                                src: [
    459                                         'wp-admin/js/*.js',
     459                                        'wp-admin/js/**/*.js',
    460460                                        'wp-includes/js/*.js',
    461461                                        'wp-includes/js/mediaelement/wp-mediaelement.js',
    462462                                        'wp-includes/js/mediaelement/wp-playlist.js',
  • src/wp-admin/css/customize-widgets.css

    diff --git src/wp-admin/css/customize-widgets.css src/wp-admin/css/customize-widgets.css
    index bdb58982fe..0a9fbf5869 100644
     
    213213        display: block;
    214214}
    215215
     216/* Text Widget */
     217.wp-customizer div.mce-inline-toolbar-grp,
     218.wp-customizer div.mce-tooltip {
     219        z-index: 500100 !important;
     220}
     221.wp-customizer .ui-autocomplete.wplink-autocomplete {
     222        z-index: 500110; /* originally 100110, but z-index of .wp-full-overlay is 500000 */
     223}
     224.wp-customizer #wp-link-backdrop {
     225        z-index: 500100; /* originally 100100, but z-index of .wp-full-overlay is 500000 */
     226}
     227.wp-customizer #wp-link-wrap {
     228        z-index: 500105; /* originally 100105, but z-index of .wp-full-overlay is 500000 */
     229}
     230
    216231/**
    217232 * Styles for new widget addition panel
    218233 */
  • new file src/wp-admin/js/widgets/text-widgets.js

    diff --git src/wp-admin/js/widgets/text-widgets.js src/wp-admin/js/widgets/text-widgets.js
    new file mode 100644
    index 0000000000..a8802527e3
    - +  
     1/* global tinymce, switchEditors */
     2/* eslint consistent-this: [ "error", "control" ] */
     3wp.textWidgets = ( function( $ ) {
     4        'use strict';
     5
     6        var component = {};
     7
     8        /**
     9         * Text widget control.
     10         *
     11         * @class TextWidgetControl
     12         * @constructor
     13         * @abstract
     14         */
     15        component.TextWidgetControl = Backbone.View.extend({
     16
     17                /**
     18                 * View events.
     19                 *
     20                 * @type {Object}
     21                 */
     22                events: {},
     23
     24                /**
     25                 * Initialize.
     26                 *
     27                 * @param {Object}         options - Options.
     28                 * @param {Backbone.Model} options.model - Model.
     29                 * @param {jQuery}         options.el - Control container element.
     30                 * @returns {void}
     31                 */
     32                initialize: function initialize( options ) {
     33                        var control = this;
     34
     35                        if ( ! options.el ) {
     36                                throw new Error( 'Missing options.el' );
     37                        }
     38
     39                        Backbone.View.prototype.initialize.call( control, options );
     40
     41                        /*
     42                         * Create a container element for the widget control fields.
     43                         * This is inserted into the DOM immediately before the the .widget-content
     44                         * element because the contents of this element are essentially "managed"
     45                         * by PHP, where each widget update cause the entire element to be emptied
     46                         * and replaced with the rendered output of WP_Widget::form() which is
     47                         * sent back in Ajax request made to save/update the widget instance.
     48                         * To prevent a "flash of replaced DOM elements and re-initialized JS
     49                         * components", the JS template is rendered outside of the normal form
     50                         * container.
     51                         */
     52                        control.fieldContainer = $( '<div class="text-widget-fields"></div>' );
     53                        control.fieldContainer.html( wp.template( 'widget-text-control-fields' ) );
     54                        control.widgetContentContainer = control.$el.find( '.widget-content:first' );
     55                        control.widgetContentContainer.before( control.fieldContainer );
     56
     57                        control.fields = {
     58                                title: control.fieldContainer.find( '.title' ),
     59                                text: control.fieldContainer.find( '.text' )
     60                        };
     61
     62                        // Sync input fields to hidden sync fields which actually get sent to the server.
     63                        _.each( control.fields, function( fieldInput, fieldName ) {
     64                                fieldInput.on( 'input change', function updateSyncField() {
     65                                        var syncInput = control.widgetContentContainer.find( 'input[type=hidden].' + fieldName );
     66                                        if ( syncInput.val() !== $( this ).val() ) {
     67                                                syncInput.val( $( this ).val() );
     68                                                syncInput.trigger( 'change' );
     69                                        }
     70                                });
     71
     72                                // Note that syncInput cannot be re-used because it will be destroyed with each widget-updated event.
     73                                fieldInput.val( control.widgetContentContainer.find( 'input[type=hidden].' + fieldName ).val() );
     74                        });
     75                },
     76
     77                /**
     78                 * Update input fields from the sync fields.
     79                 *
     80                 * This function is called at the widget-updated and widget-synced events.
     81                 * A field will only be updated if it is not currently focused, to avoid
     82                 * overwriting content that the user is entering.
     83                 *
     84                 * @returns {void}
     85                 */
     86                updateFields: function updateFields() {
     87                        var control = this, syncInput;
     88
     89                        if ( ! control.fields.title.is( document.activeElement ) ) {
     90                                syncInput = control.widgetContentContainer.find( 'input[type=hidden].title' );
     91                                control.fields.title.val( syncInput.val() );
     92                        }
     93
     94                        syncInput = control.widgetContentContainer.find( 'input[type=hidden].text' );
     95                        if ( control.fields.text.is( ':visible' ) ) {
     96                                if ( ! control.fields.text.is( document.activeElement ) ) {
     97                                        control.fields.text.val( syncInput.val() );
     98                                }
     99                        } else if ( control.editor && ! control.editorFocused && syncInput.val() !== control.fields.text.val() ) {
     100                                control.editor.setContent( wp.editor.autop( syncInput.val() ) );
     101                        }
     102                },
     103
     104                /**
     105                 * Initialize editor.
     106                 *
     107                 * @returns {void}
     108                 */
     109                initializeEditor: function initializeEditor() {
     110                        var control = this, changeDebounceDelay = 1000, iframeKeepAliveInterval = 1000, id, textarea, restoreTextMode = false;
     111                        textarea = control.fields.text;
     112                        id = textarea.attr( 'id' );
     113
     114                        /**
     115                         * Build (or re-build) the visual editor.
     116                         *
     117                         * @returns {void}
     118                         */
     119                        function buildEditor() {
     120                                var editor, triggerChangeIfDirty, onInit;
     121
     122                                // Destroy any existing editor so that it can be re-initialized after a widget-updated event.
     123                                if ( tinymce.get( id ) )    {
     124                                        restoreTextMode = tinymce.get( id ).isHidden();
     125                                        wp.editor.remove( id );
     126                                }
     127
     128                                wp.editor.initialize( id, {
     129                                        tinymce: {
     130                                                wpautop: true
     131                                        },
     132                                        quicktags: true
     133                                } );
     134
     135                                editor = window.tinymce.get( id );
     136                                if ( ! editor ) {
     137                                        throw new Error( 'Failed to initialize editor' );
     138                                }
     139                                onInit = function() {
     140                                        watchForDestroyedBody( control.$el.find( 'iframe' )[0] );
     141
     142                                        // If a prior mce instance was replaced, and it was in text mode, toggle to text mode.
     143                                        if ( restoreTextMode ) {
     144                                                switchEditors.go( id, 'toggle' );
     145                                        }
     146                                };
     147
     148                                if ( editor.initialized ) {
     149                                        onInit();
     150                                } else {
     151                                        editor.on( 'init', onInit );
     152                                }
     153
     154                                control.editorFocused = false;
     155                                triggerChangeIfDirty = function() {
     156                                        if ( editor.isDirty() ) {
     157                                                editor.save();
     158                                                textarea.trigger( 'change' );
     159                                        }
     160                                };
     161                                editor.on( 'focus', function() {
     162                                        control.editorFocused = true;
     163                                } );
     164                                editor.on( 'NodeChange', _.debounce( triggerChangeIfDirty, changeDebounceDelay ) );
     165                                editor.on( 'blur', function() {
     166                                        control.editorFocused = false;
     167                                        triggerChangeIfDirty();
     168                                } );
     169
     170                                control.editor = editor;
     171                        }
     172
     173                        /**
     174                         * Watch an iframe for the destruction of its TinyMCE contenteditable contents.
     175                         *
     176                         * @todo There may be a better way to listen for an iframe being destroyed.
     177                         * @param {HTMLIFrameElement} iframe - TinyMCE iframe.
     178                         * @returns {void}
     179                         */
     180                        function watchForDestroyedBody( iframe ) {
     181                                var timeoutId = setInterval( function() {
     182                                        if ( ! iframe.contentWindow || iframe.contentWindow.document.body.id ) {
     183                                                return;
     184                                        }
     185                                        clearInterval( timeoutId );
     186                                        buildEditor();
     187                                }, iframeKeepAliveInterval );
     188                        }
     189
     190                        buildEditor();
     191                }
     192        });
     193
     194        /**
     195         * Mapping of widget ID to instances of TextWidgetControl subclasses.
     196         *
     197         * @type {Object.<string, wp.textWidgets.TextWidgetControl>}
     198         */
     199        component.widgetControls = {};
     200
     201        /**
     202         * Handle widget being added or initialized for the first time at the widget-added event.
     203         *
     204         * @param {jQuery.Event} event - Event.
     205         * @param {jQuery}       widgetContainer - Widget container element.
     206         * @returns {void}
     207         */
     208        component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) {
     209                var widgetForm, idBase, widgetControl, widgetId, animatedCheckDelay = 50, widgetInside, renderWhenAnimationDone;
     210                widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen.
     211
     212                idBase = widgetForm.find( '> .id_base' ).val();
     213                if ( 'text' !== idBase ) {
     214                        return;
     215                }
     216
     217                // Prevent initializing already-added widgets.
     218                widgetId = widgetForm.find( '> .widget-id' ).val();
     219                if ( component.widgetControls[ widgetId ] ) {
     220                        return;
     221                }
     222
     223                widgetControl = new component.TextWidgetControl({
     224                        el: widgetContainer
     225                });
     226
     227                component.widgetControls[ widgetId ] = widgetControl;
     228
     229                /*
     230                 * Render the widget once the widget parent's container finishes animating,
     231                 * as the widget-added event fires with a slideDown of the container.
     232                 * This ensures that the textarea is visible and an iframe can be embedded
     233                 * with TinyMCE being able to set contenteditable on it.
     234                 */
     235                widgetInside = widgetContainer.parent();
     236                renderWhenAnimationDone = function() {
     237                        if ( widgetInside.is( ':animated' ) ) {
     238                                setTimeout( renderWhenAnimationDone, animatedCheckDelay );
     239                        } else {
     240                                widgetControl.initializeEditor();
     241                        }
     242                };
     243                renderWhenAnimationDone();
     244        };
     245
     246        /**
     247         * Sync widget instance data sanitized from server back onto widget model.
     248         *
     249         * This gets called via the 'widget-updated' event when saving a widget from
     250         * the widgets admin screen and also via the 'widget-synced' event when making
     251         * a change to a widget in the customizer.
     252         *
     253         * @param {jQuery.Event} event - Event.
     254         * @param {jQuery}       widgetContainer - Widget container element.
     255         * @returns {void}
     256         */
     257        component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) {
     258                var widgetForm, widgetId, widgetControl, idBase;
     259                widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
     260
     261                idBase = widgetForm.find( '> .id_base' ).val();
     262                if ( 'text' !== idBase ) {
     263                        return;
     264                }
     265
     266                widgetId = widgetForm.find( '> .widget-id' ).val();
     267                widgetControl = component.widgetControls[ widgetId ];
     268                if ( ! widgetControl ) {
     269                        return;
     270                }
     271
     272                widgetControl.updateFields();
     273        };
     274
     275        /**
     276         * Initialize functionality.
     277         *
     278         * This function exists to prevent the JS file from having to boot itself.
     279         * When WordPress enqueues this script, it should have an inline script
     280         * attached which calls wp.textWidgets.init().
     281         *
     282         * @returns {void}
     283         */
     284        component.init = function init() {
     285                var $document = $( document );
     286                $document.on( 'widget-added', component.handleWidgetAdded );
     287                $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated );
     288
     289                /*
     290                 * Manually trigger widget-added events for media widgets on the admin
     291                 * screen once they are expanded. The widget-added event is not triggered
     292                 * for each pre-existing widget on the widgets admin screen like it is
     293                 * on the customizer. Likewise, the customizer only triggers widget-added
     294                 * when the widget is expanded to just-in-time construct the widget form
     295                 * when it is actually going to be displayed. So the following implements
     296                 * the same for the widgets admin screen, to invoke the widget-added
     297                 * handler when a pre-existing media widget is expanded.
     298                 */
     299                $( function initializeExistingWidgetContainers() {
     300                        var widgetContainers;
     301                        if ( 'widgets' !== window.pagenow ) {
     302                                return;
     303                        }
     304                        widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' );
     305                        widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() {
     306                                var widgetContainer = $( this );
     307                                component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer );
     308                        });
     309                });
     310        };
     311
     312        return component;
     313})( jQuery );
  • src/wp-includes/default-filters.php

    diff --git src/wp-includes/default-filters.php src/wp-includes/default-filters.php
    index e6050774c6..ef412e9e6a 100644
    add_filter( 'list_cats', 'wptexturize' ); 
    164164
    165165add_filter( 'wp_sprintf', 'wp_sprintf_l', 10, 2 );
    166166
    167 add_filter( 'widget_text', 'balanceTags' );
     167add_filter( 'widget_text',         'balanceTags'          );
     168add_filter( 'widget_text_content', 'capital_P_dangit', 11 );
     169add_filter( 'widget_text_content', 'wptexturize'          );
     170add_filter( 'widget_text_content', 'convert_smilies',  20 );
     171add_filter( 'widget_text_content', 'wpautop'              );
    168172
    169173add_filter( 'date_i18n', 'wp_maybe_decline_date' );
    170174
  • src/wp-includes/script-loader.php

    diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
    index 43209721f5..344bc4559b 100644
    function wp_default_scripts( &$scripts ) { 
    602602                $scripts->add( 'admin-gallery', "/wp-admin/js/gallery$suffix.js", array( 'jquery-ui-sortable' ) );
    603603
    604604                $scripts->add( 'admin-widgets', "/wp-admin/js/widgets$suffix.js", array( 'jquery-ui-sortable', 'jquery-ui-draggable', 'jquery-ui-droppable' ), false, 1 );
     605                $scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util' ) );
     606                $scripts->add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' );
    605607
    606608                $scripts->add( 'theme', "/wp-admin/js/theme$suffix.js", array( 'wp-backbone', 'wp-a11y' ), false, 1 );
    607609
  • src/wp-includes/widgets/class-wp-widget-text.php

    diff --git src/wp-includes/widgets/class-wp-widget-text.php src/wp-includes/widgets/class-wp-widget-text.php
    index a379fd719d..434a123f90 100644
    class WP_Widget_Text extends WP_Widget { 
    2828                        'description' => __( 'Arbitrary text or HTML.' ),
    2929                        'customize_selective_refresh' => true,
    3030                );
    31                 $control_ops = array( 'width' => 400, 'height' => 350 );
     31                $control_ops = array(
     32                        'width' => 400,
     33                        'height' => 350,
     34                );
    3235                parent::__construct( 'text', __( 'Text' ), $widget_ops, $control_ops );
    3336        }
    3437
    3538        /**
     39         * Add hooks for enqueueing assets when registering all widget instances of this widget class.
     40         *
     41         * @since 4.8.0
     42         * @access public
     43         */
     44        public function _register() {
     45
     46                // Note that the widgets component in the customizer will also do the 'admin_print_scripts-widgets.php' action in WP_Customize_Widgets::print_scripts().
     47                add_action( 'admin_print_scripts-widgets.php', array( $this, 'enqueue_admin_scripts' ) );
     48
     49                // Note that the widgets component in the customizer will also do the 'admin_footer-widgets.php' action in WP_Customize_Widgets::print_footer_scripts().
     50                add_action( 'admin_footer-widgets.php', array( $this, 'render_control_template_scripts' ) );
     51
     52                parent::_register();
     53        }
     54
     55        /**
    3656         * Outputs the content for the current Text widget instance.
    3757         *
    3858         * @since 2.8.0
    class WP_Widget_Text extends WP_Widget { 
    6181                 */
    6282                $text = apply_filters( 'widget_text', $widget_text, $instance, $this );
    6383
     84                if ( isset( $instance['filter'] ) ) {
     85                        if ( 'content' === $instance['filter'] ) {
     86
     87                                /**
     88                                 * Filters the content of the Text widget to apply changes expected from the visual (TinyMCE) editor.
     89                                 *
     90                                 * By default a subset of the_content filters are applied, including wpautop and wptexturize.
     91                                 *
     92                                 * @since 4.8.0
     93                                 *
     94                                 * @param string         $widget_text The widget content.
     95                                 * @param array          $instance    Array of settings for the current widget.
     96                                 * @param WP_Widget_Text $this        Current Text widget instance.
     97                                 */
     98                                $text = apply_filters( 'widget_text_content', $widget_text, $instance, $this );
     99
     100                        } elseif ( $instance['filter'] ) {
     101                                $text = wpautop( $text ); // Back-compat for instances prior to 4.8.
     102                        }
     103                }
     104
    64105                echo $args['before_widget'];
    65106                if ( ! empty( $title ) ) {
    66107                        echo $args['before_title'] . $title . $args['after_title'];
    67                 } ?>
    68                         <div class="textwidget"><?php echo !empty( $instance['filter'] ) ? wpautop( $text ) : $text; ?></div>
     108                }
     109
     110                ?>
     111                        <div class="textwidget"><?php echo $text; ?></div>
    69112                <?php
    70113                echo $args['after_widget'];
    71114        }
    class WP_Widget_Text extends WP_Widget { 
    89132                } else {
    90133                        $instance['text'] = wp_kses_post( $new_instance['text'] );
    91134                }
    92                 $instance['filter'] = ! empty( $new_instance['filter'] );
     135
     136                /*
     137                 * Re-use legacy 'filter' (wpautop) property to now indicate content filters will always apply.
     138                 * Prior to 4.8, this is a boolean value used to indicate whether or not wpautop should be
     139                 * applied. By re-using this property, downgrading WordPress from 4.8 to 4.7 will ensure
     140                 * that the content for Text widgets created with TinyMCE will continue to get wpautop.
     141                 */
     142                $instance['filter'] = 'content';
     143
    93144                return $instance;
    94145        }
    95146
    96147        /**
     148         * Loads the required scripts and styles for the widget control.
     149         *
     150         * @since 4.8.0
     151         * @access public
     152         */
     153        public function enqueue_admin_scripts() {
     154                wp_enqueue_editor();
     155                wp_enqueue_script( 'text-widgets' );
     156        }
     157
     158        /**
    97159         * Outputs the Text widget settings form.
    98160         *
    99161         * @since 2.8.0
     162         * @since 4.8.0 Form only contains hidden inputs which are synced with JS template.
    100163         * @access public
     164         * @see WP_Widget_Visual_Text::render_control_template_scripts()
    101165         *
    102166         * @param array $instance Current settings.
     167         * @return void
    103168         */
    104169        public function form( $instance ) {
    105                 $instance = wp_parse_args( (array) $instance, array( 'title' => '', 'text' => '' ) );
    106                 $filter = isset( $instance['filter'] ) ? $instance['filter'] : 0;
    107                 $title = sanitize_text_field( $instance['title'] );
     170                $instance = wp_parse_args(
     171                        (array) $instance,
     172                        array(
     173                                'title' => '',
     174                                'text' => '',
     175                        )
     176                );
    108177                ?>
    109                 <p><label for="<?php echo $this->get_field_id('title'); ?>"><?php _e('Title:'); ?></label>
    110                 <input class="widefat" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" type="text" value="<?php echo esc_attr($title); ?>" /></p>
    111 
    112                 <p><label for="<?php echo $this->get_field_id( 'text' ); ?>"><?php _e( 'Content:' ); ?></label>
    113                 <textarea class="widefat" rows="16" cols="20" id="<?php echo $this->get_field_id('text'); ?>" name="<?php echo $this->get_field_name('text'); ?>"><?php echo esc_textarea( $instance['text'] ); ?></textarea></p>
     178                <input id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" class="title" type="hidden" value="<?php echo esc_attr( $instance['title'] ); ?>">
     179                <input id="<?php echo $this->get_field_id( 'text' ); ?>" name="<?php echo $this->get_field_name( 'text' ); ?>" class="text" type="hidden" value="<?php echo esc_attr( $instance['text'] ); ?>">
     180                <?php
     181        }
    114182
    115                 <p><input id="<?php echo $this->get_field_id('filter'); ?>" name="<?php echo $this->get_field_name('filter'); ?>" type="checkbox"<?php checked( $filter ); ?> />&nbsp;<label for="<?php echo $this->get_field_id('filter'); ?>"><?php _e('Automatically add paragraphs'); ?></label></p>
     183        /**
     184         * Render form template scripts.
     185         *
     186         * @since 4.8.0
     187         * @access public
     188         */
     189        public function render_control_template_scripts() {
     190                ?>
     191                <script type="text/html" id="tmpl-widget-text-control-fields">
     192                        <# var elementIdPrefix = 'el' + String( Math.random() ).replace( /\D/g, '' ) + '_' #>
     193                        <p>
     194                                <label for="{{ elementIdPrefix }}title"><?php esc_html_e( 'Title:' ); ?></label>
     195                                <input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
     196                        </p>
     197                        <p>
     198                                <label for="{{ elementIdPrefix }}text" class="screen-reader-text"><?php esc_html_e( 'Content:' ); ?></label>
     199                                <textarea id="{{ elementIdPrefix }}text" class="widefat text" style="height: 200px" rows="16" cols="20"></textarea>
     200                        </p>
     201                </script>
    116202                <?php
    117203        }
    118204}