WordPress.org

Make WordPress Core

Ticket #38342: 38342.11.diff

File 38342.11.diff, 16.7 KB (added by joehoyle, 4 years ago)

Offload validation to the backend, handled i18n automatically

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

    diff --git a/src/wp-admin/css/dashboard.css b/src/wp-admin/css/dashboard.css
    index 72b0230..3ef5d82 100644
    a b form.initial-form.quickpress-open input#title { 
    518518        resize: none;
    519519}
    520520
     521#quick-press.is-saving .spinner {
     522        visibility: inherit;
     523}
     524
    521525/* Dashboard Quick Draft - Drafts list */
    522526
    523527.js #dashboard_quick_press .drafts {
    form.initial-form.quickpress-open input#title { 
    541545        margin: 0 12px;
    542546}
    543547
     548#dashboard_quick_press .drafts ul.is-placeholder li {
     549        padding: 3px 0;
     550        color: transparent;
     551}
     552
     553@keyframes loading-fade {
     554        0% { opacity: .5; }
     555        50% { opacity: 1; }
     556        100% { opacity: .5; }
     557}
     558
     559#dashboard_quick_press .drafts ul.is-placeholder li:before,
     560#dashboard_quick_press .drafts ul.is-placeholder li:after {
     561        content: '';
     562        display: block;
     563        height: 13px;
     564        background: #eee;
     565        animation: loading-fade 1.6s ease-in-out infinite;
     566}
     567
     568#dashboard_quick_press .drafts ul.is-placeholder li:before {
     569        margin-bottom: 5px;
     570        width: 40%;
     571}
     572
     573#dashboard_quick_press .drafts ul.is-placeholder li:after {
     574        width: 80%;
     575}
     576
    544577#dashboard_quick_press .drafts li {
    545578        margin-bottom: 1em;
    546579}
     580
     581#dashboard_quick_press .drafts li.is-new {
     582        background-color: #fffbe5;
     583}
     584
    547585#dashboard_quick_press .drafts li time {
    548586        color: #72777c;
    549587}
  • src/wp-admin/includes/dashboard.php

    diff --git a/src/wp-admin/includes/dashboard.php b/src/wp-admin/includes/dashboard.php
    index 0ecf0a2..ff0c081 100644
    a b function wp_dashboard_setup() { 
    5050        if ( is_blog_admin() && current_user_can( get_post_type_object( 'post' )->cap->create_posts ) ) {
    5151                $quick_draft_title = sprintf( '<span class="hide-if-no-js">%1$s</span> <span class="hide-if-js">%2$s</span>', __( 'Quick Draft' ), __( 'Drafts' ) );
    5252                wp_add_dashboard_widget( 'dashboard_quick_press', $quick_draft_title, 'wp_dashboard_quick_press' );
     53                add_action( 'admin_footer', 'wp_dashboard_print_recent_drafts_template' );
    5354        }
    5455
    5556        // WordPress News
    function wp_dashboard_quick_press( $error_msg = false ) { 
    494495        $post_ID = (int) $post->ID;
    495496?>
    496497
    497         <form name="post" action="<?php echo esc_url( admin_url( 'post.php' ) ); ?>" method="post" id="quick-press" class="initial-form hide-if-no-js">
    498 
    499                 <?php if ( $error_msg ) : ?>
    500                 <div class="error"><?php echo $error_msg; ?></div>
    501                 <?php endif; ?>
     498        <form name="post" method="post" id="quick-press" class="initial-form hide-if-no-js">
    502499
    503500                <div class="input-text-wrap" id="title-wrap">
    504                         <label class="screen-reader-text prompt" for="title" id="title-prompt-text">
     501                        <label class="prompt" for="title" id="title-prompt-text">
    505502
    506503                                <?php
    507504                                /** This filter is documented in wp-admin/edit-form-advanced.php */
    508505                                echo apply_filters( 'enter_title_here', __( 'Title' ), $post );
    509506                                ?>
    510507                        </label>
    511                         <input type="text" name="post_title" id="title" autocomplete="off" />
     508                        <input type="text" name="title" id="title" autocomplete="off" />
    512509                </div>
    513510
    514511                <div class="textarea-wrap" id="description-wrap">
    515                         <label class="screen-reader-text prompt" for="content" id="content-prompt-text"><?php _e( 'What&#8217;s on your mind?' ); ?></label>
     512                        <label class="prompt" for="content" id="content-prompt-text"><?php _e( 'What&#8217;s on your mind?' ); ?></label>
    516513                        <textarea name="content" id="content" class="mceEditor" rows="3" cols="15" autocomplete="off"></textarea>
    517514                </div>
    518 
    519515                <p class="submit">
    520                         <input type="hidden" name="action" id="quickpost-action" value="post-quickdraft-save" />
    521                         <input type="hidden" name="post_ID" value="<?php echo $post_ID; ?>" />
    522                         <input type="hidden" name="post_type" value="post" />
    523                         <?php wp_nonce_field( 'add-post' ); ?>
     516                        <div class="error inline" style="display: none;"><p></p></div>
     517                        <div class="spinner no-float"></div>
    524518                        <?php submit_button( __( 'Save Draft' ), 'primary', 'save', false, array( 'id' => 'save-post' ) ); ?>
    525519                        <br class="clear" />
    526520                </p>
    527521
    528522        </form>
     523        <div id="quick-press-drafts" class="drafts">
     524                <p class="view-all" style="display: none;">
     525                        <a href="<?php echo esc_url( admin_url( 'edit.php?post_status=draft' ) ) ?>" aria-label="<?php esc_attr_e( 'View all drafts' ) ?>"><?php _ex( 'View all', 'drafts' ) ?></a>
     526                </p>
     527                <h2 class="hide-if-no-js"><?php esc_html_e( 'Drafts' ) ?></h2>
     528                <ul class="drafts-list is-placeholder">
     529                        <li><span class="screen-reader-text"><?php esc_html_e( 'Loading…' ) ?></span></li>
     530                </ul>
     531        </div>
    529532        <?php
    530         wp_dashboard_recent_drafts();
    531533}
    532534
    533535/**
    function wp_dashboard_recent_drafts( $drafts = false ) { 
    586588}
    587589
    588590/**
     591 * Get the HTML template for the Quick Draft recent posts.
     592 *
     593 * @since 4.7.0
     594 *
     595 * @return string The template HTML.
     596 */
     597function wp_dashboard_print_recent_drafts_template() {
     598        ?>
     599        <script id="tmpl-item-quick-press-draft" type="text/template">
     600                <div class="draft-title">
     601                        <a href="post.php?post={{ data.id }}&action=edit" aria-label="<?php esc_attr_e( 'Edit Post' ) ?>">{{ data.title.rendered }}</a>
     602                        <time datetime="{{ data.date }}">{{ data.formattedDate }}</time>
     603                </div>
     604                {{{ data.formattedContent }}}
     605        </script>
     606        <?php
     607}
     608
     609/**
    589610 * Outputs a row for the Recent Comments widget.
    590611 *
    591612 * @access private
  • src/wp-admin/js/dashboard.js

    diff --git a/src/wp-admin/js/dashboard.js b/src/wp-admin/js/dashboard.js
    index fa100dd..23ec7f9 100644
    a b  
    1 /* global pagenow, ajaxurl, postboxes, wpActiveEditor:true */
    2 var ajaxWidgets, ajaxPopulateWidgets, quickPressLoad;
     1/* global _, wp, quickPress, pagenow, ajaxurl, postboxes, wpActiveEditor:true */
     2var ajaxWidgets, ajaxPopulateWidgets;
    33
    44jQuery(document).ready( function($) {
    55        var welcomePanel = $( '#welcome-panel' ),
    jQuery(document).ready( function($) { 
    6161
    6262        postboxes.add_postbox_toggles(pagenow, { pbshow: ajaxPopulateWidgets } );
    6363
    64         /* QuickPress */
    65         quickPressLoad = function() {
    66                 var act = $('#quickpost-action'), t;
    67 
    68                 $( '#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]' ).prop( 'disabled' , false );
    69 
    70                 t = $('#quick-press').submit( function( e ) {
    71                         e.preventDefault();
    72                         $('#dashboard_quick_press #publishing-action .spinner').show();
    73                         $('#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]').prop('disabled', true);
    74 
    75                         $.post( t.attr( 'action' ), t.serializeArray(), function( data ) {
    76                                 // Replace the form, and prepend the published post.
    77                                 $('#dashboard_quick_press .inside').html( data );
    78                                 $('#quick-press').removeClass('initial-form');
    79                                 quickPressLoad();
    80                                 highlightLatestPost();
    81                                 $('#title').focus();
    82                         });
    83 
    84                         function highlightLatestPost () {
    85                                 var latestPost = $('.drafts ul li').first();
    86                                 latestPost.css('background', '#fffbe5');
    87                                 setTimeout(function () {
    88                                         latestPost.css('background', 'none');
    89                                 }, 1000);
    90                         }
    91                 } );
    92 
    93                 $('#publish').click( function() { act.val( 'post-quickpress-publish' ); } );
    94 
    95                 $('#title, #tags-input, #content').each( function() {
    96                         var input = $(this), prompt = $('#' + this.id + '-prompt-text');
    97 
    98                         if ( '' === this.value ) {
    99                                 prompt.removeClass('screen-reader-text');
    100                         }
    101 
    102                         prompt.click( function() {
    103                                 $(this).addClass('screen-reader-text');
    104                                 input.focus();
    105                         });
    106 
    107                         input.blur( function() {
    108                                 if ( '' === this.value ) {
    109                                         prompt.removeClass('screen-reader-text');
    110                                 }
    111                         });
    112 
    113                         input.focus( function() {
    114                                 prompt.addClass('screen-reader-text');
    115                         });
    116                 });
    117 
    118                 $('#quick-press').on( 'click focusin', function() {
    119                         wpActiveEditor = 'content';
    120                 });
    121 
    122                 autoResizeTextarea();
    123         };
    124         quickPressLoad();
    125 
    12664        $( '.meta-box-sortables' ).sortable( 'option', 'containment', '#wpwrap' );
    12765
    12866        function autoResizeTextarea() {
    jQuery(document).ready( function($) { 
    186124                });
    187125        }
    188126
     127        autoResizeTextarea();
     128
     129});
     130
     131wp.api.loadPromise.done( function() {
     132        var $ = jQuery,
     133                QuickPress = {},
     134                draftsCollection;
     135
     136        /**
     137         * Collections
     138         */
     139
     140        QuickPress.Views = {};
     141
     142        QuickPress.Views.Form = wp.Backbone.View.extend( {
     143                events: {
     144                        'click :input': 'hidePromptAndFocus',
     145                        'focus :input': 'hidePrompt',
     146                        'blur :input': 'showPrompt',
     147                        reset: 'showAllPrompts',
     148                        click: 'setActiveEditor',
     149                        focusin: 'setActiveEditor',
     150                        submit: 'submit'
     151                },
     152
     153                initialize: function() {
     154                        this.showAllPrompts();
     155
     156                        this.listenTo( this.model, 'invalid', this.render );
     157                        this.listenTo( this.model, 'error', this.showSyncError );
     158                },
     159
     160                togglePrompt: function( element, visible ) {
     161                        var $input = $( element ),
     162                                hasContent = $input.val().length > 0;
     163
     164                        $( element ).siblings( '.prompt' ).toggleClass( 'screen-reader-text', ! visible || hasContent );
     165                },
     166
     167                showAllPrompts: function() {
     168                        this.$el.find( ':input' ).each( _.bind( function( i, input ) {
     169                                // Prompt toggling must be deferred because the reset event is
     170                                // fired before the input values have been cleared
     171                                _.defer( _.bind( this.togglePrompt, this, input, true ) );
     172                        }, this ) );
     173                },
     174
     175                showPrompt: function( event ) {
     176                        this.togglePrompt( event.target, true );
     177                },
     178
     179                hidePrompt: function( event ) {
     180                        this.togglePrompt( event.target, false );
     181                },
     182
     183                hidePromptAndFocus: function( event ) {
     184                        this.togglePrompt( event.target, false );
     185                        $( ':input', event.target ).focus();
     186                },
     187
     188                setActiveEditor: function() {
     189                        wpActiveEditor = 'content';
     190                },
     191
     192                showSyncError: function( model, error ) {
     193                        try {
     194                                this.syncError = JSON.parse( error.responseText ).message;
     195                        } catch( e ) {
     196                                this.syncError = quickPress.l10n.error;
     197                        }
     198
     199                        this.render();
     200                },
     201
     202                submit: function( event ) {
     203                        var values;
     204
     205                        delete this.syncError;
     206                        event.preventDefault();
     207
     208                        // jQuery's serializeArray returns an array of field tuples, which
     209                        // we need to transform into an object before sending to API
     210                        values = _.reduce( this.$el.serializeArray(), function( memo, field ) {
     211                                memo[ field.name ] = field.value;
     212                                return memo;
     213                        }, {} );
     214
     215                        // Ensure that by setting these fields on model that it is valid
     216                        // before proceeding with save
     217                        this.model.set( values );
     218                        if ( ! this.model.isValid() ) {
     219                                return;
     220                        }
     221
     222                        // Show a spinner during the callback.
     223                        this.$el.addClass( 'is-saving' );
     224
     225                        this.model.save()
     226                                .always( _.bind( function() {
     227                                        this.$el.removeClass( 'is-saving' );
     228                                }, this ) )
     229                                .success( _.bind( function() {
     230                                        this.collection.add( this.model, { at: 0 } );
     231                                        this.model = new wp.api.models.Post();
     232                                        this.el.reset();
     233                                }, this ) );
     234                },
     235
     236                render: function() {
     237                        var $error = this.$el.find( '.error' ),
     238                                errorText;
     239
     240                        if ( this.syncError ) {
     241                                // Error via API save failure
     242                                errorText = this.syncError;
     243                        }
     244
     245                        // Error notice is only visible if error text determined
     246                        $error.toggle( !! errorText );
     247                        if ( errorText ) {
     248                                $error.html( $( '<p />', { text: errorText } ) );
     249                        }
     250                }
     251        } );
     252
     253        QuickPress.Views.DraftList = wp.Backbone.View.extend( {
     254
     255                initialize: function() {
     256                        this.listenTo( this.collection, 'sync', this.onDraftsLoaded );
     257                },
     258
     259                onDraftsLoaded: function() {
     260                        this.listenTo( this.collection, 'add', this.renderNew );
     261                        this.render();
     262                },
     263
     264                renderNew: function() {
     265                        // Display highlight effect to first (added) item for one second
     266                        var $newEl = this.render().$el.find( 'li:first' ).addClass( 'is-new' );
     267                        setTimeout( function() {
     268                                $newEl.removeClass( 'is-new' );
     269                        }, 1000 );
     270                },
     271
     272                render: function() {
     273                        // Hide drafts list if no drafts exist
     274                        this.$el.toggle( this.collection.length > 0 );
     275
     276                        // "View All" link is visible if 5 or more drafts, since we only
     277                        // show a maximum of 5 drafts in the list
     278                        this.$el.find( '.view-all' ).toggle( this.collection.length > 0 );
     279
     280                        // If after drafts load, this could be the first render, so remove
     281                        // placeholder effect and render the first four drafts
     282                        this.$el.find( '.drafts-list' )
     283                                .removeClass( 'is-placeholder' )
     284                                .html( _.map( this.collection.models, function( draft ) {
     285                                        return new QuickPress.Views.DraftListItem( {
     286                                                model: draft
     287                                        } ).render().el;
     288                                } ) );
     289
     290                        return this;
     291                }
     292        } );
     293
     294        QuickPress.Views.DraftListItem = wp.Backbone.View.extend( {
     295                tagName: 'li',
     296
     297                template: wp.template( 'item-quick-press-draft' ),
     298
     299                render: function() {
     300                        var attributes = _.clone( this.model.attributes );
     301                        attributes.formattedContent = wp.formatting.trimWords( attributes.content.rendered, 10 );
     302
     303                        // We can format dates using newer browser i18n features, but also
     304                        // provide a fallback to the not-as-nice Date#toLocaleDateString
     305                        date = new Date( wp.api.utils.parseISO8601( attributes.date + quickPress.timezoneOffset ) );
     306                        if ( 'undefined' !== typeof Intl && Intl.DateTimeFormat ) {
     307                                attributes.formattedDate = new Intl.DateTimeFormat( undefined, {
     308                                        month: 'long',
     309                                        day: 'numeric',
     310                                        year: 'numeric'
     311                                } ).format( date );
     312                        } else {
     313                                attributes.formattedDate = date.toLocaleDateString();
     314                        }
     315
     316                        this.$el.html( this.template( attributes ) );
     317
     318                        return this;
     319                }
     320        } );
     321
     322        /**
     323         * Initialize
     324         */
     325
     326        // Fetch drafts
     327        draftsCollection = new wp.api.collections.Posts();
     328        draftsCollection.fetch( {
     329                data: {
     330                        status: 'draft',
     331                        author: quickPress.currentUserId,
     332                        per_page: 4,
     333                        order_by: 'date',
     334                        'quick-draft-post-list': true
     335                }
     336        } );
     337
     338        // Drafts list is initialized but not rendered until drafts load
     339        new QuickPress.Views.DraftList( {
     340                el: '#quick-press-drafts',
     341                collection: draftsCollection
     342        } );
     343
     344        new QuickPress.Views.Form( {
     345                el: '#quick-press',
     346                model: new wp.api.models.Post(),
     347                collection: draftsCollection
     348        } ).render();
    189349} );
  • src/wp-includes/js/wp-util.js

    diff --git a/src/wp-includes/js/wp-util.js b/src/wp-includes/js/wp-util.js
    index 527441d..79ab07e 100644
    a b window.wp = window.wp || {}; 
    121121                }
    122122        };
    123123
     124        // wp.formatting
     125        // ------
     126        //
     127        // Tools for formatting strings
     128        wp.formatting = {
     129                settings: settings.formatting || {},
     130
     131                /**
     132                 * Trims text to a certain number of words.
     133                 *
     134                 * @see wp_trim_words
     135                 *
     136                 * @param  {string}  text     Text to trim.
     137                 * @param  {?number} numWords Number of words (default: 55).
     138                 * @param  {?string} more     What to append if text needs to be trimmed (default: '…').
     139                 * @return {string}           Trimmed text.
     140                 */
     141                trimWords: function( text, numWords, more ) {
     142                        var words, separator;
     143
     144                        if ( 'undefined' === typeof numWords ) {
     145                                numWords = 55;
     146                        }
     147
     148                        if ( 'undefined' === typeof more ) {
     149                                more = wp.formatting.settings.trimWordsMore;
     150                        }
     151
     152                        text = text.replace( /[\n\r\t ]+/g, ' ' ).replace( /^ | $/g, '' );
     153
     154                        if ( wp.formatting.settings.trimWordsByCharacter ) {
     155                                separator = '';
     156                        } else {
     157                                separator = ' ';
     158                        }
     159
     160                        words = text.split( separator );
     161
     162                        if ( words.length <= numWords ) {
     163                                return words.join( separator );
     164                        }
     165
     166                        return words.slice( 0, numWords ).join( separator ) + more;
     167                }
     168        };
     169
    124170}(jQuery));
  • src/wp-includes/script-loader.php

    diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php
    index be32073..31a7950 100644
    a b function wp_default_scripts( &$scripts ) { 
    336336                'ajax' => array(
    337337                        'url' => admin_url( 'admin-ajax.php', 'relative' ),
    338338                ),
     339                'formatting' => array(
     340                        'trimWordsMore'  => __( '&hellip;' ),
     341                        /*
     342                         * translators: If your word count is based on single characters (e.g. East Asian characters),
     343                         * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
     344                         * Do not translate into your own language.
     345                         */
     346                        'trimWordsByCharacter' => strpos( _x( 'words', 'Word count type. Do not translate!' ), 'characters' ) === 0 && preg_match( '/^utf\-?8$/i', get_option( 'blog_charset' ) ),
     347                ),
    339348        ) );
    340349
    341350        $scripts->add( 'wp-backbone', "/wp-includes/js/wp-backbone$suffix.js", array('backbone', 'wp-util'), false, 1 );
    function wp_default_scripts( &$scripts ) { 
    721730                        'current' => __( 'Current Color' ),
    722731                ) );
    723732
    724                 $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox' ), false, 1 );
     733                $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox', 'wp-api', 'wp-backbone' ), false, 1 );
     734                did_action( 'init' ) && $scripts->localize( 'dashboard', 'quickPress', array(
     735                        'currentUserId'  => get_current_user_id(),
     736                        'l10n' => array(
     737                                'error'      => __( 'An error has occurred. Please reload the page and try again.' ),
     738                        ),
     739                        'timezoneOffset' => ( get_option( 'gmt_offset' ) >= 0 ? '+' : '-' ) . date( 'H:i', abs( get_option( 'gmt_offset' ) * 3600 ) ),
     740                ) );
    725741
    726742                $scripts->add( 'list-revisions', "/wp-includes/js/wp-list-revisions$suffix.js" );
    727743