WordPress.org

Make WordPress Core

Ticket #38342: 38342.6.diff

File 38342.6.diff, 39.2 KB (added by adamsilverstein, 3 years ago)
  • src/wp-admin/css/dashboard.css

    diff --git src/wp-admin/css/dashboard.css src/wp-admin/css/dashboard.css
    index d467f0c..80f0292 100644
    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@-webkit-keyframes loading-fade {
     554
     555        0% {
     556                opacity: 0.5;
     557        }
     558
     559        50% {
     560                opacity: 1;
     561        }
     562
     563        100% {
     564                opacity: 0.5;
     565        }
     566}
     567
     568@keyframes loading-fade {
     569
     570        0% {
     571                opacity: 0.5;
     572        }
     573
     574        50% {
     575                opacity: 1;
     576        }
     577
     578        100% {
     579                opacity: 0.5;
     580        }
     581}
     582
     583#dashboard_quick_press .drafts ul.is-placeholder li:before,
     584#dashboard_quick_press .drafts ul.is-placeholder li:after {
     585        content: "";
     586        display: block;
     587        height: 13px;
     588        background: #eee;
     589        -webkit-animation: loading-fade 1.6s ease-in-out infinite;
     590        animation: loading-fade 1.6s ease-in-out infinite;
     591}
     592
     593#dashboard_quick_press .drafts ul.is-placeholder li:before {
     594        margin-bottom: 5px;
     595        width: 40%;
     596}
     597
     598#dashboard_quick_press .drafts ul.is-placeholder li:after {
     599        width: 80%;
     600}
     601
    544602#dashboard_quick_press .drafts li {
    545603        margin-bottom: 1em;
    546604}
     605
     606#dashboard_quick_press .drafts li.is-new {
     607        background-color: #fffbe5;
     608}
     609
    547610#dashboard_quick_press .drafts li time {
    548611        color: #72777c;
    549612}
  • src/wp-admin/includes/dashboard.php

    diff --git src/wp-admin/includes/dashboard.php src/wp-admin/includes/dashboard.php
    index 40035f8..7c0f4b3 100644
    function wp_dashboard_quick_press( $error_msg = false ) { 
    494494        $post_ID = (int) $post->ID;
    495495?>
    496496
    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; ?>
    502 
     497        <form name="post" method="post" id="quick-press" class="initial-form hide-if-no-js">
     498                <div class="notice notice-error notice-alt inline hidden"><p></p></div>
    503499                <div class="input-text-wrap" id="title-wrap">
    504                         <label class="screen-reader-text prompt" for="title" id="title-prompt-text">
     500                        <label class="prompt" for="title" id="title-prompt-text">
    505501
    506502                                <?php
    507503                                /** This filter is documented in wp-admin/edit-form-advanced.php */
    508504                                echo apply_filters( 'enter_title_here', __( 'Title' ), $post );
    509505                                ?>
    510506                        </label>
    511                         <input type="text" name="post_title" id="title" autocomplete="off" />
     507                        <input type="text" name="title" id="title" autocomplete="off" />
    512508                </div>
    513509
    514510                <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>
     511                        <label class="prompt" for="content" id="content-prompt-text"><?php _e( 'What&#8217;s on your mind?' ); ?></label>
    516512                        <textarea name="content" id="content" class="mceEditor" rows="3" cols="15" autocomplete="off"></textarea>
    517513                </div>
    518 
    519514                <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' ); ?>
     515                        <div class="spinner no-float"></div>
    524516                        <?php submit_button( __( 'Save Draft' ), 'primary', 'save', false, array( 'id' => 'save-post' ) ); ?>
    525517                        <br class="clear" />
    526518                </p>
    527519
    528520        </form>
     521        <div id="quick-press-drafts" class="drafts">
     522                <p class="view-all" style="display: none;">
     523                        <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>
     524                </p>
     525                <h2 class="hide-if-no-js"><?php _e( 'Drafts' ) ?></h2>
     526                <ul class="drafts-list is-placeholder">
     527                        <li><span class="screen-reader-text"><?php _e( 'Loading&hellip;' ) ?></span></li>
     528                </ul>
     529        </div>
     530        <script id="tmpl-item-quick-press-draft" type="text/template">
     531                <div class="draft-title">
     532                        <a href="<?php echo ( esc_url( admin_url( 'post.php?post={{ data.id }}&action=edit' ) ) ); ?>" aria-label="<?php esc_attr_e( 'Edit Post' ) ?>">{{ data.formattedTitle }}</a>
     533                        <time datetime="{{ data.date }}">{{ data.formattedDate }}</time>
     534                </div>
     535                {{{ data.formattedContent }}}
     536        </script>
    529537        <?php
    530         wp_dashboard_recent_drafts();
    531538}
    532539
    533540/**
    534541 * Show recent drafts of the user on the dashboard.
    535542 *
    536543 * @since 2.7.0
     544 * @deprecated 4.8.0
    537545 *
    538546 * @param array $drafts
    539547 */
    function wp_dashboard_recent_drafts( $drafts = false ) { 
    548556                        'order'          => 'DESC'
    549557                );
    550558
    551                 /**
    552                  * Filters the post query arguments for the 'Recent Drafts' dashboard widget.
    553                  *
    554                  * @since 4.4.0
    555                  *
    556                  * @param array $query_args The query arguments for the 'Recent Drafts' dashboard widget.
    557                  */
     559                /** This filter is documented in wp-includes/rest-api.php */
    558560                $query_args = apply_filters( 'dashboard_recent_drafts_query_args', $query_args );
    559561
    560562                $drafts = get_posts( $query_args );
  • src/wp-admin/js/dashboard.js

    diff --git src/wp-admin/js/dashboard.js src/wp-admin/js/dashboard.js
    index fa100dd..6dee942 100644
     
    1 /* global pagenow, ajaxurl, postboxes, wpActiveEditor:true */
    2 var ajaxWidgets, ajaxPopulateWidgets, quickPressLoad;
     1/* global _, wp, quickDraft, pagenow, ajaxurl, postboxes */
     2var ajaxWidgets, ajaxPopulateWidgets, QuickDraft = {};
    33
    44jQuery(document).ready( function($) {
    55        var welcomePanel = $( '#welcome-panel' ),
    jQuery(document).ready( function($) { 
    5959        };
    6060        ajaxPopulateWidgets();
    6161
    62         postboxes.add_postbox_toggles(pagenow, { pbshow: ajaxPopulateWidgets } );
    63 
    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                         });
     62        postboxes.add_postbox_toggles(pagenow, { pbshow: function( id ) {
     63                ajaxPopulateWidgets();
    10664
    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();
     65                if ( 'dashboard_quick_press' === id ) {
     66                        QuickDraft.init();
     67                }
     68        } } );
    12569
    12670        $( '.meta-box-sortables' ).sortable( 'option', 'containment', '#wpwrap' );
    12771
    jQuery(document).ready( function($) { 
    186130                });
    187131        }
    188132
     133        autoResizeTextarea();
     134
     135        if ( jQuery( '#dashboard_quick_press' ).is( ':visible' ) ) {
     136                QuickDraft.init();
     137        }
     138});
     139
     140// Set up the QuickDraft views.
     141QuickDraft.Views = {};
     142
     143/**
     144 * Set up a view object for the quick draft form.
     145 *
     146 * @since 4.8.0
     147 *
     148 * @augments wp.Backbone.View
     149 */
     150QuickDraft.Views.Form = wp.Backbone.View.extend( {
     151
     152        // Set up our default action handlers.
     153        events: {
     154
     155                // Hide the prompt whena field receives focus.
     156                'focus :input': 'hidePrompt',
     157
     158                // Possibly re-display the prompt when a field looses focus.
     159                'blur :input':  'showPrompt',
     160
     161                // Listen for a reset event, showing all prompts.
     162                'reset':        'showAllPrompts',
     163
     164                // Listed for and handle form submissions.
     165                'submit':       'handleFormSubmission'
     166        },
     167
     168        // Initialize the QuickDraft Form view.
     169        initialize: function() {
     170
     171                // Show all prompts in the form to begin.
     172                this.showAllPrompts();
     173
     174                // Rerender if the error state changes.
     175                quickDraft.state.on( 'change:errorState', _.bind( this.render, this ) );
     176        },
     177
     178        /**
     179         * Handle toggling of the helper prompts shown inside each field.
     180         *
     181         * Will only show the prompt if the field is empty.
     182         *
     183         * @param  {object}  element Field element containing the helper prompt.
     184         * @param  {boolean} visible Should the prompt be visible?
     185         */
     186        togglePrompt: function( element, visible ) {
     187                var $input = jQuery( element ),
     188                        hasContent = $input.val().length > 0;
     189
     190                // Set the visibility of the elements nearest prompt to passed 'visible' value.
     191                jQuery( element ).siblings( '.prompt' ).toggleClass( 'screen-reader-text', ! visible || hasContent );
     192        },
     193
     194        // Show all of the field promts.
     195        showAllPrompts: function() {
     196
     197                // Show all of the field prompts.
     198                this.$el.find( ':input' ).each( _.bind( function( i, input ) {
     199                        _.defer( _.bind( this.togglePrompt, this, input, true ) );
     200                }, this ) );
     201        },
     202
     203        /**
     204         * Show the prompt inside a field.
     205         *
     206         * @param {object} event The event triggering this show request.
     207         */
     208        showPrompt: function( event ) {
     209                this.togglePrompt( event.target, true );
     210        },
     211
     212        /**
     213         * Hide the prompt inside a field.
     214         *
     215         * @param {object} event The event triggering this hide request.
     216         */
     217        hidePrompt: function( event ) {
     218                this.togglePrompt( event.target, false );
     219        },
     220
     221        /**
     222         * Handle error conditions.
     223         *
     224         * {string} error The error condition, or false to reset.
     225         */
     226        setErrorState: function( error ) {
     227
     228                // Set or reset the app state error condition.
     229                quickDraft.state.set( 'errorState', error );
     230
     231                if ( false !== error ) {
     232
     233                        // Alert screen readers that an error occurred.
     234                        wp.a11y.speak( error, 'assertive' );
     235                }
     236
     237        },
     238
     239        /**
     240         * Handle the form submission event.
     241         *
     242         * @param {object} event The form submission event.
     243         */
     244        handleFormSubmission: function( event ) {
     245                var values,
     246                        hasValuesToSave = false;
     247
     248                // Prevent the browser's default form submission handling.
     249                event.preventDefault();
     250
     251                // Prevent double submissions by checking the submitting state.
     252                if ( quickDraft.state.get( 'submitting' ) ) {
     253                        return;
     254                }
     255
     256                // Reset the error state.
     257                this.setErrorState( false );
     258
     259                // Extract the form field values.
     260                values = _.reduce( this.$el.serializeArray(), function( memo, field ) {
     261                        memo[ field.name ] = field.value;
     262                        hasValuesToSave    = hasValuesToSave || ( '' !== field.value );
     263                        return memo;
     264                }, {} );
     265
     266                // If the values are all blank, show an error.
     267                if ( ! hasValuesToSave ) {
     268
     269                        // Set the error.
     270                        this.setErrorState( quickDraft.l10n.errorEmptyFields );
     271                        return;
     272                }
     273
     274                // Save the new values to the model and confirm they are valid.
     275                this.model.set( values );
     276                if ( ! this.model.isValid() ) {
     277                        return;
     278                }
     279
     280                // Show a spinner during the callback.
     281                this.$el.addClass( 'is-saving' );
     282
     283
     284                // Set the state sbmitting to avoid double saves
     285                quickDraft.state.set( 'submitting', true );
     286
     287                // Trigger the model save.
     288                this.model.save()
     289
     290                        // Always remove the spinner.
     291                        .always(
     292                                _.bind( function() {
     293                                        this.$el.removeClass( 'is-saving' );
     294
     295                                        // Submission complete
     296                                        quickDraft.state.set( 'submitting', false );
     297
     298                                }, this )
     299                        )
     300
     301                        // Handle save success.
     302                        .success(
     303                                _.bind( function() {
     304                                        // Success! Clear any previous error state.
     305                                         this.render();
     306
     307                                        // Add the post model to the head of our collection.
     308                                        this.collection.add( this.model, { at: 0 } );
     309
     310                                        // Create a new post model to contain the form data and reset the form.
     311                                        this.model = new wp.api.models.Post();
     312                                        this.model.on( 'change:errorState', _.bind( this.render, this ) );
     313
     314                                        this.el.reset();
     315                                }, this )
     316                        )
     317
     318                        // Handle save failure.
     319                        .error(
     320                                _.bind( function( model, error ) {
     321                                        var message = '';
     322
     323                                        // Try to parse and use the response message.
     324                                        try {
     325                                                message = JSON.parse( error.responseText ).message;
     326                                        } catch( e ) {
     327
     328                                                // Fall back to a default error string if the parse fails.
     329                                                message = quickDraft.l10n.error;
     330                                        }
     331
     332                                        // Set the app error condition.
     333                                        this.setErrorState( message );
     334                                }, this )
     335                        );
     336        },
     337
     338        // Render the form view.
     339        render: function() {
     340                var $error    = this.$el.find( '.notice-alt' ),
     341                        errorText = quickDraft.state.get( 'errorState' );
     342
     343                // Error notice is only visible if error text is set.
     344                $error.toggleClass( 'hidden', ! errorText );
     345                if ( errorText ) {
     346
     347                        // Note: The inner text transform prevents XSS via html().
     348                        $error.html( jQuery( '<p />', { text: errorText } ) );
     349                }
     350        }
     351} );
     352
     353/**
     354 * Set up a view object for the Quick Draft list of drafts.
     355 *
     356 * @since 4.8.0
     357 *
     358 * @augments wp.Backbone.View
     359 */
     360QuickDraft.Views.DraftList = wp.Backbone.View.extend( {
     361
     362        // Initialize the draft list view.
     363        initialize: function() {
     364
     365                // Render the view once the drafts have loaded.
     366                this.listenTo( this.collection, 'sync', this.onDraftsLoaded );
     367        },
     368
     369        // Once the drafts have loaded, complete the setup.
     370        onDraftsLoaded: function() {
     371
     372                // Add a listener for new items added to the underlying (draft) post collection.
     373                this.listenTo( this.collection, 'add', this.renderNew );
     374
     375                // Render the view!
     376                this.render();
     377        },
     378
     379        // Handle a new item being added to the collection.
     380        renderNew: function() {
     381
     382                // Display highlight effect to first (added) item for one second.
     383                var $newEl = this.render().$el.find( 'li:first' ).addClass( 'is-new' );
     384                setTimeout( function() {
     385                        $newEl.removeClass( 'is-new' );
     386                }, 1000 );
     387
     388                // Alert screen readers that a new draft has been added.
     389                wp.a11y.speak( quickDraft.l10n.newDraftCreated, 'assertive' );
     390        },
     391
     392        // Render the draft post list view.
     393        render: function() {
     394
     395                // Hide drafts list entirely if no drafts exist.
     396                this.$el.toggle( this.collection.length > 0 );
     397
     398                // Display a 'View All' link if there are more drafts available.
     399                this.$el.find( '.view-all' ).toggle( this.collection.hasMore() );
     400
     401                // Remove the placeholder class and render the models.
     402                this.$el.find( '.drafts-list' )
     403                        .removeClass( 'is-placeholder' )
     404                        .html(
     405                                _.map( this.collection.models, function( draft ) {
     406                                        return new QuickDraft.Views.DraftListItem( {
     407                                                model: draft
     408                                        } ).render().el;
     409                                } )
     410                        );
     411
     412                return this;
     413        }
    189414} );
     415
     416/**
     417 * Set up a view object an individual draft in the draft list.
     418 *
     419 * @since 4.8.0
     420 *
     421 * @augments wp.Backbone.View
     422 */
     423QuickDraft.Views.DraftListItem = wp.Backbone.View.extend( {
     424        tagName: 'li',
     425
     426        // Render beased on the passed template.
     427        template: wp.template( 'item-quick-press-draft' ),
     428
     429        // Render a single draft list item.
     430        render: function() {
     431
     432                // Clone the original model attributes, so we can leave the model untouched.
     433                var attributes = _.clone( this.model.attributes );
     434
     435                // Trim the content to 10 words.
     436                attributes.formattedContent = wp.formatting.trimWords( attributes.content.rendered, 10 );
     437
     438                // If the title is missing entirely, add a no title placeholder.
     439                attributes.formattedTitle = attributes.title.rendered.length > 0 ? attributes.title.rendered : quickDraft.l10n.noTitle;
     440
     441                // Format the data using Intl.DateTimeFormat with a fallback to date.toLocaleDateString.
     442                var date = new Date( wp.api.utils.parseISO8601( attributes.date + quickDraft.timezoneOffset ) );
     443                if ( 'undefined' !== typeof Intl && Intl.DateTimeFormat ) {
     444                        attributes.formattedDate = new Intl.DateTimeFormat( undefined, {
     445                                month: 'long',
     446                                day: 'numeric',
     447                                year: 'numeric'
     448                        } ).format( date );
     449                } else {
     450                        attributes.formattedDate = date.toLocaleDateString();
     451                }
     452
     453                // Output the rendered template.
     454                this.$el.html( this.template( attributes ) );
     455
     456                // Continue the rendering chain.
     457                return this;
     458        }
     459} );
     460
     461
     462/**
     463 * Initialize the Quick Draft feature.
     464 *
     465 * @since 4.8.0
     466 *
     467 */
     468QuickDraft.init = function() {
     469
     470        // Set up a state model to track the application state.
     471        quickDraft.state = new Backbone.Model({
     472                'errorState': false
     473        });
     474
     475        // Wait for the wp-api client to initialize.
     476        wp.api.loadPromise.done( function() {
     477
     478                // Fetch up to 4 of the current user's recent drafts by extending wp.api.collections.Posts.
     479                var draftsCollection = new wp.api.collections.Posts();
     480                draftsCollection.fetch( {
     481                        data: wp.hooks.applyFilters(
     482                                'dashboard_recent_drafts_fetch_args',
     483                                {
     484                                        status: 'draft',
     485                                        author: quickDraft.currentUserId,
     486                                        per_page: 4,
     487                                        order_by: 'date',
     488                                }
     489                        )
     490                } );
     491
     492                // Drafts list is initialized but not rendered until drafts load.
     493                new QuickDraft.Views.DraftList( {
     494                        el: '#quick-press-drafts',
     495                        collection: draftsCollection
     496                } );
     497
     498                new QuickDraft.Views.Form( {
     499                        el: '#quick-press',
     500                        model: new wp.api.models.Post(),
     501                        collection: draftsCollection
     502                } ).render();
     503        });
     504};
  • new file src/wp-includes/js/wp-hooks.js

    diff --git src/wp-includes/js/wp-hooks.js src/wp-includes/js/wp-hooks.js
    new file mode 100644
    index 0000000..c7b2d79
    - +  
     1( function( wp ) {
     2        'use strict';
     3
     4        /**
     5         * Contains the registered hooks, keyed by hook type. Each hook type is an
     6         * array of objects with priority and callback of each registered hook.
     7         */
     8        var HOOKS = {};
     9
     10        /**
     11         * Returns a function which, when invoked, will add a hook.
     12         *
     13         * @param  {string}   type Type for which hooks are to be added
     14         * @return {Function}      Hook added
     15         */
     16        function createAddHookByType( type ) {
     17                /**
     18                 * Adds the hook to the appropriate hooks container
     19                 *
     20                 * @param {string}   hook     Name of hook to add
     21                 * @param {Function} callback Function to call when the hook is run
     22                 * @param {?number}  priority Priority of this hook (default=10)
     23                 */
     24                return function( hook, callback, priority ) {
     25                        var hookObject, hooks;
     26                        if ( typeof hook !== 'string' || typeof callback !== 'function' ) {
     27                                return;
     28                        }
     29
     30                        // Assign default priority
     31                        if ( 'undefined' === typeof priority ) {
     32                                priority = 10;
     33                        } else {
     34                                priority = parseInt( priority, 10 );
     35                        }
     36
     37                        // Validate numeric priority
     38                        if ( isNaN( priority ) ) {
     39                                return;
     40                        }
     41
     42                        // Check if adding first of type
     43                        if ( ! HOOKS[ type ] ) {
     44                                HOOKS[ type ] = {};
     45                        }
     46
     47                        hookObject = {
     48                                callback: callback,
     49                                priority: priority
     50                        };
     51
     52                        if ( HOOKS[ type ].hasOwnProperty( hook ) ) {
     53                                // Append and re-sort amongst existing
     54                                hooks = HOOKS[ type ][ hook ];
     55                                hooks.push( hookObject );
     56                                hooks = sortHooks( hooks );
     57                        } else {
     58                                // First of its type needs no sort
     59                                hooks = [ hookObject ];
     60                        }
     61
     62                        HOOKS[ type ][ hook ] = hooks;
     63                };
     64        }
     65
     66        /**
     67         * Returns a function which, when invoked, will remove a specified hook.
     68         *
     69         * @param  {string}   type Type for which hooks are to be removed
     70         * @return {Function}      Hook remover
     71         */
     72        function createRemoveHookByType( type ) {
     73                /**
     74                 * Removes the specified hook by resetting its value.
     75                 *
     76                 * @param {string}    hook     Name of hook to remove
     77                 * @param {?Function} callback The specific callback to be removed. If
     78                 *                             omitted, clears all callbacks.
     79                 */
     80                return function( hook, callback ) {
     81                        var handlers, i;
     82
     83                        // Baily early if no hooks exist by this name
     84                        if ( ! HOOKS[ type ] || ! HOOKS[ type ].hasOwnProperty( hook ) ) {
     85                                return;
     86                        }
     87
     88                        if ( callback ) {
     89                                // Try to find specified callback to remove
     90                                handlers = HOOKS[ type ][ hook ];
     91                                for ( i = handlers.length - 1; i >= 0; i-- ) {
     92                                        if ( handlers[ i ].callback === callback ) {
     93                                                handlers.splice( i, 1 );
     94                                        }
     95                                }
     96                        } else {
     97                                // Reset hooks to empty
     98                                delete HOOKS[ type ][ hook ];
     99                        }
     100                };
     101        }
     102
     103        /**
     104         * Returns a function which, when invoked, will execute all registered
     105         * hooks of the specified type by calling upon runner with its hook name
     106         * and arguments.
     107         *
     108         * @param  {string}   type   Type for which hooks are to be run, one of 'action' or 'filter'.
     109         * @param  {Function} runner Function to invoke for each hook callback
     110         * @return {Function}        Hook runner
     111         */
     112        function createRunHookByType( type, runner ) {
     113                /**
     114                 * Runs the specified hook.
     115                 *
     116                 * @param  {string} hook The hook to run
     117                 * @param  {...*}   args Arguments to pass to the action/filter
     118                 * @return {*}           Return value of runner, if applicable
     119                 * @private
     120                 */
     121                return function( /* hook, ...args */ ) {
     122                        var args, hook;
     123
     124                        args = Array.prototype.slice.call( arguments );
     125                        hook = args.shift();
     126
     127                        if ( typeof hook === 'string' ) {
     128                                return runner( hook, args );
     129                        }
     130                };
     131        }
     132
     133        /**
     134         * Performs an action if it exists.
     135         *
     136         * @param {string} action The action to perform.
     137         * @param {...*}   args   Optional args to pass to the action.
     138         * @private
     139         */
     140        function runDoAction( action, args ) {
     141                var handlers, i;
     142                if ( HOOKS.actions ) {
     143                        handlers = HOOKS.actions[ action ];
     144                }
     145
     146                if ( ! handlers ) {
     147                        return;
     148                }
     149
     150                HOOKS.actions.current = action;
     151
     152                for ( i = 0; i < handlers.length; i++ ) {
     153                        handlers[ i ].callback.apply( null, args );
     154                        HOOKS.actions[ action ].runs = HOOKS.actions[ action ].runs ? HOOKS.actions[ action ].runs + 1 : 1;
     155                }
     156        }
     157
     158        /**
     159         * Performs a filter if it exists.
     160         *
     161         * @param  {string} filter The filter to apply.
     162         * @param  {...*}   args   Optional args to pass to the filter.
     163         * @return {*}             The filtered value
     164         * @private
     165         */
     166        function runApplyFilters( filter, args ) {
     167                var handlers, i;
     168                if ( HOOKS.filters ) {
     169                        handlers = HOOKS.filters[ filter ];
     170                }
     171
     172                if ( ! handlers ) {
     173                        return args[ 0 ];
     174                }
     175
     176                HOOKS.filters.current = filter;
     177                HOOKS.filters[ filter ].runs = HOOKS.filters[ filter ].runs ? HOOKS.filters[ filter ].runs + 1 : 1;
     178
     179                for ( i = 0; i < handlers.length; i++ ) {
     180                        args[ 0 ] = handlers[ i ].callback.apply( null, args );
     181                }
     182
     183                return args[ 0 ];
     184        }
     185
     186        /**
     187         * Use an insert sort for keeping our hooks organized based on priority.
     188         *
     189         * @see http://jsperf.com/javascript-sort
     190         *
     191         * @param  {Array} hooks Array of the hooks to sort
     192         * @return {Array}       The sorted array
     193         * @private
     194         */
     195        function sortHooks( hooks ) {
     196                var i, tmpHook, j, prevHook;
     197                for ( i = 1; i < hooks.length; i++ ) {
     198                        tmpHook = hooks[ i ];
     199                        j = i;
     200                        while ( ( prevHook = hooks[ j - 1 ] ) && prevHook.priority > tmpHook.priority ) {
     201                                hooks[ j ] = hooks[ j - 1 ];
     202                                --j;
     203                        }
     204                        hooks[ j ] = tmpHook;
     205                }
     206
     207                return hooks;
     208        }
     209
     210        /**
     211         * Checks to see if an action is currently being executed.
     212         *
     213         * @param  {string} type   Type of hooks to check, one of 'action' or 'filter'.
     214         * @param {string}  action The name of the action to check for, if omitted will check for any action being performed.
     215         *
     216         * @return {[type]}      [description]
     217         */
     218        function createDoingHookByType( type ) {
     219                return function( action ) {
     220
     221                        // If the action was not passed, check for any current hook.
     222                        if ( 'undefined' === typeof action ) {
     223                                return 'undefined' !== typeof HOOKS[ type ].current;
     224                        }
     225
     226                        // Return the current hook.
     227                        return HOOKS[ type ] && HOOKS[ type ].current ?
     228                                action === HOOKS[ type ].current :
     229                                false;
     230                };
     231        }
     232
     233        /**
     234         * Retrieve the number of times an action is fired.
     235         *
     236         * @param  {string} type   Type for which hooks to check, one of 'action' or 'filter'.
     237         * @param {string}  action The action to check.
     238         *
     239         * @return {[type]}      [description]
     240         */
     241        function createDidHookByType( type ) {
     242                return function( action ) {
     243                        return HOOKS[ type ] && HOOKS[ type ][ action ] && HOOKS[ type ][ action ].runs ?
     244                                HOOKS[ type ][ action ].runs :
     245                                0;
     246                };
     247        }
     248
     249        /**
     250         * Check to see if an action is registered for a hook.
     251         *
     252         * @param  {string} type   Type for which hooks to check, one of 'action' or 'filter'.
     253         * @param {string}  action  The action to check.
     254         *
     255         * @return {bool}      Whether an action has been registered for a hook.
     256         */
     257        function createHasHookByType( type ) {
     258                return function( action ) {
     259                        return HOOKS[ type ] && HOOKS[ type ][ action ] ?
     260                                !! HOOKS[ type ][ action ] :
     261                                false;
     262                };
     263        }
     264
     265        wp.hooks = {
     266                removeFilter: createRemoveHookByType( 'filters' ),
     267                applyFilters: createRunHookByType( 'filters', runApplyFilters ),
     268                addFilter: createAddHookByType( 'filters' ),
     269                removeAction: createRemoveHookByType( 'actions' ),
     270                doAction: createRunHookByType( 'actions', runDoAction ),
     271                addAction: createAddHookByType( 'actions' ),
     272                doingAction: createDoingHookByType( 'actions' ),
     273                didAction: createDidHookByType( 'actions' ),
     274                hasAction: createHasHookByType( 'actions' )
     275        };
     276} )( window.wp = window.wp || {} );
  • src/wp-includes/js/wp-util.js

    diff --git src/wp-includes/js/wp-util.js src/wp-includes/js/wp-util.js
    index 527441d..bdc7531 100644
    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. Optional, default is 55.
     138                 * @param  {string} more     What to append if text needs to be trimmed. Optional, default is '…'.
     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/plugin.php

    diff --git src/wp-includes/plugin.php src/wp-includes/plugin.php
    index 86f1c3b..86f9db8 100644
    function doing_filter( $filter = null ) { 
    363363}
    364364
    365365/**
    366  * Retrieve the name of an action currently being processed.
     366 * Retrieve whether action currently being processed.
    367367 *
    368368 * @since 3.9.0
    369369 *
  • src/wp-includes/script-loader.php

    diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
    index def438c..1067e4b 100644
    function wp_default_scripts( &$scripts ) { 
    8585
    8686        $scripts->add( 'wp-a11y', "/wp-includes/js/wp-a11y$suffix.js", array( 'jquery' ), false, 1 );
    8787
     88        $scripts->add( 'wp-hooks', "/wp-includes/js/wp-hooks$suffix.js", array(), false, 1 );
     89
    8890        $scripts->add( 'sack', "/wp-includes/js/tw-sack$suffix.js", array(), '1.6.1', 1 );
    8991
    9092        $scripts->add( 'quicktags', "/wp-includes/js/quicktags$suffix.js", array(), false, 1 );
    function wp_default_scripts( &$scripts ) { 
    336338                'ajax' => array(
    337339                        'url' => admin_url( 'admin-ajax.php', 'relative' ),
    338340                ),
     341                'formatting' => array(
     342                        'trimWordsMore'  => __( '&hellip;' ),
     343                        /*
     344                         * translators: If your word count is based on single characters (e.g. East Asian characters),
     345                         * enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
     346                         * Do not translate into your own language.
     347                         */
     348                        'trimWordsByCharacter' => strpos( _x( 'words', 'Word count type. Do not translate!' ), 'characters' ) === 0 && preg_match( '/^utf\-?8$/i', get_option( 'blog_charset' ) ),
     349                ),
    339350        ) );
    340351
    341352        $scripts->add( 'wp-backbone', "/wp-includes/js/wp-backbone$suffix.js", array('backbone', 'wp-util'), false, 1 );
    function wp_default_scripts( &$scripts ) { 
    724735                        'current' => __( 'Current Color' ),
    725736                ) );
    726737
    727                 $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox' ), false, 1 );
     738                $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox', 'wp-api', 'wp-backbone', 'wp-a11y', 'wp-util', 'wp-hooks' ), false, 1 );
     739                did_action( 'init' ) && $scripts->localize( 'dashboard', 'quickDraft', array(
     740                        'currentUserId'  => get_current_user_id(),
     741                        'l10n' => array(
     742                                'error'            => __( 'An error has occurred. Please reload the page and try again.' ),
     743                                'newDraftCreated'  => __( 'Success. A new draft was created.' ),
     744                                'errorEmptyFields' => __( 'Error. All fields were empty.' ),
     745                                'noTitle'          => __( '(no title)' ),
     746                        ),
     747                        'timezoneOffset' => ( get_option( 'gmt_offset' ) >= 0 ? '+' : '-' ) . date( 'H:i', abs( get_option( 'gmt_offset' ) * 3600 ) ),
     748                ) );
    728749
    729750                $scripts->add( 'list-revisions', "/wp-includes/js/wp-list-revisions$suffix.js" );
    730751
  • tests/qunit/index.html

    diff --git tests/qunit/index.html tests/qunit/index.html
    index 0c9d820..a707e13 100644
     
    5050                <script src="../../src/wp-includes/js/customize-base.js"></script>
    5151                <script src="../../src/wp-includes/js/customize-models.js"></script>
    5252                <script src="../../src/wp-includes/js/shortcode.js"></script>
     53                <script src="../../src/wp-includes/js/wp-hooks.js"></script>
    5354                <script src="../../src/wp-admin/js/customize-controls.js"></script>
    5455                <script src="../../src/wp-includes/js/wp-api.js"></script>
    5556
     
    7071                <script src="wp-admin/js/customize-base.js"></script>
    7172                <script src="wp-admin/js/customize-header.js"></script>
    7273                <script src="wp-includes/js/shortcode.js"></script>
     74                <script src="wp-includes/js/wp-hooks.js"></script>
    7375                <script src="wp-includes/js/wp-api.js"></script>
    7476                <script src="wp-admin/js/customize-controls.js"></script>
    7577                <script src="wp-admin/js/customize-controls-utils.js"></script>
     
    7779                <script src="wp-admin/js/customize-widgets.js"></script>
    7880                <script src="wp-admin/js/word-count.js"></script>
    7981                <script src="wp-admin/js/nav-menu.js"></script>
     82                <script src="wp-includes/js/wp-util.js"></script>
    8083
    8184                <!-- Customizer templates for sections -->
    8285                <script type="text/html" id="tmpl-customize-section-default">
  • new file tests/qunit/wp-includes/js/wp-hooks.js

    diff --git tests/qunit/wp-includes/js/wp-hooks.js tests/qunit/wp-includes/js/wp-hooks.js
    new file mode 100644
    index 0000000..8f894b2
    - +  
     1/* global wp */
     2( function( QUnit ) {
     3        QUnit.module( 'wp-hooks' );
     4
     5        function filter_a( str ) {
     6                return str + 'a';
     7        }
     8        function filter_b( str ) {
     9                return str + 'b';
     10        }
     11        function filter_c( str ) {
     12                return str + 'c';
     13        }
     14        function action_a() {
     15                window.actionValue += 'a';
     16        }
     17        function action_b() {
     18                window.actionValue += 'b';
     19        }
     20        function action_c() {
     21                window.actionValue += 'c';
     22        }
     23        window.actionValue = '';
     24
     25        QUnit.test( 'add and remove a filter', function() {
     26                expect( 1 );
     27                wp.hooks.addFilter( 'test.filter', filter_a );
     28                wp.hooks.removeFilter( 'test.filter' );
     29                equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'test' );
     30        } );
     31
     32        QUnit.test( 'add a filter and run it', function() {
     33                expect( 1 );
     34                wp.hooks.addFilter( 'test.filter', filter_a );
     35                equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testa' );
     36                wp.hooks.removeFilter( 'test.filter' );
     37        } );
     38
     39        QUnit.test( 'add 2 filters in a row and run them', function() {
     40                expect( 1 );
     41                wp.hooks.addFilter( 'test.filter', filter_a );
     42                wp.hooks.addFilter( 'test.filter', filter_b );
     43                equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testab' );
     44                wp.hooks.removeFilter( 'test.filter' );
     45        } );
     46
     47        QUnit.test( 'add 3 filters with different priorities and run them', function() {
     48                expect( 1 );
     49                wp.hooks.addFilter( 'test.filter', filter_a );
     50                wp.hooks.addFilter( 'test.filter', filter_b, 2 );
     51                wp.hooks.addFilter( 'test.filter', filter_c, 8 );
     52                equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testbca' );
     53                wp.hooks.removeFilter( 'test.filter' );
     54        } );
     55
     56        QUnit.test( 'add and remove an action', function() {
     57                expect( 1 );
     58                window.actionValue = '';
     59                wp.hooks.addAction( 'test.action', action_a );
     60                wp.hooks.removeAction( 'test.action' );
     61                wp.hooks.doAction( 'test.action' );
     62                equal( window.actionValue, '' );
     63        } );
     64
     65        QUnit.test( 'add an action and run it', function() {
     66                expect( 1 );
     67                window.actionValue = '';
     68                wp.hooks.addAction( 'test.action', action_a );
     69                wp.hooks.doAction( 'test.action' );
     70                equal( window.actionValue, 'a' );
     71                wp.hooks.removeAction( 'test.action' );
     72        } );
     73
     74        QUnit.test( 'add 2 actions in a row and then run them', function() {
     75                expect( 1 );
     76                window.actionValue = '';
     77                wp.hooks.addAction( 'test.action', action_a );
     78                wp.hooks.addAction( 'test.action', action_b );
     79                wp.hooks.doAction( 'test.action' );
     80                equal( window.actionValue, 'ab' );
     81                wp.hooks.removeAction( 'test.action' );
     82        } );
     83
     84        QUnit.test( 'add 3 actions with different priorities and run them', function() {
     85                expect( 1 );
     86                window.actionValue = '';
     87                wp.hooks.addAction( 'test.action', action_a );
     88                wp.hooks.addAction( 'test.action', action_b, 2 );
     89                wp.hooks.addAction( 'test.action', action_c, 8 );
     90                wp.hooks.doAction( 'test.action' );
     91                equal( window.actionValue, 'bca' );
     92                wp.hooks.removeAction( 'test.action' );
     93        } );
     94
     95        QUnit.test( 'pass in two arguments to an action', function() {
     96                var arg1 = 10,
     97                        arg2 = 20;
     98
     99                expect( 4 );
     100
     101                wp.hooks.addAction( 'test.action', function( a, b ) {
     102                        equal( arg1, a );
     103                        equal( arg2, b );
     104                } );
     105                wp.hooks.doAction( 'test.action', arg1, arg2 );
     106                wp.hooks.removeAction( 'test.action' );
     107
     108                equal( arg1, 10 );
     109                equal( arg2, 20 );
     110        } );
     111
     112        QUnit.test( 'fire action multiple times', function() {
     113                var func;
     114                expect( 2 );
     115
     116                func = function() {
     117                        ok( true );
     118                };
     119
     120                wp.hooks.addAction( 'test.action', func );
     121                wp.hooks.doAction( 'test.action' );
     122                wp.hooks.doAction( 'test.action' );
     123                wp.hooks.removeAction( 'test.action' );
     124        } );
     125
     126        QUnit.test( 'remove specific action callback', function() {
     127                window.actionValue = '';
     128                wp.hooks.addAction( 'test.action', action_a );
     129                wp.hooks.addAction( 'test.action', action_b, 2 );
     130                wp.hooks.addAction( 'test.action', action_c, 8 );
     131
     132                wp.hooks.removeAction( 'test.action', action_b );
     133                wp.hooks.doAction( 'test.action' );
     134                equal( window.actionValue, 'ca' );
     135                wp.hooks.removeAction( 'test.action' );
     136        } );
     137
     138        QUnit.test( 'remove specific filter callback', function() {
     139                wp.hooks.addFilter( 'test.filter', filter_a );
     140                wp.hooks.addFilter( 'test.filter', filter_b, 2 );
     141                wp.hooks.addFilter( 'test.filter', filter_c, 8 );
     142
     143                wp.hooks.removeFilter( 'test.filter', filter_b );
     144                equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testca' );
     145                wp.hooks.removeFilter( 'test.filter' );
     146        } );
     147
     148        // Test doingAction, didAction, hasAction.
     149        QUnit.test( 'Test doingAction, didAction and hasAction.', function() {
     150
     151                // Reset state for testing.
     152                wp.hooks.removeAction( 'test.action' );
     153                wp.hooks.addAction( 'another.action', function(){} );
     154                wp.hooks.doAction( 'another.action' );
     155
     156                // Verify no action is running yet.
     157                ok( ! wp.hooks.doingAction( 'test.action' ), 'The test.action is not running.' );
     158                equal( wp.hooks.didAction( 'test.action' ), 0, 'The test.action has not run.' );
     159                ok( ! wp.hooks.hasAction( 'test.action' ), 'The test.action is not registered.' );
     160
     161                wp.hooks.addAction( 'test.action', action_a );
     162
     163                // Verify action added, not running yet.
     164                ok( ! wp.hooks.doingAction( 'test.action' ), 'The test.action is not running.' );
     165                equal( wp.hooks.didAction( 'test.action' ), 0, 'The test.action has not run.' );
     166                ok( wp.hooks.hasAction( 'test.action' ), 'The test.action is registered.' );
     167
     168                wp.hooks.doAction( 'test.action' );
     169
     170                // Verify action added and running.
     171                ok( wp.hooks.doingAction( 'test.action' ), 'The test.action is running.' );
     172                equal( wp.hooks.didAction( 'test.action' ), 1, 'The test.action has run once.' );
     173                ok( wp.hooks.hasAction( 'test.action' ), 'The test.action is registered.' );
     174
     175                wp.hooks.doAction( 'test.action' );
     176                equal( wp.hooks.didAction( 'test.action' ), 2, 'The test.action has run twice.' );
     177
     178                wp.hooks.removeAction( 'test.action' );
     179
     180                // Verify state is reset appropriately.
     181                ok( wp.hooks.doingAction( 'test.action' ), 'The test.action is running.' );
     182                equal( wp.hooks.didAction( 'test.action' ), 0, 'The test.action has not run.' );
     183                ok( ! wp.hooks.hasAction( 'test.action' ), 'The test.action is not registered.' );
     184
     185                wp.hooks.doAction( 'another.action' );
     186                ok( ! wp.hooks.doingAction( 'test.action' ), 'The test.action is running.' );
     187
     188
     189        } );
     190
     191} )( window.QUnit );
  • new file tests/qunit/wp-includes/js/wp-util.js

    diff --git tests/qunit/wp-includes/js/wp-util.js tests/qunit/wp-includes/js/wp-util.js
    new file mode 100644
    index 0000000..6d42228
    - +  
     1/* global wp */
     2( function( QUnit ) {
     3        wp.formatting.settings.trimWordsMore = '&hellip;';
     4        QUnit.module( 'wp-util' );
     5        var longText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce varius lacinia vehicula. Etiam sapien risus, ultricies ac posuere eu, convallis sit amet augue. Pellentesque urna massa, lacinia vel iaculis eget, bibendum in mauris. Aenean eleifend pulvinar ligula, a convallis eros gravida non. Suspendisse potenti. Pellentesque et odio tortor. In vulputate pellentesque libero, sed dapibus velit mollis viverra. Pellentesque id urna euismod dolor cursus sagittis.';
     6
     7        QUnit.test( 'wp.formatting.trimWords', function( assert ) {
     8                _.each( [
     9                        {
     10                                'trimmed': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce varius lacinia vehicula. Etiam sapien risus, ultricies ac posuere eu, convallis sit amet augue. Pellentesque urna massa, lacinia vel iaculis eget, bibendum in mauris. Aenean eleifend pulvinar ligula, a convallis eros gravida non. Suspendisse potenti. Pellentesque et odio tortor. In vulputate pellentesque libero, sed dapibus velit&hellip;',
     11                                'text': longText,
     12                                'description': 'Trims to 55 by default.'
     13                        },
     14                        {
     15                                'trimmed': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce varius&hellip;',
     16                                'text': longText,
     17                                'length': 10,
     18                                'description': 'Trims to 10.'
     19                        },
     20                        {
     21                                'trimmed': 'Lorem ipsum dolor sit amet,[...] Read on!',
     22                                'text': longText,
     23                                'description': 'Trims to 5 and uses custom more.',
     24                                'length': 5,
     25                                'more': '[...] Read on!'
     26                        },
     27                        {
     28                                'trimmed': 'This is some short text.',
     29                                'text': 'This is some short text.',
     30                                'description': 'Doesn\'t strip short text.'
     31                        }
     32
     33                ], function( test ) {
     34                        assert.equal(
     35                                wp.formatting.trimWords( test.text, test.length, test.more ),
     36                                test.trimmed,
     37                                test.description
     38                        );
     39                } );
     40        } );
     41} )( window.QUnit );