diff --git src/wp-includes/js/wp-hooks.js src/wp-includes/js/wp-hooks.js
new file mode 100644
index 0000000000..78e4348255
--- /dev/null
+++ src/wp-includes/js/wp-hooks.js
@@ -0,0 +1,336 @@
+( function( wp ) {
+ 'use strict';
+
+ /**
+ * Contains the registered hooks, keyed by hook type. Each hook type is an
+ * array of objects with priority and callback of each registered hook.
+ */
+ var HOOKS = {};
+
+ /**
+ * Returns a function which, when invoked, will add a hook.
+ *
+ * @param {string} type Type for which hooks are to be added
+ * @return {Function} Hook added
+ */
+ function createAddHookByType( type ) {
+ /**
+ * Adds the hook to the appropriate hooks container
+ *
+ * @param {string} hook Name of hook to add
+ * @param {Function} callback Function to call when the hook is run
+ * @param {?number} priority Priority of this hook (default=10)
+ */
+ return function( hook, callback, priority ) {
+ var hookObject, hooks;
+ if ( typeof hook !== 'string' || typeof callback !== 'function' ) {
+ return;
+ }
+
+ // Assign default priority
+ if ( 'undefined' === typeof priority ) {
+ priority = 10;
+ } else {
+ priority = parseInt( priority, 10 );
+ }
+
+ // Validate numeric priority
+ if ( isNaN( priority ) ) {
+ return;
+ }
+
+ // Check if adding first of type
+ if ( ! HOOKS[ type ] ) {
+ HOOKS[ type ] = {};
+ }
+
+ hookObject = {
+ callback: callback,
+ priority: priority
+ };
+
+ if ( HOOKS[ type ].hasOwnProperty( hook ) ) {
+ // Append and re-sort amongst existing
+ hooks = HOOKS[ type ][ hook ];
+ hooks.push( hookObject );
+ hooks = sortHooks( hooks );
+ } else {
+ // First of its type needs no sort
+ hooks = [ hookObject ];
+ }
+
+ HOOKS[ type ][ hook ] = hooks;
+ };
+ }
+
+ /**
+ * Returns a function which, when invoked, will remove a specified hook.
+ *
+ * @param {string} type Type for which hooks are to be removed.
+ * @param {bool} removeAll Whether to always remove all hooked callbacks.
+ *
+ * @return {Function} Hook remover.
+ */
+ function createRemoveHookByType( type, removeAll ) {
+ /**
+ * Removes the specified hook by resetting its value.
+ *
+ * @param {string} hook Name of hook to remove
+ * @param {Function} callback The specific callback to be removed. If
+ * omitted, clears all callbacks.
+ */
+ return function( hook, callback ) {
+ var handlers, i;
+
+ // Baily early if no hooks exist by this name
+ if ( ! HOOKS[ type ] || ! HOOKS[ type ].hasOwnProperty( hook ) ) {
+ return;
+ }
+
+ if ( callback && ! removeAll ) {
+ // Try to find specified callback to remove
+ handlers = HOOKS[ type ][ hook ];
+ for ( i = handlers.length - 1; i >= 0; i-- ) {
+ if ( handlers[ i ].callback === callback ) {
+ handlers.splice( i, 1 );
+ }
+ }
+ } else {
+ // Reset hooks to empty
+ delete HOOKS[ type ][ hook ];
+ }
+ };
+ }
+
+ /**
+ * Returns a function which, when invoked, will execute all registered
+ * hooks of the specified type by calling upon runner with its hook name
+ * and arguments.
+ *
+ * @param {string} type Type for which hooks are to be run, one of 'action' or 'filter'.
+ * @param {Function} runner Function to invoke for each hook callback
+ * @return {Function} Hook runner
+ */
+ function createRunHookByType( type, runner ) {
+ /**
+ * Runs the specified hook.
+ *
+ * @param {string} hook The hook to run
+ * @param {...*} args Arguments to pass to the action/filter
+ * @return {*} Return value of runner, if applicable
+ * @private
+ */
+ return function( /* hook, ...args */ ) {
+ var args, hook;
+
+ args = Array.prototype.slice.call( arguments );
+ hook = args.shift();
+
+ if ( typeof hook === 'string' ) {
+ return runner( hook, args );
+ }
+ };
+ }
+
+ /**
+ * Performs an action if it exists.
+ *
+ * @param {string} action The action to perform.
+ * @param {...*} args Optional args to pass to the action.
+ * @private
+ */
+ function runDoAction( action, args ) {
+ var handlers, i;
+ if ( HOOKS.actions ) {
+ handlers = HOOKS.actions[ action ];
+ }
+
+ if ( ! handlers ) {
+ return;
+ }
+
+ HOOKS.actions.current = action;
+
+ for ( i = 0; i < handlers.length; i++ ) {
+ handlers[ i ].callback.apply( null, args );
+ HOOKS.actions[ action ].runs = HOOKS.actions[ action ].runs ? HOOKS.actions[ action ].runs + 1 : 1;
+ }
+
+ }
+
+ /**
+ * Performs a filter if it exists.
+ *
+ * @param {string} filter The filter to apply.
+ * @param {...*} args Optional args to pass to the filter.
+ * @return {*} The filtered value
+ * @private
+ */
+ function runApplyFilters( filter, args ) {
+ var handlers, i;
+ if ( HOOKS.filters ) {
+ handlers = HOOKS.filters[ filter ];
+ }
+
+ if ( ! handlers ) {
+ return args[ 0 ];
+ }
+
+ HOOKS.filters.current = filter;
+ HOOKS.filters[ filter ].runs = HOOKS.filters[ filter ].runs ? HOOKS.filters[ filter ].runs + 1 : 1;
+
+ for ( i = 0; i < handlers.length; i++ ) {
+ args[ 0 ] = handlers[ i ].callback.apply( null, args );
+ }
+ delete( HOOKS.filters.current );
+
+ return args[ 0 ];
+ }
+
+ /**
+ * Use an insert sort for keeping our hooks organized based on priority.
+ *
+ * @see http://jsperf.com/javascript-sort
+ *
+ * @param {Array} hooks Array of the hooks to sort
+ * @return {Array} The sorted array
+ * @private
+ */
+ function sortHooks( hooks ) {
+ var i, tmpHook, j, prevHook;
+ for ( i = 1; i < hooks.length; i++ ) {
+ tmpHook = hooks[ i ];
+ j = i;
+ while ( ( prevHook = hooks[ j - 1 ] ) && prevHook.priority > tmpHook.priority ) {
+ hooks[ j ] = hooks[ j - 1 ];
+ --j;
+ }
+ hooks[ j ] = tmpHook;
+ }
+
+ return hooks;
+ }
+
+
+ /**
+ * See what action is currently being executed.
+ *
+ * @param {string} type Type of hooks to check, one of 'action' or 'filter'.
+ * @param {string} action The name of the action to check for.
+ *
+ * @return {[type]} [description]
+ */
+ function createCurrentHookByType( type ) {
+ return function( action ) {
+
+ // If the action was not passed, check for any current hook.
+ if ( 'undefined' === typeof action ) {
+ return false;
+ }
+
+ // Return the current hook.
+ return HOOKS[ type ] && HOOKS[ type ].current ?
+ HOOKS[ type ].current :
+ false;
+ };
+ }
+
+
+
+ /**
+ * Checks to see if an action is currently being executed.
+ *
+ * @param {string} type Type of hooks to check, one of 'action' or 'filter'.
+ * @param {string} action The name of the action to check for, if omitted will check for any action being performed.
+ *
+ * @return {[type]} [description]
+ */
+ function createDoingHookByType( type ) {
+ return function( action ) {
+
+ // If the action was not passed, check for any current hook.
+ if ( 'undefined' === typeof action ) {
+ return 'undefined' !== typeof HOOKS[ type ].current;
+ }
+
+ // Return the current hook.
+ return HOOKS[ type ] && HOOKS[ type ].current ?
+ action === HOOKS[ type ].current :
+ false;
+ };
+ }
+
+ /**
+ * Retrieve the number of times an action is fired.
+ *
+ * @param {string} type Type for which hooks to check, one of 'action' or 'filter'.
+ * @param {string} action The action to check.
+ *
+ * @return {[type]} [description]
+ */
+ function createDidHookByType( type ) {
+ return function( action ) {
+ return HOOKS[ type ] && HOOKS[ type ][ action ] && HOOKS[ type ][ action ].runs ?
+ HOOKS[ type ][ action ].runs :
+ 0;
+ };
+ }
+
+ /**
+ * Check to see if an action is registered for a hook.
+ *
+ * @param {string} type Type for which hooks to check, one of 'action' or 'filter'.
+ * @param {string} action The action to check.
+ *
+ * @return {bool} Whether an action has been registered for a hook.
+ */
+ function createHasHookByType( type ) {
+ return function( action ) {
+ return HOOKS[ type ] && HOOKS[ type ][ action ] ?
+ !! HOOKS[ type ][ action ] :
+ false;
+ };
+ }
+
+ /**
+ * Remove all the actions registered to a hook.
+ */
+ function createRemoveAllByType( type ) {
+ return createRemoveHookByType( type, true );
+ }
+
+ wp.hooks = {
+
+ // Remove functions,
+ removeFilter: createRemoveHookByType( 'filters' ),
+ removeAction: createRemoveHookByType( 'actions' ),
+
+
+ // Do action/apply filter functions.
+ doAction: createRunHookByType( 'actions', runDoAction ),
+ applyFilters: createRunHookByType( 'filters', runApplyFilters ),
+
+ // Add functions.
+ addAction: createAddHookByType( 'actions' ),
+ addFilter: createAddHookByType( 'filters' ),
+
+ // Doing functions.
+ doingAction: createDoingHookByType( 'actions' ), /* True for actions until next action fired. */
+ doingFilter: createDoingHookByType( 'filters' ), /* True for filters while filter is being applied. */
+
+ // Did functions.
+ didAction: createDidHookByType( 'actions' ),
+ didFilter: createDidHookByType( 'filters' ),
+
+ // Has functions.
+ hasAction: createHasHookByType( 'actions' ),
+ hasFilter: createHasHookByType( 'filters' ),
+
+ // Remove all functions.
+ removeAllActions: createRemoveAllByType( 'actions' ),
+ removeAllFilters: createRemoveAllByType( 'filters' ),
+
+ // Current filter.
+ currentFilter: createCurrentHookByType( 'filters' )
+ };
+} )( window.wp = window.wp || {} );
diff --git src/wp-includes/plugin.php src/wp-includes/plugin.php
index 86f1c3b319..86f9db8964 100644
--- src/wp-includes/plugin.php
+++ src/wp-includes/plugin.php
@@ -363,7 +363,7 @@ function doing_filter( $filter = null ) {
}
/**
- * Retrieve the name of an action currently being processed.
+ * Retrieve whether action currently being processed.
*
* @since 3.9.0
*
diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
index 0058c5e956..790272188e 100644
--- src/wp-includes/script-loader.php
+++ src/wp-includes/script-loader.php
@@ -85,6 +85,8 @@ function wp_default_scripts( &$scripts ) {
$scripts->add( 'wp-a11y', "/wp-includes/js/wp-a11y$suffix.js", array( 'jquery' ), false, 1 );
+ $scripts->add( 'wp-hooks', "/wp-includes/js/wp-hooks$suffix.js", array(), false, 1 );
+
$scripts->add( 'sack', "/wp-includes/js/tw-sack$suffix.js", array(), '1.6.1', 1 );
$scripts->add( 'quicktags', "/wp-includes/js/quicktags$suffix.js", array(), false, 1 );
diff --git tests/qunit/index.html tests/qunit/index.html
index c41fffe63a..183c492d85 100644
--- tests/qunit/index.html
+++ tests/qunit/index.html
@@ -76,6 +76,7 @@
+
@@ -122,6 +123,7 @@
+
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
--- /dev/null
+++ tests/qunit/wp-includes/js/wp-hooks.js
@@ -0,0 +1,229 @@
+/* global wp */
+( function( QUnit ) {
+ QUnit.module( 'wp-hooks' );
+
+ function filter_a( str ) {
+ return str + 'a';
+ }
+ function filter_b( str ) {
+ return str + 'b';
+ }
+ function filter_c( str ) {
+ return str + 'c';
+ }
+ function action_a() {
+ window.actionValue += 'a';
+ }
+ function action_b() {
+ window.actionValue += 'b';
+ }
+ function action_c() {
+ window.actionValue += 'c';
+ }
+ function filter_check() {
+ ok( wp.hooks.doingFilter( 'runtest.filter' ), 'The runtest.filter is running.' );
+ }
+ window.actionValue = '';
+
+ QUnit.test( 'add and remove a filter', function() {
+ expect( 1 );
+ wp.hooks.addFilter( 'test.filter', filter_a );
+ wp.hooks.removeFilter( 'test.filter' );
+ equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'test' );
+ } );
+
+ QUnit.test( 'add a filter and run it', function() {
+ expect( 1 );
+ wp.hooks.addFilter( 'test.filter', filter_a );
+ equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testa' );
+ wp.hooks.removeFilter( 'test.filter' );
+ } );
+
+ QUnit.test( 'add 2 filters in a row and run them', function() {
+ expect( 1 );
+ wp.hooks.addFilter( 'test.filter', filter_a );
+ wp.hooks.addFilter( 'test.filter', filter_b );
+ equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testab' );
+ wp.hooks.removeFilter( 'test.filter' );
+ } );
+
+ QUnit.test( 'add 3 filters with different priorities and run them', function() {
+ expect( 1 );
+ wp.hooks.addFilter( 'test.filter', filter_a );
+ wp.hooks.addFilter( 'test.filter', filter_b, 2 );
+ wp.hooks.addFilter( 'test.filter', filter_c, 8 );
+ equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testbca' );
+ wp.hooks.removeFilter( 'test.filter' );
+ } );
+
+ QUnit.test( 'add and remove an action', function() {
+ expect( 1 );
+ window.actionValue = '';
+ wp.hooks.addAction( 'test.action', action_a );
+ wp.hooks.removeAction( 'test.action' );
+ wp.hooks.doAction( 'test.action' );
+ equal( window.actionValue, '' );
+ } );
+
+ QUnit.test( 'add an action and run it', function() {
+ expect( 1 );
+ window.actionValue = '';
+ wp.hooks.addAction( 'test.action', action_a );
+ wp.hooks.doAction( 'test.action' );
+ equal( window.actionValue, 'a' );
+ wp.hooks.removeAction( 'test.action' );
+ } );
+
+ QUnit.test( 'add 2 actions in a row and then run them', function() {
+ expect( 1 );
+ window.actionValue = '';
+ wp.hooks.addAction( 'test.action', action_a );
+ wp.hooks.addAction( 'test.action', action_b );
+ wp.hooks.doAction( 'test.action' );
+ equal( window.actionValue, 'ab' );
+ wp.hooks.removeAction( 'test.action' );
+ } );
+
+ QUnit.test( 'add 3 actions with different priorities and run them', function() {
+ expect( 1 );
+ window.actionValue = '';
+ wp.hooks.addAction( 'test.action', action_a );
+ wp.hooks.addAction( 'test.action', action_b, 2 );
+ wp.hooks.addAction( 'test.action', action_c, 8 );
+ wp.hooks.doAction( 'test.action' );
+ equal( window.actionValue, 'bca' );
+ wp.hooks.removeAction( 'test.action' );
+ } );
+
+ QUnit.test( 'pass in two arguments to an action', function() {
+ var arg1 = 10,
+ arg2 = 20;
+
+ expect( 4 );
+
+ wp.hooks.addAction( 'test.action', function( a, b ) {
+ equal( arg1, a );
+ equal( arg2, b );
+ } );
+ wp.hooks.doAction( 'test.action', arg1, arg2 );
+ wp.hooks.removeAction( 'test.action' );
+
+ equal( arg1, 10 );
+ equal( arg2, 20 );
+ } );
+
+ QUnit.test( 'fire action multiple times', function() {
+ var func;
+ expect( 2 );
+
+ func = function() {
+ ok( true );
+ };
+
+ wp.hooks.addAction( 'test.action', func );
+ wp.hooks.doAction( 'test.action' );
+ wp.hooks.doAction( 'test.action' );
+ wp.hooks.removeAction( 'test.action' );
+ } );
+
+ QUnit.test( 'remove specific action callback', function() {
+ window.actionValue = '';
+ wp.hooks.addAction( 'test.action', action_a );
+ wp.hooks.addAction( 'test.action', action_b, 2 );
+ wp.hooks.addAction( 'test.action', action_c, 8 );
+
+ wp.hooks.removeAction( 'test.action', action_b );
+ wp.hooks.doAction( 'test.action' );
+ equal( window.actionValue, 'ca' );
+ wp.hooks.removeAction( 'test.action' );
+ } );
+
+ QUnit.test( 'remove all action callbacks', function() {
+ window.actionValue = '';
+ wp.hooks.addAction( 'test.action', action_a );
+ wp.hooks.addAction( 'test.action', action_b, 2 );
+ wp.hooks.addAction( 'test.action', action_c, 8 );
+
+ wp.hooks.removeAllActions( 'test.action' );
+ wp.hooks.doAction( 'test.action' );
+ equal( window.actionValue, '' );
+ } );
+
+ QUnit.test( 'remove specific filter callback', function() {
+ wp.hooks.addFilter( 'test.filter', filter_a );
+ wp.hooks.addFilter( 'test.filter', filter_b, 2 );
+ wp.hooks.addFilter( 'test.filter', filter_c, 8 );
+
+ wp.hooks.removeFilter( 'test.filter', filter_b );
+ equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'testca' );
+ wp.hooks.removeFilter( 'test.filter' );
+ } );
+
+ QUnit.test( 'remove all filter callbacks', function() {
+ wp.hooks.addFilter( 'test.filter', filter_a );
+ wp.hooks.addFilter( 'test.filter', filter_b, 2 );
+ wp.hooks.addFilter( 'test.filter', filter_c, 8 );
+
+ wp.hooks.removeAllFilters( 'test.filter' );
+ equal( wp.hooks.applyFilters( 'test.filter', 'test' ), 'test' );
+ } );
+
+ // Test doingAction, didAction, hasAction.
+ QUnit.test( 'Test doingAction, didAction and hasAction.', function() {
+
+ // Reset state for testing.
+ wp.hooks.removeAction( 'test.action' );
+ wp.hooks.addAction( 'another.action', function(){} );
+ wp.hooks.doAction( 'another.action' );
+
+ // Verify no action is running yet.
+ ok( ! wp.hooks.doingAction( 'test.action' ), 'The test.action is not running.' );
+ equal( wp.hooks.didAction( 'test.action' ), 0, 'The test.action has not run.' );
+ ok( ! wp.hooks.hasAction( 'test.action' ), 'The test.action is not registered.' );
+
+ wp.hooks.addAction( 'test.action', action_a );
+
+ // Verify action added, not running yet.
+ ok( ! wp.hooks.doingAction( 'test.action' ), 'The test.action is not running.' );
+ equal( wp.hooks.didAction( 'test.action' ), 0, 'The test.action has not run.' );
+ ok( wp.hooks.hasAction( 'test.action' ), 'The test.action is registered.' );
+
+ wp.hooks.doAction( 'test.action' );
+
+ // Verify action added and running.
+ ok( wp.hooks.doingAction( 'test.action' ), 'The test.action is running.' );
+ equal( wp.hooks.didAction( 'test.action' ), 1, 'The test.action has run once.' );
+ ok( wp.hooks.hasAction( 'test.action' ), 'The test.action is registered.' );
+
+ wp.hooks.doAction( 'test.action' );
+ equal( wp.hooks.didAction( 'test.action' ), 2, 'The test.action has run twice.' );
+
+ wp.hooks.removeAction( 'test.action' );
+
+ // Verify state is reset appropriately.
+ ok( wp.hooks.doingAction( 'test.action' ), 'The test.action is running.' );
+ equal( wp.hooks.didAction( 'test.action' ), 0, 'The test.action has not run.' );
+ ok( ! wp.hooks.hasAction( 'test.action' ), 'The test.action is not registered.' );
+
+ wp.hooks.doAction( 'another.action' );
+ ok( ! wp.hooks.doingAction( 'test.action' ), 'The test.action is running.' );
+
+ // Verify hasAction returns false when no matching action.
+ ok( ! wp.hooks.hasAction( 'notatest.action' ), 'The notatest.action is registered.' );
+
+ } );
+
+ QUnit.test( 'Verify doingFilter, didFilter and hasFilter.', function() {
+ expect( 4 );
+ wp.hooks.addFilter( 'runtest.filter', filter_check );
+
+ // Verify filter added and running.
+ var test = wp.hooks.applyFilters( 'runtest.filter', true );
+ equal( wp.hooks.didFilter( 'runtest.filter' ), 1, 'The runtest.filter has run once.' );
+ ok( wp.hooks.hasFilter( 'runtest.filter' ), 'The runtest.filter is registered.' );
+ ok( ! wp.hooks.hasFilter( 'notatest.filter' ), 'The notatest.filter is not registered.' );
+
+ wp.hooks.removeFilter( 'runtest.filter' );
+ } );
+
+} )( window.QUnit );