WordPress.org

Make WordPress Core

Ticket #21170: 21170.11.diff

File 21170.11.diff, 19.2 KB (added by adamsilverstein, 2 years ago)
  • 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 0000000000..b11a31396d
    - +  
     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                actions: [],
     10                filters: []
     11        };
     12
     13        /**
     14         * Returns a function which, when invoked, will add a hook.
     15         *
     16         * @param  {string}   hooksArray Hooks array to which hooks are to be added
     17         * @return {Function}            Hook added.
     18         */
     19        function createAddHook( hooksArray ) {
     20                /**
     21                 * Adds the hook to the appropriate hooks container
     22                 *
     23                 * @param {string}   hook     Name of hook to add
     24                 * @param {Function} callback Function to call when the hook is run
     25                 * @param {?number}  priority Priority of this hook (default=10)
     26                 */
     27                return function( hook, callback, priority ) {
     28                        var hookObject, hooks;
     29                        if ( typeof hook !== 'string' || typeof callback !== 'function' ) {
     30                                return;
     31                        }
     32
     33                        // Assign default priority
     34                        if ( 'undefined' === typeof priority ) {
     35                                priority = 10;
     36                        } else {
     37                                priority = parseInt( priority, 10 );
     38                        }
     39
     40                        // Validate numeric priority
     41                        if ( isNaN( priority ) ) {
     42                                return;
     43                        }
     44
     45                        hookObject = {
     46                                callback: callback,
     47                                priority: priority
     48                        };
     49
     50                        if ( hooksArray.hasOwnProperty( hook ) ) {
     51                                // Append and re-sort amongst existing
     52                                hooks = hooksArray[ hook ];
     53                                hooks.push( hookObject );
     54                                hooks = sortHooks( hooks );
     55                        } else {
     56                                // First of its type needs no sort
     57                                hooks = [ hookObject ];
     58                        }
     59
     60                        hooksArray[ hook ] = hooks;
     61                };
     62        }
     63
     64        /**
     65         * Returns a function which, when invoked, will remove a specified hook.
     66         *
     67         * @param  {string}   hooksArray Hooks array from which hooks are to be removed.
     68         * @param  {bool}     removeAll  Whether to always remove all hooked callbacks.
     69         *
     70         * @return {Function}           Hook remover.
     71         */
     72        function createRemoveHook( hooksArray, removeAll ) {
     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. Optional, 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 ( ! hooksArray || ! hooksArray.hasOwnProperty( hook ) ) {
     85                                return;
     86                        }
     87
     88                        if ( callback && ! removeAll ) {
     89                                // Try to find specified callback to remove
     90                                handlers = hooksArray[ 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 hooksArray[ 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  {Function} runner Function to invoke for each hook callback
     109         * @return {Function}        Hook runner
     110         */
     111        function createRunHook( runner ) {
     112                /**
     113                 * Runs the specified hook.
     114                 *
     115                 * @param  {string} hook The hook to run
     116                 * @param  {...*}   args Arguments to pass to the action/filter
     117                 * @return {*}           Return value of runner, if applicable
     118                 * @private
     119                 */
     120                return function( /* hook, ...args */ ) {
     121                        var args, hook;
     122
     123                        args = Array.prototype.slice.call( arguments );
     124                        hook = args.shift();
     125
     126                        if ( typeof hook === 'string' ) {
     127                                return runner( hook, args );
     128                        }
     129                };
     130        }
     131
     132        /**
     133         * Performs an action if it exists.
     134         *
     135         * @param {string} action The action to perform.
     136         * @param {...*}   args   Optional args to pass to the action.
     137         * @private
     138         */
     139        function runDoAction( action, args ) {
     140                var handlers, i;
     141                if ( HOOKS.actions ) {
     142                        handlers = HOOKS.actions[ action ];
     143                }
     144
     145                if ( ! handlers ) {
     146                        return;
     147                }
     148
     149                HOOKS.actions.current = action;
     150
     151                for ( i = 0; i < handlers.length; i++ ) {
     152                        handlers[ i ].callback.apply( null, args );
     153                        HOOKS.actions[ action ].runs = HOOKS.actions[ action ].runs ? HOOKS.actions[ action ].runs + 1 : 1;
     154                }
     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                delete( HOOKS.filters.current );
     183
     184                return args[ 0 ];
     185        }
     186
     187        /**
     188         * Use an insert sort for keeping our hooks organized based on priority.
     189         *
     190         * @see http://jsperf.com/javascript-sort
     191         *
     192         * @param  {Array} hooks Array of the hooks to sort
     193         * @return {Array}       The sorted array
     194         * @private
     195         */
     196        function sortHooks( hooks ) {
     197                var i, tmpHook, j, prevHook;
     198                for ( i = 1; i < hooks.length; i++ ) {
     199                        tmpHook = hooks[ i ];
     200                        j = i;
     201                        while ( ( prevHook = hooks[ j - 1 ] ) && prevHook.priority > tmpHook.priority ) {
     202                                hooks[ j ] = hooks[ j - 1 ];
     203                                --j;
     204                        }
     205                        hooks[ j ] = tmpHook;
     206                }
     207
     208                return hooks;
     209        }
     210
     211
     212        /**
     213         * See what action is currently being executed.
     214         *
     215         * @param  {string} hooksArray Hooks array of hooks to check.
     216         * @param {string}  action     The name of the action to check for.
     217         *
     218         * @return {Function}          A function that gets the currently executing filter,
     219         */
     220        function createCurrentHook( hooksArray ) {
     221
     222                /**
     223                 * Get the current active hook.
     224                 *
     225                 * @param {string}  action The name of the action to check for, if omitted will check for any action being performed.
     226                 *
     227                 * @return {string}          Returns the currently executing action, or false if none.
     228                 */
     229                return function() {
     230
     231                        // If the action was not passed, check for any current hook.
     232                        if ( 'undefined' === typeof action ) {
     233                                return false;
     234                        }
     235
     236                        // Return the current hook.
     237                        return hooksArray && hooksArray.current ?
     238                                hooksArray.current :
     239                                false;
     240                };
     241        }
     242
     243
     244
     245        /**
     246         * Checks to see if an action is currently being executed.
     247         *
     248         * @param  {string} type   Type of hooks to check.
     249         * @param {string}  action The name of the action to check for, if omitted will check for any action being performed.
     250         *
     251         * @return {[type]}      [description]
     252         */
     253        function createDoingHook( hooksArray ) {
     254                return function( action ) {
     255
     256                        // If the action was not passed, check for any current hook.
     257                        if ( 'undefined' === typeof action ) {
     258                                return 'undefined' !== typeof hooksArray.current;
     259                        }
     260
     261                        // Return the current hook.
     262                        return hooksArray && hooksArray.current ?
     263                                action === hooksArray.current :
     264                                false;
     265                };
     266        }
     267
     268        /**
     269         * Retrieve the number of times an action is fired.
     270         *
     271         * @param {string} hooksArray Hooks array of hooks to check.
     272         * @param {string} action     The action to check.
     273         *
     274         * @return {[type]}      [description]
     275         */
     276        function createDidHook( hooksArray ) {
     277                return function( action ) {
     278                        return hooksArray && hooksArray[ action ] && hooksArray[ action ].runs ?
     279                                hooksArray[ action ].runs :
     280                                0;
     281                };
     282        }
     283
     284        /**
     285         * Check to see if an action is registered for a hook.
     286         *
     287         * @param {string} hooksArray Hooks array of hooks to check.
     288         * @param {string} action     The action to check.
     289         *
     290         * @return {bool}      Whether an action has been registered for a hook.
     291         */
     292        function createHasHook( hooksArray ) {
     293                return function( action ) {
     294                        return hooksArray && hooksArray[ action ] ?
     295                                !! hooksArray[ action ] :
     296                                false;
     297                };
     298        }
     299
     300        /**
     301         * Remove all the actions registered to a hook.
     302         *
     303         * @param {string} hooksArray Hooks array of hooks to check.
     304         *
     305         * @return {Function}         All hook remover.
     306         */
     307        function createRemoveAllHook( hooksArray ) {
     308                return createRemoveHook( hooksArray, true );
     309        }
     310
     311        wp.hooks = {
     312
     313                // Remove functions.
     314                removeFilter: createRemoveHook( HOOKS.filters ),
     315                removeAction: createRemoveHook( HOOKS.actions ),
     316
     317                // Do action/apply filter functions.
     318                doAction:     createRunHook( runDoAction ),
     319                applyFilters: createRunHook( runApplyFilters ),
     320
     321                // Add functions.
     322                addAction: createAddHook( HOOKS.actions ),
     323                addFilter: createAddHook( HOOKS.filters ),
     324
     325                // Doing action: true until next action fired.
     326                doingAction: createDoingHook( HOOKS.actions ),
     327
     328                // Doing filter: true while filter is being applied.
     329                doingFilter: createDoingHook( HOOKS.filters ),
     330
     331                // Did functions.
     332                didAction: createDidHook( HOOKS.actions ),
     333                didFilter: createDidHook( HOOKS.filters ),
     334
     335                // Has functions.
     336                hasAction: createHasHook( HOOKS.actions ),
     337                hasFilter: createHasHook( HOOKS.filters ),
     338
     339                // Remove all functions.
     340                removeAllActions: createRemoveAllHook( HOOKS.actions ),
     341                removeAllFilters: createRemoveAllHook( HOOKS.filters ),
     342
     343                // Current filter.
     344                currentFilter: createCurrentHook( HOOKS.filters )
     345        };
     346} )( window.wp = window.wp || {} );
  • src/wp-includes/plugin.php

    diff --git src/wp-includes/plugin.php src/wp-includes/plugin.php
    index 86f1c3b319..86f9db8964 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 0058c5e956..790272188e 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 );
  • tests/qunit/index.html

    diff --git tests/qunit/index.html tests/qunit/index.html
    index c41fffe63a..183c492d85 100644
     
    7676                <script src="../../src/wp-includes/js/customize-base.js"></script>
    7777                <script src="../../src/wp-includes/js/customize-models.js"></script>
    7878                <script src="../../src/wp-includes/js/shortcode.js"></script>
     79                <script src="../../src/wp-includes/js/wp-hooks.js"></script>
    7980                <script src="../../src/wp-admin/js/customize-controls.js"></script>
    8081                <script src="../../src/wp-includes/js/wp-api.js"></script>
    8182
     
    122123                <script src="wp-admin/js/customize-base.js"></script>
    123124                <script src="wp-admin/js/customize-header.js"></script>
    124125                <script src="wp-includes/js/shortcode.js"></script>
     126                <script src="wp-includes/js/wp-hooks.js"></script>
    125127                <script src="wp-includes/js/wp-api.js"></script>
    126128                <script src="wp-admin/js/customize-controls.js"></script>
    127129                <script src="wp-admin/js/customize-controls-utils.js"></script>
  • 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 0000000000..57b0d09d52
    - +  
     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        function filter_check() {
     24                ok( wp.hooks.doingFilter( 'runtest.filter' ), 'The runtest.filter is running.' );
     25        }
     26        window.actionValue = '';
     27
     28        QUnit.test( 'add and remove a filter', function() {
     29                expect( 1 );
     30                wp.hooks.addFilter( 'test.filter', filter_a );
     31                wp.hooks.removeFilter( 'test.filter' );
     32                equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'test' );
     33        } );
     34
     35        QUnit.test( 'add a filter and run it', function() {
     36                expect( 1 );
     37                wp.hooks.addFilter( 'test.filter', filter_a );
     38                equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testa' );
     39                wp.hooks.removeFilter( 'test.filter' );
     40        } );
     41
     42        QUnit.test( 'add 2 filters in a row and run them', function() {
     43                expect( 1 );
     44                wp.hooks.addFilter( 'test.filter', filter_a );
     45                wp.hooks.addFilter( 'test.filter', filter_b );
     46                equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testab' );
     47                wp.hooks.removeFilter( 'test.filter' );
     48        } );
     49
     50        QUnit.test( 'add 3 filters with different priorities and run them', function() {
     51                expect( 1 );
     52                wp.hooks.addFilter( 'test.filter', filter_a );
     53                wp.hooks.addFilter( 'test.filter', filter_b, 2 );
     54                wp.hooks.addFilter( 'test.filter', filter_c, 8 );
     55                equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testbca' );
     56                wp.hooks.removeFilter( 'test.filter' );
     57        } );
     58
     59        QUnit.test( 'add and remove an action', function() {
     60                expect( 1 );
     61                window.actionValue = '';
     62                wp.hooks.addAction( 'test.action', action_a );
     63                wp.hooks.removeAction( 'test.action' );
     64                wp.hooks.doAction( 'test.action' );
     65                equal( window.actionValue, '' );
     66        } );
     67
     68        QUnit.test( 'add an action and run it', function() {
     69                expect( 1 );
     70                window.actionValue = '';
     71                wp.hooks.addAction( 'test.action', action_a );
     72                wp.hooks.doAction( 'test.action' );
     73                equal( window.actionValue, 'a' );
     74                wp.hooks.removeAction( 'test.action' );
     75        } );
     76
     77        QUnit.test( 'add 2 actions in a row and then run them', function() {
     78                expect( 1 );
     79                window.actionValue = '';
     80                wp.hooks.addAction( 'test.action', action_a );
     81                wp.hooks.addAction( 'test.action', action_b );
     82                wp.hooks.doAction( 'test.action' );
     83                equal( window.actionValue, 'ab' );
     84                wp.hooks.removeAction( 'test.action' );
     85        } );
     86
     87        QUnit.test( 'add 3 actions with different priorities and run them', function() {
     88                expect( 1 );
     89                window.actionValue = '';
     90                wp.hooks.addAction( 'test.action', action_a );
     91                wp.hooks.addAction( 'test.action', action_b, 2 );
     92                wp.hooks.addAction( 'test.action', action_c, 8 );
     93                wp.hooks.doAction( 'test.action' );
     94                equal( window.actionValue, 'bca' );
     95                wp.hooks.removeAction( 'test.action' );
     96        } );
     97
     98        QUnit.test( 'pass in two arguments to an action', function() {
     99                var arg1 = 10,
     100                        arg2 = 20;
     101
     102                expect( 4 );
     103
     104                wp.hooks.addAction( 'test.action', function( a, b ) {
     105                        equal( arg1, a );
     106                        equal( arg2, b );
     107                } );
     108                wp.hooks.doAction( 'test.action', arg1, arg2 );
     109                wp.hooks.removeAction( 'test.action' );
     110
     111                equal( arg1, 10 );
     112                equal( arg2, 20 );
     113        } );
     114
     115        QUnit.test( 'fire action multiple times', function() {
     116                var func;
     117                expect( 2 );
     118
     119                func = function() {
     120                        ok( true );
     121                };
     122
     123                wp.hooks.addAction( 'test.action', func );
     124                wp.hooks.doAction( 'test.action' );
     125                wp.hooks.doAction( 'test.action' );
     126                wp.hooks.removeAction( 'test.action' );
     127        } );
     128
     129        QUnit.test( 'remove specific action callback', function() {
     130                window.actionValue = '';
     131                wp.hooks.addAction( 'test.action', action_a );
     132                wp.hooks.addAction( 'test.action', action_b, 2 );
     133                wp.hooks.addAction( 'test.action', action_c, 8 );
     134
     135                wp.hooks.removeAction( 'test.action', action_b );
     136                wp.hooks.doAction( 'test.action' );
     137                equal( window.actionValue, 'ca' );
     138                wp.hooks.removeAction( 'test.action' );
     139        } );
     140
     141        QUnit.test( 'remove all action callbacks', function() {
     142                window.actionValue = '';
     143                wp.hooks.addAction( 'test.action', action_a );
     144                wp.hooks.addAction( 'test.action', action_b, 2 );
     145                wp.hooks.addAction( 'test.action', action_c, 8 );
     146
     147                wp.hooks.removeAllActions( 'test.action' );
     148                wp.hooks.doAction( 'test.action' );
     149                equal( window.actionValue, '' );
     150        } );
     151
     152        QUnit.test( 'remove specific filter callback', function() {
     153                wp.hooks.addFilter( 'test.filter', filter_a );
     154                wp.hooks.addFilter( 'test.filter', filter_b, 2 );
     155                wp.hooks.addFilter( 'test.filter', filter_c, 8 );
     156
     157                wp.hooks.removeFilter( 'test.filter', filter_b );
     158                equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testca' );
     159                wp.hooks.removeFilter( 'test.filter' );
     160        } );
     161
     162        QUnit.test( 'remove all filter callbacks', function() {
     163                wp.hooks.addFilter( 'test.filter', filter_a );
     164                wp.hooks.addFilter( 'test.filter', filter_b, 2 );
     165                wp.hooks.addFilter( 'test.filter', filter_c, 8 );
     166
     167                wp.hooks.removeAllFilters( 'test.filter' );
     168                equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'test' );
     169        } );
     170
     171        // Test doingAction, didAction, hasAction.
     172        QUnit.test( 'Test doingAction, didAction and hasAction.', function() {
     173
     174                // Reset state for testing.
     175                wp.hooks.removeAction( 'test.action' );
     176                wp.hooks.addAction( 'another.action', function(){} );
     177                wp.hooks.doAction( 'another.action' );
     178
     179                // Verify no action is running yet.
     180                ok( ! wp.hooks.doingAction( 'test.action' ), 'The test.action is not running.' );
     181                equal( wp.hooks.didAction( 'test.action' ), 0, 'The test.action has not run.' );
     182                ok( ! wp.hooks.hasAction( 'test.action' ), 'The test.action is not registered.' );
     183
     184                wp.hooks.addAction( 'test.action', action_a );
     185
     186                // Verify action added, not running yet.
     187                ok( ! wp.hooks.doingAction( 'test.action' ), 'The test.action is not running.' );
     188                equal( wp.hooks.didAction( 'test.action' ), 0, 'The test.action has not run.' );
     189                ok( wp.hooks.hasAction( 'test.action' ), 'The test.action is registered.' );
     190
     191                wp.hooks.doAction( 'test.action' );
     192
     193                // Verify action added and running.
     194                ok( wp.hooks.doingAction( 'test.action' ), 'The test.action is running.' );
     195                equal( wp.hooks.didAction( 'test.action' ), 1, 'The test.action has run once.' );
     196                ok( wp.hooks.hasAction( 'test.action' ), 'The test.action is registered.' );
     197
     198                wp.hooks.doAction( 'test.action' );
     199                equal( wp.hooks.didAction( 'test.action' ), 2, 'The test.action has run twice.' );
     200
     201                wp.hooks.removeAction( 'test.action' );
     202
     203                // Verify state is reset appropriately.
     204                ok( wp.hooks.doingAction( 'test.action' ), 'The test.action is running.' );
     205                equal( wp.hooks.didAction( 'test.action' ), 0, 'The test.action has not run.' );
     206                ok( ! wp.hooks.hasAction( 'test.action' ), 'The test.action is not registered.' );
     207
     208                wp.hooks.doAction( 'another.action' );
     209                ok( ! wp.hooks.doingAction( 'test.action' ), 'The test.action is running.' );
     210
     211                // Verify hasAction returns false when no matching action.
     212                ok( ! wp.hooks.hasAction( 'notatest.action' ), 'The notatest.action is registered.' );
     213
     214        } );
     215
     216        QUnit.test( 'Verify doingFilter, didFilter and hasFilter.', function() {
     217                expect( 4 );
     218                wp.hooks.addFilter( 'runtest.filter', filter_check );
     219
     220                // Verify filter added and running.
     221                var test = wp.hooks.applyFilters( 'runtest.filter', true );
     222                equal( wp.hooks.didFilter( 'runtest.filter' ), 1, 'The runtest.filter has run once.' );
     223                ok( wp.hooks.hasFilter( 'runtest.filter' ), 'The runtest.filter is registered.' );
     224                ok( ! wp.hooks.hasFilter( 'notatest.filter' ), 'The notatest.filter is not registered.' );
     225
     226                wp.hooks.removeFilter( 'runtest.filter' );
     227        } );
     228
     229} )( window.QUnit );