types.js/event.js
Alex A. Naanou 4921d82eb4 minor bugfix...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2022-10-28 14:15:28 +03:00

400 lines
11 KiB
JavaScript

/**********************************************************************
*
*
*
* XXX need ability to extend event to implement proxy events...
* ...i.e. .on(..) / ... get called on one object but the handler
* bound on a different object via a proxy event method...
* XXX is types/events the right place for this???
* XXX should we have .pre/.post events???
* XXX should we propogate event handling to parent/overloaded events???
*
**********************************************/ /* c8 ignore next 2 */
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var object = require('ig-object')
/*********************************************************************/
// Event method wrappers...
var EventCommand =
module.EventCommand =
object.Constructor('EventCommand', {
name: null,
__init__: function(name, data={}){
Object.assign(this, data, {name}) },
})
module.TRIGGER = module.EventCommand('TRIGGER')
// Create an "eventful" method...
//
// The resulting method can be either called directly or via .trigger(..).
// Handlrs can be bound to it via .on(..) and unbound via .off(..) and
// calling it will trigger the handlers either after the user func(..)
// return or when the user calles the passed handler(..) function.
//
// Eventful(name[, options])
// -> method
//
// Eventful(name, func[, options])
// -> method
//
//
// Trigger the event...
// method(...args)
// -> ..
// NOTE: if func(..) returns undefined this will return undefined
// when called without a context and the call context otherwise
//
//
// func(handle, ...args)
// -> ..
//
//
// trigger event handlers...
// handle()
// handle(true)
// -> true
// -> false
//
// trigger event handlers and overload handler arguments...
// handle(true, ...)
// -> true
// -> false
//
// prevent event handlers from triggering...
// handle(false)
// -> undefined
//
//
//
// Special case: EventCommand...
//
// EventCommand instance can be passed as the first argument of method,
// in this case the event function will get it but the event handlers
// will not...
// This is done to be able to externally pass commands to event methods
// that get handled in a special way by the function but not passed to
// the event handlers...
//
// method(<event-command>, ...args)
// -> ..
//
// func(handle, <event-command>, ...args)
// -> ..
//
//
//
// NOTE: calling handle(false) will exiplicitly disable calling the
// handlers for that call...
var Eventful =
module.Eventful =
object.Constructor('Eventful', {
handlerLocation: 'context',
// Sets the default return mode for .__call__(..) if either .func(..)
// is not set or returns undefined...
//
// Can be:
// 'context'
// undefined
defaultReturn: 'context',
name: null,
func: null,
toString: function(){
return this.func ?
`${this.constructor.name} `
+(this.func.toString()
.replace(/^(function[^(]*\()[^,)]*, ?/, '$1'))
: `${this.constructor.name} function ${this.name}(){}` },
__event_handlers__: null,
bind: function(context, handler){
var handlers =
// local...
this.handlerLocation == 'method' ?
(this.__event_handlers__ = this.__event_handlers__ || [])
// context (default)...
//: (context.__event_handlers__ == null ?
: !context.hasOwnProperty('__event_handlers__') ?
Object.defineProperty(context, '__event_handlers__', {
value: {[this.name]: (handlers = [])},
enumerable: false,
configurable: true,
writable: true,
})
&& handlers
: (context.__event_handlers__[this.name] =
context.__event_handlers__[this.name] || [])
// add handler...
handlers.push(handler)
return this },
unbind: function(context, handler){
var handlers =
this.handlerLocation == 'method' ?
method.__event_handlers__
//: (context.__event_handlers__ || {})[this.name]) || []
: context.hasOwnProperty('__event_handlers__') ?
(context.__event_handlers__ || {})[this.name] || []
: []
handlers.splice(0, handlers.length,
...handlers.filter(function(h){
return h !== handler
&& h.__event_original_handler !== handler }))
return this },
__call__: function(context, ...args){
var that = this
var handlers =
this.handlerLocation == 'method' ?
(this.__event_handlers__ || [])
: []
// context (default)...
// NOTE: these are allways called...
handlers = handlers
//.concat((context.__event_handlers__ || {})[this.name] || [])
.concat(context.hasOwnProperty('__event_handlers__') ?
(context.__event_handlers__ || {})[this.name] || []
: [])
// NOTE: this will stop event handling if one of the handlers
// explicitly returns false...
// NOTE: if the user does not call handle() it will be called
// after the event action is done but before it returns...
// NOTE: to explicitly disable calling the handlers func must
// call handle(false)
var did_handle = false
var handle = function(run=true, ...alt_args){
did_handle = true
var a = (run === true
&& arguments.length > 1) ?
alt_args
: args
a = a[0] instanceof EventCommand ?
a.slice(1)
: a
return run ?
handlers
.reduce(function(res, handler){
return res === true
&& handler.call(context, that.name, ...a) !== false }, true)
: undefined }
// call...
var res = this.func ?
this.func.call(context, handle, ...args)
: undefined
// call the handlers if the user either didn't call handle()
// or explicitly called handle(false)...
!did_handle
&& handle()
return res
?? (this.defaultReturn == 'context' ?
context
: res ) },
__init__: function(name, func, options={}){
options = func && typeof(func) != 'function' ?
func
: options
Object.assign(this, options)
Object.defineProperty(this, 'name', { value: name })
func
&& typeof(func) == 'function'
&& Object.defineProperty(this, 'func', {
value: func,
enumerable: false,
}) },
})
// Extends Eventful(..) adding ability to bind events via the
// resulting method directly by passing it a function...
//
// Event(name[, options])
// -> method
//
// Event(name, func[, options])
// -> method
//
//
// Bind handler...
// method(handler)
// -> this
//
// Unbind handler...
// method(handler, false)
// -> this
//
// Trigger handlers...
// method(...args)
// -> this
//
//
// func(handle, ...args)
//
//
// Special case:
//
// Force trigger event...
// method(TRIGGER, ...args)
// -> this
//
// This will pass args to the event action regardless whether the first
// arg is a function or not...
var Event =
module.Event =
object.Constructor('Event', Eventful, {
toString: function(){
return this.orig_func ?
'Event '
+this.orig_func.toString()
.replace(/^(function[^(]*\()[^,)]*, ?/, '$1')
: `Event function ${this.name}(){}`},
__call__: function(context, ...args){
// add handler...
// NOTE: when the first arg is an event command this will
// fall through to calling the action...
if(typeof(args[0]) == 'function'){
this.bind(context, args[0])
return context
// call the action...
} else {
return object.parentCall(Event.prototype.__call__, this, context, ...args) } },
})
// Like Event(..) but produces an event method that can only be triggered
// via .trigger(name, ...), calling this is a no-op...
var PureEvent =
module.PureEvent =
object.Constructor('PureEvent', Event, {
toString: function(){
return `PureEvent ${this.name}(){}`},
__init__: function(name, func, options={}){
if(typeof(func) != 'function'){
options = func
func = undefined }
object.parentCall(PureEvent.prototype.__init__, this,
name,
function(handle, trigger, ...args){
trigger === module.TRIGGER ?
func && func.call(this, handle, ...args)
: handle(false) }, options) },
})
//---------------------------------------------------------------------
// Mixins...
// XXX might be nice to add support to pre/post handlers...
// XXX still not sure about the builtin-local event control flow...
// XXX do we need to be able to force global handler???
var EventHandlerMixin =
module.EventHandlerMixin = object.Mixin('EventHandlerMixin', {
//__event_handlers__: null,
on: function(evt, func){
// event...
if(evt in this
&& this[evt].bind){
this[evt].bind(this, func)
// non-event...
} else {
//this.__event_handlers__ == null
!this.hasOwnProperty('__event_handlers__')
&& Object.defineProperty(this, '__event_handlers__', {
value: {},
enumerable: false,
configurable: true,
writable: true,
})
;(this.__event_handlers__[evt] =
this.__event_handlers__[evt] || [])
.push(func) }
return this },
one: function(evt, func){
var handler
this.on(evt,
handler = Object.assign(
function(handle, ...args){
this.off(evt, handler)
return func.call(this, handle, ...args) }.bind(this),
{__event_original_handler: func}))
return this },
// XXX do we need .off(evt, 'all')
off: function(evt, func){
// event...
if(evt in this
&& this[evt].unbind){
this[evt].unbind(this, func)
// non-event...
} else {
var handlers = this.__event_handlers__
&& (this.__event_handlers__[evt] || [])
handlers
&& handlers.splice(0, handlers.length,
...handlers.filter(function(h){
return h !== func
&& h.__event_original_handler !== func })) }
return this },
// XXX revise...
trigger: function(evt, ...args){
evt in this ?
// XXX add a better check...
this[evt](module.TRIGGER, ...args)
//: this.__event_handlers__
: this.hasOwnProperty('__event_handlers__')
&& (this.__event_handlers__[evt] || [])
.forEach(function(h){ h(evt, ...args) })
return this },
})
// NOTE: this can't be added via Object.assign(..), use object.mixinFlat(..)
// instead...
var EventDocMixin =
module.EventDocMixin = object.Mixin('EventDocMixin', {
get eventful(){
return object.deepKeys(this)
.filter(function(n){
// avoid triggering props...
return !object.values(this, n, true).next().value.get
// XXX this is too strict...
&& (this[n] || {}).constructor === Eventful}.bind(this)) },
get events(){
return object.deepKeys(this)
.filter(function(n){
// avoid triggering props...
return !object.values(this, n, true).next().value.get
// XXX this is too strict...
&& (this[n] || {}).constructor === Event }.bind(this)) },
})
var EventMixin =
module.EventMixin =
object.Mixin('EventMixin',
EventHandlerMixin,
EventDocMixin)
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })