-
Notifications
You must be signed in to change notification settings - Fork 29
Add the WordPress hook library from 21170-core #13
Changes from 73 commits
6bca9da
85c73be
dc71cd1
4013c9a
ff65810
be62409
45d70c3
632c98a
77c9906
b4b713d
9d24e77
57f6038
1901b43
b05f488
f6c2b45
f50aadb
bee29d7
f8e129d
b02296c
aeca7df
678f790
78acfe1
637af4d
b7b30f4
b351db4
498e15b
e3392c3
53294fd
07b61a0
ee303e6
b1251e3
93998e1
7eda5e2
0e9986a
c9890db
de6efd7
ad47998
e217d7f
562cf37
f882313
9eeb198
fd0ccd3
fc48fbf
566af31
6ce6268
83dffa4
2a60b9f
79ef421
742857a
bf69f89
bd911d0
b802c1d
4e393d6
5d0513c
c11003c
ad09872
a19fa3b
0498ee8
5b8c5f3
6327a54
dd10eab
8be21e5
70b3ebc
a3b4901
6653227
2e9b96e
54f70c1
afa92ea
0919ab9
d319b9d
90942a7
e2547d9
cefc79a
ca95500
cc8b852
06ddd95
edd46d0
77be5ed
2c62cdc
581e715
4c7f252
ef06958
319a023
4ece207
aa709ac
a8628a0
242f351
d8af28f
650e9e2
a617214
30e5e82
3a13768
7c1fc29
45747e5
a24847b
9f4cefc
73744fa
7465667
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# WP-JS-Hooks | ||
|
||
A lightweight & efficient EventManager for JavaScript in WordPress. | ||
|
||
|
||
### API Usage | ||
API functions can be called via the global `wp.hooks` like this `wp.hooks.addAction()`, etc. | ||
|
||
* `addAction( 'namespace.identifier', callback, priority )` | ||
* `addFilter( 'namespace.identifier', callback, priority )` | ||
* `removeAction( 'namespace.identifier', callback )` | ||
* `removeFilter( 'namespace.identifier', callback )` | ||
* `removeAllActions( 'namespace.identifier' )` | ||
* `removeAllFilters( 'namespace.identifier' )` | ||
* `doAction( 'namespace.identifier', arg1, arg2, moreArgs, finalArg )` | ||
* `applyFilters( 'namespace.identifier', content )` | ||
* `doingAction( 'namespace.identifier' )` | ||
* `doingFilter( 'namespace.identifier' )` | ||
* `didAction( 'namespace.identifier' )` | ||
* `didFilter( 'namespace.identifier' )` | ||
* `hasAction( 'namespace.identifier' )` | ||
* `hasFilter( 'namespace.identifier' )` | ||
|
||
|
||
### Background | ||
See ticket [#21170](http://core.trac.wordpress.org/ticket/21170) for more information. | ||
|
||
|
||
### Features | ||
|
||
* Fast and lightweight. | ||
* Priorities system ensures hooks with lower integer priority are fired first. | ||
* Uses native object hash lookup for finding hook callbacks. | ||
* Utilizes insertion sort for keeping priorities correct. Best Case: O(n), worst case: O(n^2) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd suggest a https://docs.npmjs.com/files/package.json#files e.g. omitting tests. |
||
"name": "@wordpress/hooks", | ||
"version": "0.1.0", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/WordPress/packages.git" | ||
}, | ||
"description": "WordPress Hooks library", | ||
"main": "build/index.js", | ||
"module": "build-module/index.js", | ||
"browser": "build-browser/index.js", | ||
"author": "WordPress", | ||
"license": "GPL-2.0+" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/** | ||
* Returns a function which, when invoked, will add a hook. | ||
* | ||
* @param {Object} hooks Stored hooks, keyed by hook name. | ||
* | ||
* @return {Function} Function that adds a new hook. | ||
*/ | ||
function createAddHook( hooks ) { | ||
/** | ||
* Adds the hook to the appropriate hooks container. | ||
* | ||
* @param {string} hookName 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 addHook( hookName, callback, priority = 10 ) { | ||
if ( typeof hookName !== 'string' ) { | ||
console.error( 'The hook name must be a string.' ); | ||
return; | ||
} | ||
|
||
if ( /^__/.test( hookName ) ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the interest of starting with a restrictive API that allows for expansion later, let's restrict hook names to something like |
||
console.error( 'The hook name cannot begin with `__`.' ); | ||
return; | ||
} | ||
|
||
if ( typeof callback !== 'function' ) { | ||
console.error( 'The hook callback must be a function.' ); | ||
return; | ||
} | ||
|
||
// Validate numeric priority | ||
if ( typeof priority !== 'number' ) { | ||
console.error( 'If specified, the hook priority must be a number.' ); | ||
return; | ||
} | ||
|
||
const handler = { callback, priority }; | ||
|
||
if ( hooks.hasOwnProperty( hookName ) ) { | ||
// Find the correct insert index of the new hook. | ||
const handlers = hooks[ hookName ].handlers; | ||
let i = 0; | ||
while ( i < handlers.length ) { | ||
if ( handlers[ i ].priority > priority ) { | ||
break; | ||
} | ||
i++; | ||
} | ||
// Insert (or append) the new hook. | ||
handlers.splice( i, 0, handler ); | ||
// We may also be currently executing this hook. If the callback | ||
// we're adding would come after the current callback, there's no | ||
// problem; otherwise we need to increase the execution index of | ||
// any other runs by 1 to account for the added element. | ||
( hooks.__current || [] ).forEach( hookInfo => { | ||
if ( hookInfo.name === hookName && hookInfo.currentIndex >= i ) { | ||
hookInfo.currentIndex++; | ||
} | ||
} ); | ||
} else { | ||
// This is the first hook of its type. | ||
hooks[ hookName ] = { | ||
handlers: [ handler ], | ||
runs: 0, | ||
}; | ||
} | ||
}; | ||
} | ||
|
||
export default createAddHook; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/** | ||
* Returns a function which, when invoked, will return the name of the | ||
* currently running hook, or `null` if no hook of the given type is currently | ||
* running. | ||
* | ||
* @param {Object} hooks Stored hooks, keyed by hook name. | ||
* | ||
* @return {Function} Function that returns the current hook. | ||
*/ | ||
function createCurrentHook( hooks, returnFirstArg ) { | ||
/** | ||
* Returns the name of the currently running hook, or `null` if no hook of | ||
* the given type is currently running. | ||
* | ||
* @return {?string} The name of the currently running hook, or | ||
* `null` if no hook is currently running. | ||
*/ | ||
return function currentHook() { | ||
if ( ! hooks.__current || ! hooks.__current.length ) { | ||
return null; | ||
} | ||
|
||
return hooks.__current[ hooks.__current.length - 1 ].name; | ||
}; | ||
} | ||
|
||
export default createCurrentHook; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
/** | ||
* Returns a function which, when invoked, will return the number of times a | ||
* hook has been called. | ||
* | ||
* @param {Object} hooks Stored hooks, keyed by hook name. | ||
* | ||
* @return {Function} Function that returns a hook's call count. | ||
*/ | ||
function createDidHook( hooks ) { | ||
/** | ||
* Returns the number of times an action has been fired. | ||
* | ||
* @param {string} hookName The hook name to check. | ||
* | ||
* @return {number} The number of times the hook has run. | ||
*/ | ||
return function didHook( hookName ) { | ||
return hooks.hasOwnProperty( hookName ) && hooks[ hookName ].runs | ||
? hooks[ hookName ].runs | ||
: 0; | ||
}; | ||
} | ||
|
||
export default createDidHook; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/** | ||
* Returns a function which, when invoked, will return whether a hook is | ||
* currently being executed. | ||
* | ||
* @param {Object} hooks Stored hooks, keyed by hook name. | ||
* | ||
* @return {Function} Function that returns whether a hook is currently | ||
* being executed. | ||
*/ | ||
function createDoingHook( hooks ) { | ||
/** | ||
* Returns whether a hook is currently being executed. | ||
* | ||
* @param {?string} hookName The name of the hook to check for. If | ||
* omitted, will check for any hook being executed. | ||
* | ||
* @return {bool} Whether the hook is being executed. | ||
*/ | ||
return function doingHook( hookName ) { | ||
// If the hookName was not passed, check for any current hook. | ||
if ( 'undefined' === typeof hookName ) { | ||
return 'undefined' !== typeof hooks.__current[0]; | ||
} | ||
|
||
// Return the __current hook. | ||
return hooks.__current[0] | ||
? hookName === hooks.__current[0].name | ||
: false; | ||
}; | ||
} | ||
|
||
export default createDoingHook; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
/** | ||
* Returns a function which, when invoked, will return whether any handlers are | ||
* attached to a particular hook. | ||
* | ||
* @param {Object} hooks Stored hooks, keyed by hook name. | ||
* | ||
* @return {Function} Function that returns whether any handlers are | ||
* attached to a particular hook. | ||
*/ | ||
function createHasHook( hooks ) { | ||
/** | ||
* Returns how many handlers are attached for the given hook. | ||
* | ||
* @param {string} hookName The name of the hook to check for. | ||
* | ||
* @return {number} The number of handlers that are attached to | ||
* the given hook. | ||
*/ | ||
return function hasHook( hookName ) { | ||
return hooks.hasOwnProperty( hookName ) | ||
? hooks[ hookName ].handlers.length | ||
: 0; | ||
}; | ||
} | ||
|
||
export default createHasHook; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
/** | ||
* Returns a function which, when invoked, will remove a specified hook or all | ||
* hooks by the given name. | ||
* | ||
* @param {Object} hooks Stored hooks, keyed by hook name. | ||
* @param {bool} removeAll Whether to remove all hooked callbacks. | ||
* | ||
* @return {Function} Function that removes hooks. | ||
*/ | ||
function createRemoveHook( hooks, removeAll ) { | ||
/** | ||
* Removes the specified callback (or all callbacks) from the hook with a | ||
* given name. | ||
* | ||
* @param {string} hookName The name of the hook to modify. | ||
* @param {?Function} callback The specific callback to be removed. If | ||
* omitted (and `removeAll` is truthy), clears | ||
* all callbacks. | ||
* | ||
* @return {number} The number of callbacks removed. | ||
*/ | ||
return function removeHook( hookName, callback ) { | ||
if ( ! removeAll && typeof callback !== 'function' ) { | ||
console.error( 'The hook callback to remove must be a function.' ); | ||
return; | ||
} | ||
|
||
// Bail if no hooks exist by this name | ||
if ( ! hooks.hasOwnProperty( hookName ) ) { | ||
return 0; | ||
} | ||
|
||
let handlersRemoved = 0; | ||
|
||
if ( removeAll ) { | ||
handlersRemoved = hooks[ hookName ].handlers.length; | ||
hooks[ hookName ] = { | ||
runs: hooks[ hookName ].runs, | ||
handlers: [], | ||
}; | ||
} else { | ||
// Try to find the specified callback to remove. | ||
const handlers = hooks[ hookName ].handlers; | ||
for ( let i = handlers.length - 1; i >= 0; i-- ) { | ||
if ( handlers[ i ].callback === callback ) { | ||
handlers.splice( i, 1 ); | ||
handlersRemoved++; | ||
// This callback may also be part of a hook that is | ||
// currently executing. If the callback we're removing | ||
// comes after the current callback, there's no problem; | ||
// otherwise we need to decrease the execution index of any | ||
// other runs by 1 to account for the removed element. | ||
( hooks.__current || [] ).forEach( hookInfo => { | ||
if ( hookInfo.name === hookName && hookInfo.currentIndex >= i ) { | ||
hookInfo.currentIndex--; | ||
} | ||
} ); | ||
} | ||
} | ||
} | ||
|
||
return handlersRemoved; | ||
}; | ||
} | ||
|
||
export default createRemoveHook; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
/** | ||
* Returns a function which, when invoked, will execute all callbacks | ||
* registered to a hook of the specified type, optionally returning the final | ||
* value of the call chain. | ||
* | ||
* @param {Object} hooks Stored hooks, keyed by hook name. | ||
* @param {?bool} returnFirstArg Whether each hook callback is expected to | ||
* return its first argument. | ||
* | ||
* @return {Function} Function that runs hook callbacks. | ||
*/ | ||
function createRunHook( hooks, returnFirstArg ) { | ||
/** | ||
* Runs all callbacks for the specified hook. | ||
* | ||
* @param {string} hookName The name of the hook to run. | ||
* @param {...*} args Arguments to pass to the hook callbacks. | ||
* | ||
* @return {*} Return value of runner, if applicable. | ||
*/ | ||
return function runHooks( hookName, ...args ) { | ||
if ( 'string' !== typeof hookName ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Requiring |
||
console.error( 'The hook name must be a string.' ); | ||
return; | ||
} | ||
|
||
if ( /^__/.test( hookName ) ) { | ||
console.error( 'The hook name cannot begin with `__`.' ); | ||
return; | ||
} | ||
|
||
if ( ! /^[a-z][a-z0-9_]*$/.test( hookName ) ) { | ||
console.error( 'The hook name can only contain numbers, letters and underscores.' ); | ||
return; | ||
} | ||
|
||
if ( ! hooks.hasOwnProperty( hookName ) ) { | ||
hooks[ hookName ] = { | ||
runs: 0, | ||
handlers: [], | ||
}; | ||
} | ||
|
||
const handlers = hooks[ hookName ].handlers; | ||
|
||
if ( ! handlers.length ) { | ||
return returnFirstArg | ||
? args[ 0 ] | ||
: undefined; | ||
} | ||
|
||
const hookInfo = { | ||
name: hookName, | ||
currentIndex: 0, | ||
}; | ||
|
||
hooks.__current = hooks.__current || []; | ||
hooks.__current.push( hookInfo ); | ||
hooks[ hookName ].runs++; | ||
|
||
let maybeReturnValue = args[ 0 ]; | ||
|
||
while ( hookInfo.currentIndex < handlers.length ) { | ||
const handler = handlers[ hookInfo.currentIndex ]; | ||
maybeReturnValue = handler.callback.apply( null, args ); | ||
if ( returnFirstArg ) { | ||
args[ 0 ] = maybeReturnValue; | ||
} | ||
hookInfo.currentIndex++; | ||
} | ||
|
||
hooks.__current.pop(); | ||
|
||
if ( returnFirstArg ) { | ||
return maybeReturnValue; | ||
} | ||
}; | ||
} | ||
|
||
export default createRunHook; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed during core JS office hours today, I'd like to see us alter how we refer to these components -- in jQuery and other libraries "namespace" refers to the your-code-specific part following the period, and the event name is the part before the period. I believe that breaking from that convention would lead to a lot of confusion.
What I would propose instead:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, action name should come first if that is the way we want to go. However, if it's required, maybe we should just make it a separate argument. See discussion starting at https://core.trac.wordpress.org/ticket/21170#comment:129.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kadamwhite this has been updated, can you please review the current format?