/********************************************************************** * * * **********************************************************************/ //var DEBUG = DEBUG != null ? DEBUG : true define(function(require){ var module = {} var args2array = require('lib/util').args2array var toggler = require('lib/toggler') var object = require('lib/object') /*********************************************************************/ // Actions // // Actions are an extension to the JavaScript object model tailored for // a set of specific tasks. // // Goals: // - provide a unified mechanism to define and manage user API's for // use in UI-hooks, keyboard mappings, scripting, ... etc. // - a means to generate configuration UI's // - a means to generate documentation // // // The main entities: // // Action set // - an object containing a number of actions, // - optionally, directly or indirectly inherited from MetaActions // and/or other action sets, // - the action handlers are bound relative to it (._action_handlers) // // Action // // + pre + pre + + post + post + // Action event handler: o-------x o-------x // v ^ // Actions o-------x o-------x // v ^ // Root Action o---|---x // // - a method, created by Action(..), // - calls all the shadowed/overloaded actions in the inheritance // chain in sequence implicitly, // NOTE: there is no way to prevent an action in the chain from // running, this is by design, i.e. no way to fully shadow. // - actions that do not shadow anything are called root actions. // - returns the action set by default (for call chaining), // - the base/root action can return any value. // NOTE: if undefined is returned, it will be replaced by the // action context/action set. // NOTE: there is no distinction between root and other actions // other than that root action's return values are not // ignored. // - can consist of two parts: the first is called before the // shadowed action (pre-callback) and the second after (post-callback). // - post-callback has access to the return value and can modify it // but not replace it. // - can be bound to, a-la an event, calling the handlers when it is // called, // // Action (event) handler // - a function, // - can be bound to run before and/or after the action itself, // - is local to an action set it was bound via, // - when an action is triggered from an action set, all the pre // handlers in its inheritance chain will be called before the // respective actions they are bound to and all the post handlers // are called directly after. // - pre handlers are passed the same arguments the original actions // got when it was called. // - post action handlers will get the root action result as first // argument succeeded by the action arguments. // // // // The action system provides three components: // // 1) Documentation generation and introspection (MetaActions) // // .getDoc() // .getDoc([, ..]) // -> dict of action-name, doc // // .actions // -> list of action names // // .length // -> number of actions // // // 2) Event-like callbacks for actions (MetaActions, Action) // // .on('action', function(){ ... }) // .on('action.post', function(){ ... }) // // .on('action.pre', function(){ ... }) // // // 3) A mechanism to define and extend already defined actions // This replaces / complements the standard JavaScript overloading // mechanisms (Action, Actions) // // // Actions... // var X = Actions({ // m: [function(){ console.log('m') }] // }) // var O = Actions(X, { // m: [function(){ // console.log('pre') // return function(res){ // console.log('post') // } // }] // }) // // NOTE: what is done here is similar to calling O.__proto__.m.call(..) // but is implicit, and not dependant on the original containing // object name/reference ('O'), thus enabling an action to be // referenced and called from any object and still chain correctly. // // // /*********************************************************************/ // helpers... /*********************************************************************/ // Construct an action object... // // Action function format: // // // pre event code... // function(..){ // ... // pre code // } // // // pre/post event code... // function(..){ // ... // pre code // return function(, ..){ // ... // post code // } // } // // // same as above but using a deferred instead of a callback... // function(..){ // ... // pre code // return $.Deferred() // .done(function(, ..){ // ... // post code // }) // } // // // An action is essentially a method with several additional features: // // - actions are split into two stages: // pre: the code of the method is executed before the action // event is fired // post: if the action returns a callback function or a deferred // object it will be executed after the event is fired // NOTE: the signature if the post stage is the same as the // action's with the added return value as first argument // (the rest og the arguments are shifted by 1). // // - actions automatically call the shadowed action, the pre stage is // executed down-up while the post stage is run in reverse order, // i.e. the pre is going down and the post is going up. // // - actions provide an event-like mechanism to register handlers or // callbacks. These callbacks are local to a specific object and will // be fired on action event/call starting from the current action // caller and down the inheritance chain, i.e. all event handlers // registered from the current object and up to the base action set // will be fired. // // - an action will return the deepest (root) action's return, if that // return is undefined, then the action set is returned instead. // // - action arguments are "threaded" through the action chain down and // root action return value and arguments are threaded back up the // action chain. // // NOTE: if the root handler is instance of Toggler (jli) and the action // is called with '?'/'??' as argument, then the toggler will be // called with the argument and return the result bypassing the // handlers. // NOTE: actions once defined do not depend on the inheritance hierarchy, // other than the .getHandlers(..) method. If this method is not // found in the inheritance chain (i.e. the link to MetaActions) // was severed, then the default will be used: // MetaActions.getHandlers(..) // This makes it possible to redefine the method if needed but // prevents the system from breaking when an action set gets // disconnected from MetaActions. This can be useful, for example, // to remove .on(..) / .off(..) handler functionality. // XXX is this correct?? // NOTE: by default an action will return 'this', i.e. the action set // object the action was called from. // // XXX add more metadata/docs: // .section // .category // ... // XXX might be a good idea to add an option to return the full results... var Action = module.Action = function Action(name, doc, ldoc, func){ // we got called without a 'new'... if(this == null || this.constructor !== Action){ // XXX using something like .apply(.., arguemnts) would be more // generel but have no time to figure out how to pass it // to new without the later complaining... return new Action(name, doc, ldoc, func) } // prevent action overloading... if(this[name] != null){ throw 'action "'+name+'" already exists.' } // create the actual instance we will be returning... var meth = function(){ var args = args2array(arguments) var that = this var getHandlers = this.getHandlers || MetaActions.getHandlers var isToggler = this.isToggler || MetaActions.isToggler // get handlers... // // NOTE: using CLASS.__proto__[name].call(this, ...) here is not // possible as there is no reliable way to get the "class" // the current method is referenced from. // ...searching the inheritance chain is not reliable as a // method can be referenced more than once, both with the // same as well as under different names... var handlers = getHandlers.call(this, name) // .map(function(h){ return h.apply(that, args) }) // special case: if the root handler is a toggler and we call // it with '?'/'??' then do not call the handlers... // XXX might be good to make this modular/configurable... if(isToggler.call(this, name)//handlers.slice(-1)[0] instanceof Toggler && args.length == 1 && (args[0] == '?' || args[0] == '??')){ return handlers.slice(-1)[0].apply(this, args) } // call handlers -- pre phase... handlers = handlers .map(function(h){ return h.apply(that, args) }) // NOTE: this action will get included and called by the code // above and below, so no need to explicitly call func... // call handlers -- post phase... // NOTE: post handlers need to get called last run pre first run post... var results = [] handlers.reverse().forEach(function(h, i){ var res = h // function... if(h instanceof Function){ //res = h.apply(that, args) res = h.apply(that, [results[0] !== undefined ? results[0] : that].concat(args)) // deferred... } else if(h != null && h.resolve instanceof Function){ //res = h.resolve() res = h.resolve.apply(h, [results[0] !== undefined ? results[0] : that].concat(args)) } results.push(res) }) // XXX might be a good idea to add an option to return the full // results... return results[0] !== undefined ? results[0] : this } meth.__proto__ = this.__proto__ // populate the action attributes... meth.name = name meth.doc = doc meth.long_doc = ldoc meth.func = func return meth } // this will make action instances behave like real functions... Action.prototype.__proto__ = Function // A base action-set object... // // This will define a set of action-set specific methods and helpers. // // XXX .off(...) needs more work... // XXX need a mechanism to publish specific attrs... var MetaActions = module.MetaActions = { // List actions... // get actions(){ var res = [] for(var k in this){ // avoid recursion... if(k == 'actions' || k == 'length'){ continue } // get only actions... if(this[k] instanceof Action){ res.push(k) } } return res }, // Number of defined actions... // get length(){ return this.actions.length }, // Get action documentation... // getDoc: function(actions){ var res = {} var that = this actions = actions == null ? this.actions : arguments.length > 1 ? args2array(arguments) : typeof(actions) == typeof('str') ? [actions] : actions // get the first defined set of docs in the inheritance chain... actions.forEach(function(n){ var cur = that res[n] = [] // go up the proto chain... while(cur.__proto__ != null){ if(cur[n] != null && cur[n].doc != null){ res[n] = [ cur[n].doc, cur[n].long_doc ] break } cur = cur.__proto__ } }) return res }, getPath: function(actions){ var res = {} var that = this actions = actions == null ? this.actions : arguments.length > 1 ? args2array(arguments) : typeof(actions) == typeof('str') ? [actions] : actions // get the first defined set of docs in the inheritance chain... actions.forEach(function(n){ var cur = that // go up the proto chain... while(cur.__proto__ != null){ if(cur[n] != null && cur[n].doc != null){ var doc = cur[n].doc var long_doc = cur[n].long_doc break } cur = cur.__proto__ } res[(doc && doc.replace(/[\\\/]$/, '/'+n)) || n] = [n, doc, long_doc] }) return res }, // Get action handlers from the inheritance chain... // // NOTE: this collects both the event handlers (in order of hierarchy, // then order of definition) and actions (in order of hierarchy) // NOTE: this is the correct order for 'pre' calling, but is the // reverse of how the 'post' handlers must be called. // // For more docs on handler sequencing and definition see: .on(..) getHandlers: function(name){ var handlers = [] var cur = this while(cur.__proto__ != null){ // get action "event" handlers... if(cur.hasOwnProperty('_action_handlers') && name in cur._action_handlers){ handlers.splice.apply(handlers, [handlers.length, 0].concat(cur._action_handlers[name])) } // get the overloading action... // NOTE: this will get all the handlers including the root // and the current handlers... // NOTE: this will ignore "shadows" that are not actions... if(cur.hasOwnProperty(name) && cur[name] instanceof Action){ handlers.push(cur[name].func) } cur = cur.__proto__ } return handlers }, // Test if the action is a Toggler... // // NOTE: an action is considered a toggler only if it's base action // is a toggler (instance of Toggler), thus, the same "top" // action can be or not be a toggler in different contexts. // // For more info on togglers see: lib/toggler.js isToggler: function(name){ var handlers = (this.getHandlers || MetaActions.getHandlers) .call(this, name) if(handlers.slice(-1)[0] instanceof toggler.Toggler){ return true } return false }, // Register an action callback... // // Register a post action callback // .on('action', [, ]) // .on('action.post', [, ]) // -> // // Register a pre action callback // .on('action.pre', [, ]) // -> // // Modes: // 'pre' - the handler is fired before the action is triggered, // and if the handler returns a deferred or a function // then that will get resolved, called resp. after // the action is done. // 'post' - the handler is fired after the action is finished. // this is the default. // // Handler Arguments: // 'pre' - the handler will get the same arguments as the main // action when called. // 'post' - the handler will get the action return value followed // by action arguments. // // The optional tag marks the handler to enable group removal via // .off(..) // // NOTE: 'post' mode is the default. // // XXX should we have multiple tags per handler??? on: function(actions, b, c){ var handler = typeof(c) == 'function' ? c : b var tag = typeof(c) == 'function' ? b : c // XXX make this split by whitespace... actions = typeof(actions) == 'string' ? actions.split(/ +/) : actions var that = this actions.forEach(function(action){ // prepare the handler... var mode = action.split('.') action = mode[0] mode = mode[1] // keep the original handler for future use... var a_handler = handler // a post handler (default)... if(mode == null || mode == 'post'){ var old_handler = a_handler a_handler = function(){ return old_handler } // NOTE: this is set so as to identify the handler for removal // via. .off(..) a_handler.orig_handler = old_handler.orig_handler || old_handler // not pre mode... } else if(mode != 'pre') { // XXX throw 'Unknown action mode: '+action+'.'+mode } a_handler.tag = tag // register handlers locally only... if(!that.hasOwnProperty('_action_handlers')){ that._action_handlers = {} } if(!(action in that._action_handlers)){ that._action_handlers[action] = [] } // register a handler only once... if(that._action_handlers[action].indexOf(a_handler) < 0){ // NOTE: last registered is first... that._action_handlers[action].splice(0, 0, a_handler) } }) return this }, // Remove an action callback... // // Remove all handlers from action: // .off('action') // .off('action', '*') // .off('action', 'all') // -> // // Remove specific handler from action: // .off('action', ) // -> // // Remove handlers from action by tag: // .off('action', ) // -> // // NOTE: the handler passed to .off(..) for removal must be the same // as the handler passed to .on(..) / .one(..) off: function(actions, handler){ if(this.hasOwnProperty('_action_handlers')){ actions = actions == '*' ? Object.keys(this._action_handlers) : typeof(actions) == 'string' ? actions.split(' ') : actions var that = this actions.forEach(function(action){ var mode = action.split('.') action = mode[0] mode = mode[1] // get the handlers... var h = that._action_handlers[action] // remove explicit handler... if(typeof(handler) == 'function'){ var i = -1 if(mode == null || mode == 'post'){ // XXX find via e.orig_handler == handler && e.mode == 'post' h.forEach(function(e, j){ // NOTE: we will only get the first match... if(e.orig_handler === handler && i == -1){ i = j } }) } else if(mode == 'pre'){ i = h.indexOf(handler) } // NOTE: unknown modes are skipped... if(i >= 0){ h.splice(i, 1) } // remove all handlers... } else if(handler == null || handler == 'all' || handler == '*'){ h.splice(0, h.length) // remove handlers by tag... } else { // filter out everything that mathches a tag in-place... h.splice.apply(h, [0, h.length] .concat(h.filter(function(e){ return e.tag != handler }))) } }) } return this }, // Register an action callback that will only fire once per event... // // This is signature compatible with .on(..) one: function(actions, b, c){ var handler = typeof(c) == 'function' ? c : b var tag = typeof(c) == 'function' ? b : c actions = typeof(actions) == 'string' ? actions.split(' ') : actions var that = this actions.forEach(function(action){ var _handler = function(){ // remove handler... that.off(action, handler) return handler.apply(this, arguments) } _handler.orig_handler = handler that.on(action, tag, _handler) }) return this }, // Get mixin object in inheritance chain... // // NOTE: if pre is true this will return the chain item before the // mixin, this is useful, for example, to remove mixins, see // .mixout(..) for an example... getMixin: function(from, pre){ var cur = this var proto = this.__proto__ while(proto != null){ // we have a hit... if(proto.hasOwnProperty('__mixin_source') && proto.__mixin_source === from){ return pre ? cur : proto } // go to next item in chain... cur = proto proto = cur.__proto__ } return null }, // Mixin a set of actions into this... // // NOTE: if 'all' is set them mixin all the actions available, // otherwise only mixin local actions... // NOTE: this will override existing own attributes. inlineMmixin: function(from, all, descriptors, all_attr_types){ // defaults... descriptors = descriptors || true all_attr_types = all_attr_types || false if(all){ var keys = [] for(var k in from){ keys.push(k) } } else { var keys = Object.keys(from) } var that = this keys.forEach(function(k){ /* // XXX is this the right way to go??? // check if we are not overwriting anything... if(that.hasOwnProperty(k)){ console.warn('WARNING:', that,'already has attribute', k, '- skipping...') return } */ // properties.... var prop = Object.getOwnPropertyDescriptor(from, k) if(descriptors && prop.get != null){ // NOTE: so as to be able to delete this on mixout... prop.configurable = true Object.defineProperty(that, k, prop) // actions and other attributes... } else { var attr = from[k] if(all_attr_types || attr instanceof Action){ that[k] = attr } } }) return this }, // Same as .inlineMmixin(..) but isolates a mixin in a seporate object // in the inheritance chain... // mixin: function(from, all, descriptors, all_attr_types){ var proto = Object.create(this.__proto__) // mixinto an empty object proto.inlineMmixin(from, all, descriptors, all_attr_types) // mark the mixin for simpler removal... proto.__mixin_source = from this.__proto__ = proto return this }, // Mixin a set of local actions into an object... // // XXX this will not work on non-actions... mixinTo: function(to, all, descriptors, all_attr_types){ return this.mixin.call(to, this, all, descriptors, all_attr_types) }, // Remove mixed in actions from this... // // NOTE: this will only remove local actions, inherited actions will // not be affected... // NOTE: this will not affect event handlers, they should be removed // manually if needed... inlineMixout: function(from, all, descriptors, all_attr_types){ // defaults... descriptors = descriptors || true all_attr_types = all_attr_types || false if(all){ var keys = [] for(var k in from){ keys.push(k) } } else { var keys = Object.keys(from) } var locals = Object.keys(this) var that = this keys.forEach(function(k){ var prop = Object.getOwnPropertyDescriptor(from, k) // descriptor... if(descriptors && prop.get != null){ if(prop.get === Object.getOwnPropertyDescriptor(that, k).get){ delete that[k] } // actions and other attrs... } else { var attr = from[k] if((all_attr_types || attr instanceof Action) // remove only local attrs... && locals.indexOf(k) >= 0){ delete that[k] } } }) return this }, // This is similare in effect but different in mechanics to .inlineMixout(..) // // This will find and remove a mixin object from the inheritance chian. // // NOTE: this will remove only the first occurance of a mixin. mixout: function(from){ var o = this.getMixin(from, true) // pop the mixin off the chain... if(o != null){ o.__proto__ = o.__proto__.__proto__ } return this }, // Remove a set of local mixed in actions from object... // mixoutFrom: function(to, all, descriptors, all_attr_types){ return this.mixout.call(to, this, all, descriptors, all_attr_types) }, // Create a child object... // // NOTE: this will create a .config in the instance that inherits from // this.config... // NOTE: this will not copy/clone any data. // // XXX is this correct??? // XXX should this be an action??? clone: function(full){ var o = Object.create(this) if(this.config){ if(full){ o.config = JSON.parse(JSON.stringify(this.config)) } else { o.config = Object.create(this.config) } } return o }, // This will create a .config in instances... // NOTE: this makes Actions compatible with lib/object.js... __init__: function(){ if(this.__proto__.config && !Object.hasOwnProperty(this, 'config')){ this.config = Object.create(this.__proto__.config) } } } // An action set... // // Actions() // Actions(, ) // -> actions // // This will pre-process an object to setup the action mechanics. // // If the 'this and prototype both contain a .config attribute then this // will make set .config.__proto__ = .config // // // The action format: // { // // full format... // : [ // , // , // // ], // // // short doc only... // : [ // , // // ], // // // only the code... // : [ // // ], // ... // } // // // NOTE: the action function is always last. // NOTE: if is not given, MetaActions will be used as default. // // For more documentation see: Action(..). // // XXX add doc, ldoc, tags and save them to each action... // XXX is .config processing correct here??? // XXX should this be a full fledged object??? var Actions = module.Actions = function Actions(a, b){ var obj = b == null ? a : b var proto = b == null ? MetaActions : a obj = obj || {} // NOTE: this is intentionally done only for own attributes... Object.keys(obj).forEach(function(k){ // NOTE: we are not getting the attrs directly (vars = obj[k]) // as that will trigger the getters on an object that is // not in a consistent state... // NOTE: this will skip all the getters and setters, they will // be included as-is... var arg = Object.getOwnPropertyDescriptor(obj, k).value // skip non-arrays... if(arg == null || arg.constructor !== Array // and arrays the last element of which is not a function... || !(arg[arg.length-1] instanceof Function)){ return } var func = arg.pop() // create a new action... obj[k] = new Action(k, arg[0], arg[1], func) }) if(proto != null){ obj.__proto__ = proto // XXX is this the right way to go??? if(obj.config != null && proto.config != null){ obj.config.__proto__ = proto.config } } return obj } /*********************************************************************/ var test = module.test = function test(){ var TestActions = module.TestActions = Actions({ testActionGen1: ['baisc test action...', 'some extra info', function(){ console.log(' test 1!') return function(){ console.log(' test 2!') } }], testActionGen2: ['baisc 2\'nd gen test action...', // no extra info... function(){ console.log(' test gen 2!') this.testActionGen1() }], }) var TestActions2 = module.TestActions2 = Actions(TestActions, { // NOTE: this looks like an action and feels like an action but // actually this is a callback as an action with this name // already exists... testActionGen1: [ function(){ console.log(' pre callback!') return function(){ console.log(' post callback!') } }], testAction2: ['this is an action', function(){ console.log('testAction2 args:', arguments) }], }) // XXX the main question here is that there is no way to know if a // particular action is going to be a root action or an action // callback because we do not know if the action in the parent // will be available at mix time or not, and the two models // are different... // XXX one way to do this is to make all code a callback and // just use the root as an event trigger... // // ...but this effectively means we are implementing // inheritance ourselves as the traditional name resolution // will no longer be used, and as in the case we implement // MRO why not go the whole way and implement multiple // inheritance in the first place... // // ...let's try and avoid this... /* var TestActionMixin = module.TestActionMixin = ActionMixin({ // XXX }) */ console.log('TestActions.testActionGen1()') TestActions.testActionGen1() console.log('TestActions.testActionGen2()') TestActions.testActionGen2() // both of these should cet a callback... console.log('TestActions2.testActionGen1()') TestActions2.testActionGen1() console.log('TestActions2.testActionGen2()') TestActions2.testActionGen2() // and an event-like handler... TestActions2.on('testActionGen1.post', function(){ console.log(' post handler! (first defined)') }) TestActions2.on('testActionGen1', function(){ console.log(' post handler! (last defined)') }) console.log('TestActions2.testActionGen1()') TestActions2.testActionGen1() TestActions2.on('testActionGen2.pre', function(){ console.log(' pre handler! (first defined)') }) TestActions2.on('testActionGen2.pre', function(){ console.log(' pre handler! (last defined)') }) console.log('TestActions2.testActionGen2()') TestActions2.testActionGen2() } /********************************************************************** * vim:set ts=4 sw=4 : */ return module })