| | 1 | ( function( window, undefined ) { |
| | 2 | 'use strict'; |
| | 3 | |
| | 4 | /** |
| | 5 | * Handles managing all events for whatever you plug it into. Priorities for hooks are based on lowest to highest in |
| | 6 | * that, lowest priority hooks are fired first. |
| | 7 | */ |
| | 8 | var EventManager = function() { |
| | 9 | var slice = Array.prototype.slice; |
| | 10 | |
| | 11 | /** |
| | 12 | * Maintain a reference to the object scope so our public methods never get confusing. |
| | 13 | */ |
| | 14 | var MethodsAvailable = { |
| | 15 | removeFilter : removeFilter, |
| | 16 | applyFilters : applyFilters, |
| | 17 | addFilter : addFilter, |
| | 18 | removeAction : removeAction, |
| | 19 | doAction : doAction, |
| | 20 | addAction : addAction |
| | 21 | }; |
| | 22 | |
| | 23 | /** |
| | 24 | * Contains the hooks that get registered with this EventManager. The array for storage utilizes a "flat" |
| | 25 | * object literal such that looking up the hook utilizes the native object literal hash. |
| | 26 | */ |
| | 27 | var STORAGE = { |
| | 28 | actions : {}, |
| | 29 | filters : {} |
| | 30 | }; |
| | 31 | |
| | 32 | /** |
| | 33 | * Adds an action to the event manager. |
| | 34 | * |
| | 35 | * @param {string} action The action to perform. |
| | 36 | * @param {Function} callback Must be a valid callback function before this action is added |
| | 37 | * @param {number} [priority=10] Controls when the function is executed in relation to other callbacks bound |
| | 38 | * to the same hook. Optional, defaults to 10. |
| | 39 | * @param {Object} [context=this] The context to bind when executing the callback.Optionsl, defaults to `this`. |
| | 40 | */ |
| | 41 | function addAction( action, callback, priority, context ) { |
| | 42 | if ( typeof action === 'string' && typeof callback === 'function' ) { |
| | 43 | priority = parseInt( ( priority || 10 ), 10 ); |
| | 44 | _addHook( 'actions', action, callback, priority, context || this ); |
| | 45 | } |
| | 46 | |
| | 47 | return MethodsAvailable; |
| | 48 | } |
| | 49 | |
| | 50 | /** |
| | 51 | * Performs an action if it exists. You can pass as many arguments as you want to this function. |
| | 52 | * The only rule is that the first argument must always be the action. |
| | 53 | * |
| | 54 | * @param {string} action The action to perform. |
| | 55 | * @param {...*} args Optional args to pass to the action. |
| | 56 | */ |
| | 57 | function doAction( /* action, arg1, arg2, ... */ ) { |
| | 58 | var args = slice.call( arguments ); |
| | 59 | var action = args.shift(); |
| | 60 | |
| | 61 | if ( typeof action === 'string' ) { |
| | 62 | _runHook( 'actions', action, args ); |
| | 63 | } |
| | 64 | |
| | 65 | return MethodsAvailable; |
| | 66 | } |
| | 67 | |
| | 68 | /** |
| | 69 | * Removes the specified action if it exists. |
| | 70 | * |
| | 71 | * @param {string} action The action to remove. |
| | 72 | * @param {Function} [callback] Callback function to remove. Optional. |
| | 73 | */ |
| | 74 | function removeAction( action, callback ) { |
| | 75 | if ( typeof action === 'string' ) { |
| | 76 | _removeHook( 'actions', action, callback ); |
| | 77 | } |
| | 78 | |
| | 79 | return MethodsAvailable; |
| | 80 | } |
| | 81 | |
| | 82 | /** |
| | 83 | * Adds a filter to the event manager. |
| | 84 | * |
| | 85 | * @param {string} filter The filter to add. |
| | 86 | * @param {Function} callback The function to call with this filter. |
| | 87 | * @param [priority=10] Used to control when the function is executed in relation to other callbacks bound to the same hook |
| | 88 | * @param [context] Supply a value to be used for this |
| | 89 | */ |
| | 90 | function addFilter( filter, callback, priority, context ) { |
| | 91 | if ( typeof filter === 'string' && typeof callback === 'function' ) { |
| | 92 | priority = parseInt( ( priority || 10 ), 10 ); |
| | 93 | _addHook( 'filters', filter, callback, priority, context ); |
| | 94 | } |
| | 95 | |
| | 96 | return MethodsAvailable; |
| | 97 | } |
| | 98 | |
| | 99 | /** |
| | 100 | * Performs a filter if it exists. You should only ever pass 1 argument to be filtered. |
| | 101 | * The only rule is that the first argument must always be the filter. |
| | 102 | * |
| | 103 | * @param {string} action The action to perform. |
| | 104 | * @param {...*} args Optional args to pass to the action. |
| | 105 | */ |
| | 106 | function applyFilters( /* filter, filtered arg, arg2, ... */ ) { |
| | 107 | var args = slice.call( arguments ); |
| | 108 | var filter = args.shift(); |
| | 109 | |
| | 110 | if ( typeof filter === 'string' ) { |
| | 111 | return _runHook( 'filters', filter, args ); |
| | 112 | } |
| | 113 | |
| | 114 | return MethodsAvailable; |
| | 115 | } |
| | 116 | |
| | 117 | /** |
| | 118 | * Removes the specified filter if it contains a namespace.identifier & exists. |
| | 119 | * |
| | 120 | * @param {string} filter The action to remove. |
| | 121 | * @param [callback] Callback function to remove. Optional. |
| | 122 | */ |
| | 123 | function removeFilter( filter, callback ) { |
| | 124 | if ( typeof filter === 'string') { |
| | 125 | _removeHook( 'filters', filter, callback ); |
| | 126 | } |
| | 127 | |
| | 128 | return MethodsAvailable; |
| | 129 | } |
| | 130 | |
| | 131 | /** |
| | 132 | * Removes the specified hook by resetting the value of it. |
| | 133 | * |
| | 134 | * @param {string} type Type of hook, either 'actions' or 'filters'. |
| | 135 | * @param {string} hook The hook (namespace.identifier) to remove. |
| | 136 | * @private |
| | 137 | */ |
| | 138 | function _removeHook( type, hook, callback, context ) { |
| | 139 | var handlers, handler, i; |
| | 140 | |
| | 141 | if ( !STORAGE[ type ][ hook ] ) { |
| | 142 | return; |
| | 143 | } |
| | 144 | if ( !callback ) { |
| | 145 | STORAGE[ type ][ hook ] = []; |
| | 146 | } else { |
| | 147 | handlers = STORAGE[ type ][ hook ]; |
| | 148 | if ( !context ) { |
| | 149 | for ( i = handlers.length; i--; ) { |
| | 150 | if ( handlers[i].callback === callback ) { |
| | 151 | handlers.splice( i, 1 ); |
| | 152 | } |
| | 153 | } |
| | 154 | } |
| | 155 | else { |
| | 156 | for ( i = handlers.length; i--; ) { |
| | 157 | handler = handlers[i]; |
| | 158 | if ( handler.callback === callback && handler.context === context) { |
| | 159 | handlers.splice( i, 1 ); |
| | 160 | } |
| | 161 | } |
| | 162 | } |
| | 163 | } |
| | 164 | } |
| | 165 | |
| | 166 | /** |
| | 167 | * Adds the hook to the appropriate storage container |
| | 168 | * |
| | 169 | * @param {string} type The hook type: 'actions' or 'filters' |
| | 170 | * @param {string} hook The hook (namespace.identifier) to add to our event manager |
| | 171 | * @param {function} callback The function that will be called when the hook is executed. |
| | 172 | * @param {number} priority The priority of this hook. Must be an integer. |
| | 173 | * @param {mixed} [context] A value to be used for `this`. Optional. |
| | 174 | * @private |
| | 175 | */ |
| | 176 | function _addHook( type, hook, callback, priority, context ) { |
| | 177 | var hookObject = { |
| | 178 | callback : callback, |
| | 179 | priority : priority, |
| | 180 | context : context |
| | 181 | }; |
| | 182 | |
| | 183 | // Utilize 'prop itself' : http://jsperf.com/hasownproperty-vs-in-vs-undefined/19 |
| | 184 | var hooks = STORAGE[ type ][ hook ]; |
| | 185 | if ( hooks ) { |
| | 186 | hooks.push( hookObject ); |
| | 187 | hooks = _hookInsertSort( hooks ); |
| | 188 | } |
| | 189 | else { |
| | 190 | hooks = [ hookObject ]; |
| | 191 | } |
| | 192 | |
| | 193 | STORAGE[ type ][ hook ] = hooks; |
| | 194 | } |
| | 195 | |
| | 196 | /** |
| | 197 | * Use an insert sort for keeping our hooks organized based on priority. |
| | 198 | * This function is ridiculously faster than bubble sort, etc: http://jsperf.com/javascript-sort |
| | 199 | * |
| | 200 | * @param {array} hooks The custom array containing all of the appropriate hooks to perform an insert sort on. |
| | 201 | * @private |
| | 202 | */ |
| | 203 | function _hookInsertSort( hooks ) { |
| | 204 | var tmpHook, j, prevHook; |
| | 205 | for( var i = 1, len = hooks.length; i < len; i++ ) { |
| | 206 | tmpHook = hooks[ i ]; |
| | 207 | j = i; |
| | 208 | while( ( prevHook = hooks[ j - 1 ] ) && prevHook.priority > tmpHook.priority ) { |
| | 209 | hooks[ j ] = hooks[ j - 1 ]; |
| | 210 | --j; |
| | 211 | } |
| | 212 | hooks[ j ] = tmpHook; |
| | 213 | } |
| | 214 | |
| | 215 | return hooks; |
| | 216 | } |
| | 217 | |
| | 218 | /** |
| | 219 | * Runs the specified hook. If it is an action, the value is not modified but if it is a filter, it is. |
| | 220 | * |
| | 221 | * @param {string} type 'actions' or 'filters' |
| | 222 | * @param {string} hook The hook to run. |
| | 223 | * @param {...*} args Arguments to pass to the action/filter. |
| | 224 | * @private |
| | 225 | */ |
| | 226 | function _runHook( type, hook, args ) { |
| | 227 | var handlers = STORAGE[ type ][ hook ], i, len; |
| | 228 | |
| | 229 | if ( ! handlers ) { |
| | 230 | return ( type === 'filters' ) ? args[0] : false; |
| | 231 | } |
| | 232 | |
| | 233 | len = handlers.length; |
| | 234 | if ( type === 'filters' ) { |
| | 235 | for ( i = 0; i < len; i++ ) { |
| | 236 | args[ 0 ] = handlers[ i ].callback.apply( handlers[ i ].context, args ); |
| | 237 | } |
| | 238 | } else { |
| | 239 | for ( i = 0; i < len; i++ ) { |
| | 240 | handlers[ i ].callback.apply( handlers[ i ].context, args ); |
| | 241 | } |
| | 242 | } |
| | 243 | |
| | 244 | return ( type === 'filters' ) ? args[ 0 ] : true; |
| | 245 | } |
| | 246 | |
| | 247 | // return all of the publicly available methods |
| | 248 | return MethodsAvailable; |
| | 249 | |
| | 250 | }; |
| | 251 | |
| | 252 | window.wp = window.wp || {}; |
| | 253 | window.wp.hooks = new EventManager(); |
| | 254 | |
| | 255 | } )( window ); |