mirror of
				https://github.com/flynx/ImageGrid.git
				synced 2025-11-04 13:20:10 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			3604 lines
		
	
	
		
			98 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			3604 lines
		
	
	
		
			98 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
/**********************************************************************
 | 
						|
* 
 | 
						|
* Core features...
 | 
						|
* 	Setup the life-cycle and the base interfaces for features to use...
 | 
						|
*
 | 
						|
* Defined here:
 | 
						|
* 	- protocol action constructor
 | 
						|
* 	- config value toggler constructor
 | 
						|
* 	- meta actions
 | 
						|
* 		- .isToggler(..) predicate
 | 
						|
* 		- .preActionHandler(..) used to toggler special args
 | 
						|
* 	- ImageGrid root object/constructor
 | 
						|
* 	- ImageGridFeatures object
 | 
						|
*
 | 
						|
*
 | 
						|
* Features:
 | 
						|
* 	- util
 | 
						|
* 	- introspection
 | 
						|
* 	- logger
 | 
						|
* 	- lifecycle
 | 
						|
* 		base life-cycle events (start/stop/..)
 | 
						|
* 		base abort api
 | 
						|
*	- serialization
 | 
						|
*		base methods to handle loading, serialization and cloning...
 | 
						|
*	- cache
 | 
						|
*		basic action/prop caching api...
 | 
						|
*	- timers
 | 
						|
*		wrapper around setInterval(..), setTimeout(..) and friends, 
 | 
						|
*		provides persistent timer triggers and introspection...
 | 
						|
* 	- journal
 | 
						|
* 		action journaling and undo/redo functionality
 | 
						|
* 		XXX needs revision...
 | 
						|
* 	- changes
 | 
						|
* 		change tracking
 | 
						|
* 	- workspace
 | 
						|
* 		XXX needs revision...
 | 
						|
* 	- tasks
 | 
						|
* 		tasks -- manage long running actions
 | 
						|
* 		queue -- manage lots of small actions as a single task
 | 
						|
* 		contexts -- XXX
 | 
						|
* 	- self-test
 | 
						|
* 		basic framework for running test actions at startup...
 | 
						|
*
 | 
						|
*
 | 
						|
* XXX some actions use the .clone(..) action/protocol, should this be 
 | 
						|
* 	defined here???
 | 
						|
*
 | 
						|
**********************************************************************/
 | 
						|
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
 | 
						|
(function(require){ var module={} // make module AMD/node compatible...
 | 
						|
/*********************************************************************/
 | 
						|
 | 
						|
// XXX
 | 
						|
var DEBUG = typeof(DEBUG) != 'undefined' ? DEBUG : true
 | 
						|
 | 
						|
var types = require('lib/types')
 | 
						|
var runner = require('lib/types/runner')
 | 
						|
var util = require('lib/util')
 | 
						|
var object = require('lib/object')
 | 
						|
var actions = require('lib/actions')
 | 
						|
var features = require('lib/features')
 | 
						|
var toggler = require('lib/toggler')
 | 
						|
 | 
						|
 | 
						|
// code/text normalization...
 | 
						|
var doc = module.doc = actions.doc
 | 
						|
var text = module.text = object.text
 | 
						|
 | 
						|
 | 
						|
 | 
						|
/*********************************************************************/
 | 
						|
// Root ImageGrid.viewer object constructor...
 | 
						|
//
 | 
						|
// This adds:
 | 
						|
// 	- toggler as action compatibility
 | 
						|
//
 | 
						|
var ImageGridMetaActions =
 | 
						|
module.ImageGridMetaActions = {
 | 
						|
	__proto__: actions.MetaActions,
 | 
						|
 | 
						|
	// Test if the action is a Toggler...
 | 
						|
	//
 | 
						|
	isToggler: 
 | 
						|
		actions.doWithRootAction(function(action){
 | 
						|
			return action instanceof toggler.Toggler }),
 | 
						|
 | 
						|
	// Handle special cases where we need to get the action result early,
 | 
						|
	// without calling handlers...
 | 
						|
	//
 | 
						|
	// These include:
 | 
						|
	// 	- toggler action special command handling (like: '?', '??', ..)
 | 
						|
	//
 | 
						|
	preActionHandler: actions.doWithRootAction(function(action, name, handlers, args){
 | 
						|
		// Special case: do not call handlers for toggler state queries...
 | 
						|
		//
 | 
						|
		// 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: 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
 | 
						|
		if(this.isToggler(name)
 | 
						|
				&& args.length == 1 
 | 
						|
				&& (args[0] == '?' || args[0] == '??')){
 | 
						|
			return {
 | 
						|
				result: handlers.slice(-1)[0].pre.apply(this, args),
 | 
						|
			} } }),
 | 
						|
} 
 | 
						|
 | 
						|
var ImageGrid = 
 | 
						|
module.ImageGrid = 
 | 
						|
	object.Constructor('ImageGrid', ImageGridMetaActions)
 | 
						|
 | 
						|
 | 
						|
// Root ImageGrid feature set....
 | 
						|
var ImageGridFeatures =
 | 
						|
module.ImageGridFeatures = 
 | 
						|
	new features.FeatureSet()
 | 
						|
 | 
						|
// setup base instance constructor...
 | 
						|
ImageGridFeatures.__actions__ = 
 | 
						|
	function(){ 
 | 
						|
		return actions.Actions(ImageGrid()) }
 | 
						|
 | 
						|
 | 
						|
 | 
						|
//---------------------------------------------------------------------
 | 
						|
// Setup runtime info...
 | 
						|
//
 | 
						|
// XXX add PWA / chrome-app...
 | 
						|
// XXX add cordova...
 | 
						|
// XXX add mobile...
 | 
						|
// XXX add widget...
 | 
						|
 | 
						|
// XXX should this contain feature versions???
 | 
						|
var runtime = ImageGridFeatures.runtime = {}
 | 
						|
 | 
						|
// nw or node...
 | 
						|
if(typeof(process) != 'undefined'){
 | 
						|
	// node...
 | 
						|
	runtime.node = true
 | 
						|
 | 
						|
	// Electron...
 | 
						|
	if(process.versions['electron'] != null 
 | 
						|
			// node mode...
 | 
						|
			&& typeof(document) != 'undefined'){
 | 
						|
		runtime.electron = true
 | 
						|
		runtime.desktop = true
 | 
						|
 | 
						|
	// nw.js 0.13+
 | 
						|
	} else if(typeof(nw) != 'undefined'){
 | 
						|
		runtime.nw = true
 | 
						|
		runtime.desktop = true
 | 
						|
 | 
						|
		// NOTE: jli is patching the Date object and with two separate 
 | 
						|
		// 		instances we'll need to sync things up...
 | 
						|
		// XXX HACK: if node and chrome Date implementations ever 
 | 
						|
		// 		significantly diverge this will break things + this is 
 | 
						|
		// 		a potential data leak between contexts...
 | 
						|
		//global.Date = window.Date
 | 
						|
 | 
						|
		// XXX this is less of a hack but it is still an implicit
 | 
						|
		util.patchDate(global.Date)
 | 
						|
		util.patchDate(window.Date)
 | 
						|
 | 
						|
	// node...
 | 
						|
	} else {
 | 
						|
		// XXX patch Date...
 | 
						|
		// XXX this will not work directly as we will need to explicitly
 | 
						|
		// 		require jli...
 | 
						|
		//patchDate(global.Date)
 | 
						|
	} }
 | 
						|
 | 
						|
// browser...
 | 
						|
// NOTE: we're avoiding detecting browser specifics for as long as possible,
 | 
						|
// 		this will minimize the headaches of supporting several non-standard
 | 
						|
// 		versions of code...
 | 
						|
if(typeof(window) != 'undefined'){
 | 
						|
	runtime.browser = true }
 | 
						|
 | 
						|
 | 
						|
 | 
						|
/*********************************************************************/
 | 
						|
// Util...
 | 
						|
 | 
						|
// Toggle value in .config...
 | 
						|
//
 | 
						|
// NOTE: if no toggler state is set this assumes that the first state 
 | 
						|
// 		is the default...
 | 
						|
// NOTE: default states is [false, true]
 | 
						|
var makeConfigToggler = 
 | 
						|
module.makeConfigToggler = 
 | 
						|
function(attr, states, a, b){
 | 
						|
	states = states || [false, true]
 | 
						|
	var pre = a
 | 
						|
	// XXX is this a good default???
 | 
						|
	//var post = b || function(action){ action != null && this.focusImage() }
 | 
						|
	var post = b
 | 
						|
 | 
						|
	return toggler.Toggler(null,
 | 
						|
		function(_, action){
 | 
						|
			var lst = states instanceof Array ? states 
 | 
						|
				: states instanceof Function ? states.call(this)
 | 
						|
				: states
 | 
						|
 | 
						|
			// get attr path...
 | 
						|
			var a = attr.split(/\./g)
 | 
						|
			var cfg = a.slice(0, -1) 
 | 
						|
				.reduce(function(res, cur){
 | 
						|
					return res[cur] }, this.config)
 | 
						|
 | 
						|
			if(action == null){
 | 
						|
				var val = cfg[a.pop()]
 | 
						|
				return val == null ? 
 | 
						|
					(lst[lst.indexOf('none')] || lst[0])
 | 
						|
					: val 
 | 
						|
 | 
						|
			} else {
 | 
						|
				cfg[a[a.length-1]] = action
 | 
						|
				this.config[a[0]] = this.config[a[0]] } },
 | 
						|
		states, pre, post) }
 | 
						|
 | 
						|
 | 
						|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 | 
						|
var UtilActions = actions.Actions({
 | 
						|
	mergeConfig: ['- System/', 
 | 
						|
		doc`Merge a config object into .config
 | 
						|
		`,
 | 
						|
		function(config){
 | 
						|
			config = config instanceof Function ? config.call(this)
 | 
						|
				: typeof(config) == typeof('str') ? this.config[config]
 | 
						|
				: config
 | 
						|
			Object.assign(this.config, config) }],
 | 
						|
})
 | 
						|
 | 
						|
var Util = 
 | 
						|
module.Util = ImageGridFeatures.Feature({
 | 
						|
	title: '',
 | 
						|
	doc: '',
 | 
						|
 | 
						|
	tag: 'util',
 | 
						|
 | 
						|
	actions: UtilActions,
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
 | 
						|
//---------------------------------------------------------------------
 | 
						|
// Introspection...
 | 
						|
 | 
						|
// Indicate that an action is not intended for direct use...
 | 
						|
//
 | 
						|
// NOTE: this will not do anything but mark the action.
 | 
						|
var notUserCallable =
 | 
						|
module.notUserCallable = 
 | 
						|
function(func){
 | 
						|
	func.__not_user_callable__ = true
 | 
						|
	return func }
 | 
						|
 | 
						|
// NOTE: this is the same as event but user-callable...
 | 
						|
var UserEvent =
 | 
						|
module.UserEvent = 
 | 
						|
function(func){
 | 
						|
	func.__event__ = true
 | 
						|
	return func }
 | 
						|
 | 
						|
// XXX rename???
 | 
						|
var Event =
 | 
						|
module.Event = 
 | 
						|
function(func){
 | 
						|
	func.__event__ = true
 | 
						|
	return notUserCallable(func) }
 | 
						|
 | 
						|
 | 
						|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 | 
						|
 | 
						|
var IntrospectionActions = actions.Actions({
 | 
						|
	get useractions(){
 | 
						|
		return this.cache('useractions', function(d){
 | 
						|
			return d instanceof Array ? 
 | 
						|
				d.slice() 
 | 
						|
				: this.actions.filter(this.isUserCallable.bind(this)) }) },
 | 
						|
	get events(){
 | 
						|
		return this.cache('events', function(d){
 | 
						|
			return d instanceof Array ? 
 | 
						|
				d.slice() 
 | 
						|
				: this.actions.filter(this.isEvent.bind(this)) }) },
 | 
						|
 | 
						|
	isUserCallable:
 | 
						|
		// XXX should this check only the root action or the whole set???
 | 
						|
		// 		...in other words: can we make an action non-user-callable
 | 
						|
		// 		anywhere other than the root action?
 | 
						|
		// 		IMO no...
 | 
						|
		//function(action){ 
 | 
						|
		//	return this.getActionAttr(action, '__not_user_callable__') != true }],
 | 
						|
		actions.doWithRootAction(function(action){
 | 
						|
			return action.__not_user_callable__ != true }),
 | 
						|
	isEvent: 
 | 
						|
		actions.doWithRootAction(function(action){
 | 
						|
			return !!action.__event__ }),
 | 
						|
 | 
						|
	// XXX revise... 
 | 
						|
	getActionMode: ['- Interface/',
 | 
						|
		doc`Get action browse mode...
 | 
						|
 | 
						|
		Get and action's .mode(..) method and return its result.
 | 
						|
 | 
						|
		Action .mode can be:
 | 
						|
			<function>			- action method.
 | 
						|
			<action-name>		- alias, name of action to get the
 | 
						|
									method from.
 | 
						|
 | 
						|
		The action .mode(..) method is called in the context of actions.
 | 
						|
 | 
						|
		Basic example:
 | 
						|
			someAction: ['Path/To/Some action',
 | 
						|
				{mode: function(){ ... }},
 | 
						|
				function(){
 | 
						|
					...
 | 
						|
				}],
 | 
						|
			someOtherAction: ['Path/To/Some action',
 | 
						|
				// alias
 | 
						|
				{mode: 'someAction'},
 | 
						|
				function(){
 | 
						|
					...
 | 
						|
				}],
 | 
						|
 | 
						|
 | 
						|
		Usage pattern:
 | 
						|
			// for cases where we need to define an explicit mode...
 | 
						|
			actionModeX: ['- System/',
 | 
						|
				{mode: function(){
 | 
						|
					return this.actionModeX() }},
 | 
						|
				core.notUserCallable(function(){
 | 
						|
					return ...
 | 
						|
				})],
 | 
						|
			someAction: [
 | 
						|
				// use the mode...
 | 
						|
				{mode: 'actionModeX'},
 | 
						|
				function(){
 | 
						|
					...
 | 
						|
				}],
 | 
						|
		`,
 | 
						|
		function(action, mode_cache){
 | 
						|
			var m = action
 | 
						|
			var visited = [m]
 | 
						|
			var last
 | 
						|
 | 
						|
			// check cache...
 | 
						|
			if(m in (mode_cache || {})){
 | 
						|
				return mode_cache[m] }
 | 
						|
 | 
						|
			// handle aliases...
 | 
						|
			do {
 | 
						|
				last = m
 | 
						|
				m = this.getActionAttr(m, 'mode')
 | 
						|
 | 
						|
				// check cache...
 | 
						|
				if(m in (mode_cache || {})){
 | 
						|
					return mode_cache[m] }
 | 
						|
 | 
						|
				// check for loops...
 | 
						|
				if(m && visited[m] != null){
 | 
						|
					m = null
 | 
						|
					break }
 | 
						|
				visited.push(m)
 | 
						|
			} while(typeof(m) == typeof('str'))
 | 
						|
 | 
						|
			//return m ? m.call(this) : undefined
 | 
						|
			return m ? 
 | 
						|
				// no cache...
 | 
						|
				(mode_cache == null ?
 | 
						|
						m.call(this)
 | 
						|
					// cache hit...
 | 
						|
					: last in mode_cache ? 
 | 
						|
						mode_cache[last] 
 | 
						|
					// call check and populate cache...
 | 
						|
					: (mode_cache[action] = 
 | 
						|
						mode_cache[last] = 
 | 
						|
							m.call(this)))
 | 
						|
				: actions.UNDEFINED }],
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
var Introspection = 
 | 
						|
module.Introspection = ImageGridFeatures.Feature({
 | 
						|
	title: '',
 | 
						|
 | 
						|
	tag: 'introspection',
 | 
						|
	depends: [
 | 
						|
		'cache'
 | 
						|
	],
 | 
						|
 | 
						|
	actions: IntrospectionActions,
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
 | 
						|
//---------------------------------------------------------------------
 | 
						|
// Logger...
 | 
						|
 | 
						|
var LoggerActions = actions.Actions({
 | 
						|
	config: {
 | 
						|
		// NOTE: if set to 0 no log limit is applied...
 | 
						|
		'log-size': 10000,
 | 
						|
	},
 | 
						|
 | 
						|
	Logger: object.Constructor('BaseLogger', {
 | 
						|
		doc: `Logger object constructor...`,
 | 
						|
 | 
						|
		root: null,
 | 
						|
		parent: null,
 | 
						|
 | 
						|
		// Quiet mode...
 | 
						|
		//
 | 
						|
		// NOTE: if local mode is not defined this will get the mode of 
 | 
						|
		// 		the nearest parent...
 | 
						|
		// XXX need these to be persistent...
 | 
						|
		// XXX add support for log levels...
 | 
						|
		__quiet: null,
 | 
						|
		get quiet(){
 | 
						|
			var cur = this
 | 
						|
			while(cur.__quiet == null && cur.parent){
 | 
						|
				cur = cur.parent }
 | 
						|
			return !!cur.__quiet },
 | 
						|
		set quiet(value){
 | 
						|
			value == null ?
 | 
						|
				(delete this.__quiet)
 | 
						|
				: (this.__quiet = !!value) },
 | 
						|
 | 
						|
		__context: null,
 | 
						|
		get context(){
 | 
						|
			return this.__context || this.root.__context },
 | 
						|
 | 
						|
		get isRoot(){
 | 
						|
			return this === this.root },
 | 
						|
 | 
						|
		__path: null,
 | 
						|
		get path(){
 | 
						|
			return (this.__path = 
 | 
						|
				this.__path == null ? 
 | 
						|
					[] 
 | 
						|
					: this.__path) },
 | 
						|
		set path(value){
 | 
						|
			this.__path = value },
 | 
						|
 | 
						|
		// NOTE: if set to 0 no log limit is applied...
 | 
						|
		// NOTE: writing to this will modify .context.config['log-size']
 | 
						|
		// 		if a available and .__max_size otherwise...
 | 
						|
		__max_size: null,
 | 
						|
		get max_size(){
 | 
						|
			return this.__max_size != null ?
 | 
						|
				this.__max_size
 | 
						|
				// this.context.config['log-size']
 | 
						|
				: ((this.context || {}).config || {})['log-size'] || 10000 },
 | 
						|
		set max_size(value){
 | 
						|
			return this.context ?
 | 
						|
				(this.context.config['log-size'] = value)
 | 
						|
				: this.__max_size = value },
 | 
						|
		// NOTE: to disable log retention in .log set this to false...
 | 
						|
		__log: null,
 | 
						|
		get log(){
 | 
						|
			return this.__log === false ?
 | 
						|
					false
 | 
						|
				: this.__log ?
 | 
						|
					this.__log
 | 
						|
				: this.isRoot ?
 | 
						|
					(this.__log = this.__log || []) 
 | 
						|
				: this.root.log },
 | 
						|
 | 
						|
 | 
						|
		// log management...
 | 
						|
		clear: function(){
 | 
						|
			this.log 
 | 
						|
				&& this.log.splice(0, this.log.length) 
 | 
						|
			return this },
 | 
						|
		// Format log to string...
 | 
						|
		//
 | 
						|
		// 	Full log...
 | 
						|
		// 	.log2str()
 | 
						|
		// 		-> str
 | 
						|
		//
 | 
						|
		// 	Slice log...
 | 
						|
		// 	.log2str(from)
 | 
						|
		// 	.log2str(from, to)
 | 
						|
		// 		-> str
 | 
						|
		//
 | 
						|
		// 	Specific item...
 | 
						|
		// 	.log2str(date, path, status, rest)
 | 
						|
		// 	.log2str([date, path, status, rest])
 | 
						|
		// 		-> str
 | 
						|
		// 		NOTE: this form does not depend on context...
 | 
						|
		//
 | 
						|
		//
 | 
						|
		// NOTE: the later form is useful for filtering:
 | 
						|
		// 		logger.log
 | 
						|
		// 			.filter(..)
 | 
						|
		// 			.map(logger.log2str)
 | 
						|
		// 			.join('\n')
 | 
						|
		//
 | 
						|
		log2str: function(){
 | 
						|
			return (arguments.length == 0 ?
 | 
						|
					   (this.log || [])
 | 
						|
					: arguments[0] instanceof Array ?
 | 
						|
						[arguments[0]]
 | 
						|
					: arguments.length < 2 ?
 | 
						|
						(this.log ?
 | 
						|
							this.log.slice(arguments[0], arguments[1])
 | 
						|
							: [])
 | 
						|
					: [arguments])
 | 
						|
				.map(function([date, path, status, rest]){
 | 
						|
					return `[${ new Date(date).getTimeStamp(true) }] ` 
 | 
						|
						+ path.join(': ') + (path.length > 0 ? ': ' : '')
 | 
						|
						+ status 
 | 
						|
						+ (rest.length > 1 ? 
 | 
						|
								':\n\t'
 | 
						|
							: rest.length == 1 ?
 | 
						|
								': '
 | 
						|
							: '')
 | 
						|
						+ rest.join(': ') })
 | 
						|
				.join('\n') },
 | 
						|
		print: function(...args){
 | 
						|
			console.log(this.log2str(...args))
 | 
						|
			return this },
 | 
						|
 | 
						|
 | 
						|
		// main API...
 | 
						|
		//
 | 
						|
		// 	.push(str, ...)
 | 
						|
		//
 | 
						|
		// 	.push(str, ..., attrs)
 | 
						|
		//
 | 
						|
		push: function(...msg){
 | 
						|
			// settings...
 | 
						|
			var attrs = typeof(msg.last()) != typeof('str') ?
 | 
						|
				msg.pop()
 | 
						|
				: {}
 | 
						|
			return msg.length == 0 ?
 | 
						|
				this
 | 
						|
				: Object.assign(
 | 
						|
					this.constructor(),
 | 
						|
					attrs,
 | 
						|
					{
 | 
						|
						root: this.root,
 | 
						|
						parent: this,
 | 
						|
						path: this.path.concat(msg),
 | 
						|
					}) },
 | 
						|
		pop: function(){
 | 
						|
			return (this.root === this || this.path.length == 1) ?
 | 
						|
				this
 | 
						|
				: Object.assign(
 | 
						|
					this.constructor(),
 | 
						|
					{
 | 
						|
						root: this.root,
 | 
						|
						path: this.path.slice(0, -1),
 | 
						|
					}) },
 | 
						|
 | 
						|
		emit: function(status, ...rest){
 | 
						|
			// write to log...
 | 
						|
			this.log !== false
 | 
						|
				&& this.log.push([
 | 
						|
					Date.now(), 
 | 
						|
					this.path, 
 | 
						|
					status, 
 | 
						|
					rest])
 | 
						|
			// maintain log size...
 | 
						|
			this.log !== false
 | 
						|
				&& (this.max_size > 0 
 | 
						|
					&& this.log.length > this.max_size 
 | 
						|
					&& this.log.splice(0, this.log.length - this.max_size))
 | 
						|
			// call context log handler...
 | 
						|
			this.context
 | 
						|
				&& this.context.handleLogItem
 | 
						|
				&& this.context.handleLogItem(this, this.path, status, ...rest)
 | 
						|
			return this },
 | 
						|
 | 
						|
 | 
						|
		__call__: function(_, status, ...rest){
 | 
						|
			return this.emit(status, ...rest) },
 | 
						|
		__init__: function(context){
 | 
						|
			this.__context = context
 | 
						|
			this.root = this 
 | 
						|
		},
 | 
						|
	}),
 | 
						|
 | 
						|
	__logger: null,
 | 
						|
	get logger(){
 | 
						|
		return (this.__logger = 
 | 
						|
			this.__logger 
 | 
						|
				|| this.Logger(this)) },
 | 
						|
	set logger(value){
 | 
						|
		this.__logger = value },
 | 
						|
 | 
						|
	// XXX move this to console-logger???
 | 
						|
	// XXX should this be an action???
 | 
						|
	handleLogItem: ['- System/',
 | 
						|
		function(logger, path, status, ...rest){
 | 
						|
			logger.quiet 
 | 
						|
				|| logger.root.quiet
 | 
						|
				|| console.log(
 | 
						|
					path.join(': ') + (path.length > 0 ? ': ' : '')
 | 
						|
						+ status 
 | 
						|
						+ (rest.length > 1 ? 
 | 
						|
								':\n\t'
 | 
						|
							: rest.length == 1 ?
 | 
						|
								': '
 | 
						|
							: ''), ...rest) }],
 | 
						|
})
 | 
						|
 | 
						|
var Logger = 
 | 
						|
module.Logger = ImageGridFeatures.Feature({
 | 
						|
	title: '',
 | 
						|
 | 
						|
	tag: 'logger',
 | 
						|
	depends: [],
 | 
						|
 | 
						|
	actions: LoggerActions,
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
 | 
						|
//---------------------------------------------------------------------
 | 
						|
// System life-cycle...
 | 
						|
 | 
						|
// XXX potential pitfall: setting up new features without restarting...
 | 
						|
// 		this can happen for instance in ig.js when starting a minimal
 | 
						|
// 		imagegrid instance and then adding new features -- these new 
 | 
						|
// 		features will not get their .start() (and friends) run...
 | 
						|
// 		There are three stages here:
 | 
						|
// 			- feature setup
 | 
						|
// 				things the feature needs to run -- <feature>.setup(..)
 | 
						|
// 			- app start
 | 
						|
// 				things the app wants to do on start
 | 
						|
// 			- ???
 | 
						|
// 				things feature action needs to run in cli should be 
 | 
						|
// 				documented and not depend on .start(..)
 | 
						|
// 				...or there should be a way to "start" the new features...
 | 
						|
// 		XXX put this in the docs...
 | 
						|
// XXX should his have state???
 | 
						|
// 		...if so, should this be a toggler???
 | 
						|
var LifeCycleActions = actions.Actions({
 | 
						|
	config: {
 | 
						|
		// if set indicates the timeput after which the application quits
 | 
						|
		// wating for .declareReady() and forcefully triggers .ready()
 | 
						|
		'declare-ready-timeout': 15000,
 | 
						|
	},
 | 
						|
	
 | 
						|
	__stop_handler: null,
 | 
						|
	__ready: null,
 | 
						|
	__ready_announce_requested: null,
 | 
						|
	__ready_announce_requests: null,
 | 
						|
 | 
						|
	// introspection...
 | 
						|
	isStarted: function(){ 
 | 
						|
		return !!this.__stop_handler },
 | 
						|
	isStopped: function(){ 
 | 
						|
		return !this.__stop_handler },
 | 
						|
	isReady: function(){ 
 | 
						|
		return !!this.__ready },
 | 
						|
	// XXX is this the right name for this???
 | 
						|
	get runtimeState(){
 | 
						|
		return this.isStopped() ? 
 | 
						|
				'stopped'
 | 
						|
			: this.isReady() ? 
 | 
						|
				'ready'
 | 
						|
			: this.isStarted() ? 
 | 
						|
				'started'
 | 
						|
			: undefined },
 | 
						|
 | 
						|
	// XXX not implemented...
 | 
						|
	// 		...this should be triggered on first run and after updates...
 | 
						|
	setup: ['- System/',
 | 
						|
		doc``,
 | 
						|
		Event(function(mode){
 | 
						|
			// System started event...
 | 
						|
			//
 | 
						|
			// Not intended for direct use.
 | 
						|
		})],
 | 
						|
 | 
						|
	start: ['- System/', 
 | 
						|
		doc`Start core action/event
 | 
						|
 | 
						|
			.start()
 | 
						|
 | 
						|
		This action triggers system start, sets up basic runtime, prepares
 | 
						|
		for shutdown (stop) and handles the .ready() event.
 | 
						|
 | 
						|
		Attributes set here:
 | 
						|
			.runtime		- indicates the runtime ImageGrid is running.
 | 
						|
								this currently supports:
 | 
						|
									node, browser, nw, unknown
 | 
						|
 | 
						|
		This will trigger .declareReady() if no action called 
 | 
						|
		.requestReadyAnnounce()
 | 
						|
 | 
						|
		This will trigger .started() event when done.
 | 
						|
 | 
						|
 | 
						|
		NOTE: .runtime attribute will not be available on the .pre handler
 | 
						|
			phase.
 | 
						|
		NOTE: .requestReadyAnnounce() should be called exclusively on the
 | 
						|
			.pre handler phase as this will check and trigger the .ready()
 | 
						|
			event before the .post phase starts.
 | 
						|
		NOTE: handlers bound to this action/event will get called on the 
 | 
						|
			start *event* thus handlers bound when the system is already 
 | 
						|
			started will not get called until next start, to bind a handler 
 | 
						|
			to the started *state* bind to 'started' / .started()
 | 
						|
		`,
 | 
						|
		function(){
 | 
						|
			var that = this
 | 
						|
			this.logger 
 | 
						|
				&& this.logger.push('System').emit('start')
 | 
						|
 | 
						|
			// NOTE: jQuery currently provides no way to check if an event
 | 
						|
			// 		is bound so we'll need to keep track manually...
 | 
						|
			if(this.__stop_handler == null){
 | 
						|
				var stop = this.__stop_handler = function(){ that.stop() }
 | 
						|
 | 
						|
			} else {
 | 
						|
				return }
 | 
						|
 | 
						|
			// set the runtime...
 | 
						|
			var runtime = this.runtime = ImageGridFeatures.runtime
 | 
						|
 | 
						|
			// nw.js...
 | 
						|
			if(runtime.nw){
 | 
						|
				// this handles both reload and close...
 | 
						|
				$(window).on('beforeunload', stop)
 | 
						|
 | 
						|
				// NOTE: we are using both events as some of them do not
 | 
						|
				// 		get triggered in specific conditions and some do,
 | 
						|
				// 		for example, this gets triggered when the window's
 | 
						|
				// 		'X' is clicked while does not on reload...
 | 
						|
				this.__nw_stop_handler = function(){
 | 
						|
					var w = this
 | 
						|
					try{
 | 
						|
						that
 | 
						|
							// wait till ALL the handlers finish before 
 | 
						|
							// exiting...
 | 
						|
							.on('stop.post', function(){
 | 
						|
								// XXX might be broken in nw13 -- test!!!
 | 
						|
								//w.close(true)
 | 
						|
								nw.App.quit()
 | 
						|
							})
 | 
						|
							.stop()
 | 
						|
 | 
						|
					// in case something breaks exit...
 | 
						|
					// XXX not sure if this is correct...
 | 
						|
					} catch(e){
 | 
						|
						console.log('ERROR:', e)
 | 
						|
 | 
						|
						DEBUG || nw.App.quit()
 | 
						|
						//this.close(true)
 | 
						|
					} }
 | 
						|
				nw.Window.get().on('close', this.__nw_stop_handler)
 | 
						|
 | 
						|
 | 
						|
			// electron...
 | 
						|
			} else if(runtime.electron){
 | 
						|
				$(window).on('beforeunload', stop)
 | 
						|
 | 
						|
			// node...
 | 
						|
			} else if(runtime.node){
 | 
						|
				process.on('exit', stop)
 | 
						|
 | 
						|
			// browser...
 | 
						|
			} else if(runtime.browser){
 | 
						|
				$(window).on('beforeunload', stop)
 | 
						|
 | 
						|
			// other...
 | 
						|
			} else {
 | 
						|
				// XXX
 | 
						|
				console.warn('Unknown runtime:', runtime) }
 | 
						|
 | 
						|
			// handle ready event...
 | 
						|
			// ...if no one requested to do it.
 | 
						|
			if(this.__ready_announce_requested == null
 | 
						|
					|| this.__ready_announce_requested <= 0){
 | 
						|
				// in the browser world trigger .declareReady(..) on load event...
 | 
						|
				if(runtime.browser){
 | 
						|
					$(function(){ that.declareReady('start') })
 | 
						|
 | 
						|
				} else {
 | 
						|
					this.declareReady('start') } }
 | 
						|
 | 
						|
			// ready timeout -> force ready...
 | 
						|
			this.config['declare-ready-timeout'] > 0
 | 
						|
				&& !this.__ready_announce_timeout
 | 
						|
				&& (this.__ready_announce_timeout = 
 | 
						|
					setTimeout(
 | 
						|
						function(){
 | 
						|
							// cleanup...
 | 
						|
							delete this.__ready_announce_timeout
 | 
						|
							if((this.__ready_announce_requests || new Set()).size == 0){
 | 
						|
								delete this.__ready_announce_requests }
 | 
						|
							// force start...
 | 
						|
							if(!this.isReady()){
 | 
						|
								// report...
 | 
						|
								this.logger 
 | 
						|
									&& this.logger.push('start')
 | 
						|
										.emit('forcing ready.')
 | 
						|
										.emit('stalled:', 
 | 
						|
											this.__ready_announce_requested, 
 | 
						|
											...(this.__ready_announce_requests || []))
 | 
						|
 | 
						|
								// force ready...
 | 
						|
								this.__ready = !!this.ready() 
 | 
						|
 | 
						|
								// cleanup...
 | 
						|
								delete this.__ready_announce_requested
 | 
						|
								delete this.__ready_announce_requests } }.bind(this), 
 | 
						|
						this.config['declare-ready-timeout']))
 | 
						|
 | 
						|
			// trigger the started event...
 | 
						|
			this.started() }],
 | 
						|
	started: ['- System/System started event',
 | 
						|
		doc`
 | 
						|
		`,
 | 
						|
	   	Event(function(){
 | 
						|
			// System started event...
 | 
						|
			//
 | 
						|
			// Not intended for direct use.
 | 
						|
		})],
 | 
						|
 | 
						|
	// NOTE: it is recommended to use this protocol in such a way that
 | 
						|
	// 		the .ready() handler would recover from a stalled 
 | 
						|
	// 		.requestReadyAnnounce() call...
 | 
						|
	ready: ['- System/System ready event',
 | 
						|
		doc`Ready core event
 | 
						|
 | 
						|
		The ready event is fired right after start is done.
 | 
						|
 | 
						|
		Any feature can request to announce 'ready' itself when it is 
 | 
						|
		done by calling .requestReadyAnnounce().
 | 
						|
		If .requestReadyAnnounce() is called, then the caller is required
 | 
						|
		to also call .declareReady().
 | 
						|
		.ready() will actually be triggered only after when .declareReady()
 | 
						|
		is called the same number of times as .requestReadyAnnounce().
 | 
						|
 | 
						|
		NOTE: at this point the system does not track the caller 
 | 
						|
			"honesty", so it is the caller's responsibility to follow
 | 
						|
			the protocol.
 | 
						|
		`,
 | 
						|
	   	Event(function(){
 | 
						|
			// System ready event...
 | 
						|
			//
 | 
						|
			// Not intended for direct use, use .declareReady() to initiate.
 | 
						|
			this.logger 
 | 
						|
				&& this.logger.push('System').emit('ready') })],
 | 
						|
	// NOTE: this calls .ready() once per session.
 | 
						|
	declareReady: ['- System/Declare system ready', 
 | 
						|
		doc`Declare ready state
 | 
						|
 | 
						|
			.declareReady()
 | 
						|
 | 
						|
		This will call .ready() but only in the following conditions:
 | 
						|
			- .requestReadyAnnounce() has not been called.
 | 
						|
			- .requestReadyAnnounce() has been called the same number of
 | 
						|
				times as .declareReady()
 | 
						|
 | 
						|
		NOTE: this will call .ready() only once per start/stop cycle.
 | 
						|
		`,
 | 
						|
		function(message){
 | 
						|
			this.__ready_announce_requested
 | 
						|
				&& (this.__ready_announce_requested -= 1)
 | 
						|
 | 
						|
			message
 | 
						|
				&& this.__ready_announce_requests instanceof Set
 | 
						|
				&& this.__ready_announce_requests.delete(message)
 | 
						|
 | 
						|
			if(!this.__ready_announce_requested 
 | 
						|
					|| this.__ready_announce_requested <= 0){
 | 
						|
				this.__ready = this.__ready 
 | 
						|
					|| !!this.ready() 
 | 
						|
				delete this.__ready_announce_requested } }],
 | 
						|
	requestReadyAnnounce: ['- System/',
 | 
						|
		doc`Request to announce the .ready() event.
 | 
						|
 | 
						|
			.requestReadyAnnounce()
 | 
						|
			.requestReadyAnnounce(message)
 | 
						|
 | 
						|
		This enables a feature to delay the .ready() call until it is 
 | 
						|
		ready, this is useful for async or long stuff that can block
 | 
						|
		or slow down the .ready() phase.
 | 
						|
 | 
						|
		To indicate readiness, .declareReady() should be used.
 | 
						|
 | 
						|
		The system will call .ready() automatically when the last 
 | 
						|
		subscriber who called .requestReadyAnnounce() calls 
 | 
						|
		.declareReady(), i.e. .declareReady() must be called at least
 | 
						|
		as many times as .requestReadyAnnounce()
 | 
						|
 | 
						|
		The actual .ready() should never get called directly.
 | 
						|
 | 
						|
		NOTE: if this is called, .ready() will not get triggered 
 | 
						|
			automatically by the system.
 | 
						|
		`,
 | 
						|
		function(message){
 | 
						|
			message
 | 
						|
				&& (this.__ready_announce_requests = 
 | 
						|
					this.__ready_announce_requests || new Set())
 | 
						|
				&& this.__ready_announce_requests.add(message)
 | 
						|
 | 
						|
			return (this.__ready_announce_requested = 
 | 
						|
				(this.__ready_announce_requested || 0) + 1) }],
 | 
						|
 | 
						|
	// XXX revise args...
 | 
						|
	// XXX should this be here???
 | 
						|
	// XXX EXPERIMENTAL...
 | 
						|
	save: ['- System/',
 | 
						|
		doc``,
 | 
						|
		function(comment){
 | 
						|
			// XXX should this trigger the saved event pre/post outer action...
 | 
						|
		}],
 | 
						|
	saved: ['- System/',
 | 
						|
		doc``,
 | 
						|
		Event(function(comment){
 | 
						|
			// Base save event...
 | 
						|
			//
 | 
						|
			// Not intended for direct use.
 | 
						|
		})],
 | 
						|
 | 
						|
	stop: ['- System/', 
 | 
						|
		doc`Stop core action
 | 
						|
 | 
						|
			.stop()
 | 
						|
 | 
						|
		This will cleanup and unbind stop events.
 | 
						|
 | 
						|
		The goal of this is to prepare for system shutdown.
 | 
						|
 | 
						|
		NOTE: it is good practice for the bound handlers to set the 
 | 
						|
			system to a state from which their corresponding start/ready
 | 
						|
			handlers can run cleanly.
 | 
						|
		`,
 | 
						|
		function(){
 | 
						|
			// browser...
 | 
						|
			this.__stop_handler 
 | 
						|
				&& this.runtime.browser
 | 
						|
				&& $(window).off('beforeunload', this.__stop_handler)
 | 
						|
 | 
						|
			// nw...
 | 
						|
			if(this.__nw_stop_handler && this.runtime.nw){
 | 
						|
				nw.Window.get().removeAllListeners('close')
 | 
						|
				delete this.__nw_stop_handler }
 | 
						|
 | 
						|
			// node...
 | 
						|
			this.__stop_handler 
 | 
						|
				&& this.runtime.node
 | 
						|
				&& process.removeAllListeners('exit')
 | 
						|
 | 
						|
			delete this.__ready
 | 
						|
			delete this.__stop_handler
 | 
						|
 | 
						|
			this.logger 
 | 
						|
				&& this.logger.push('System').emit('stop')
 | 
						|
 | 
						|
			// trigger the stopped event...
 | 
						|
			this.stopped() }],
 | 
						|
	stopped: ['- System/System stopped event',
 | 
						|
		doc`
 | 
						|
		`,
 | 
						|
	   	Event(function(){
 | 
						|
			// System stopped event...
 | 
						|
			//
 | 
						|
			// Not intended for direct use.
 | 
						|
		})],
 | 
						|
 | 
						|
	// XXX not implemented...
 | 
						|
	// 		...this should be triggered before uninstall...
 | 
						|
	cleanup: ['- System/',
 | 
						|
		doc``,
 | 
						|
		Event(function(){
 | 
						|
			// System started event...
 | 
						|
			//
 | 
						|
			// Not intended for direct use.
 | 
						|
		})],
 | 
						|
 | 
						|
	// trigger core events...
 | 
						|
	//
 | 
						|
	// NOTE: we do not need to do .one(..) as it is implemented via .on(..)
 | 
						|
	//
 | 
						|
	// XXX EXPERIMENTAL...
 | 
						|
	// 		...should this be an action???
 | 
						|
	on: ['- System/',
 | 
						|
		function(evt, ...rest){
 | 
						|
			var func = rest.slice().pop()
 | 
						|
			evt = typeof(evt) == typeof('') ? evt.split(/\s/g) : evt
 | 
						|
 | 
						|
			// we trigger the handler AFTER it is registered...
 | 
						|
			return function(){
 | 
						|
				// started...
 | 
						|
				Math.max(
 | 
						|
						evt.indexOf('started'), 
 | 
						|
						evt.indexOf('started.pre'), 
 | 
						|
						evt.indexOf('started.post')) >= 0 
 | 
						|
					&& this.isStarted()
 | 
						|
					&& func.call(this)
 | 
						|
 | 
						|
				// ready...
 | 
						|
				// NOTE: we are ignoring the '.pre' events here as we are already
 | 
						|
				// 		in the specific state... 
 | 
						|
				Math.max(
 | 
						|
						evt.indexOf('ready'), 
 | 
						|
						evt.indexOf('ready.post')) >= 0 
 | 
						|
					&& this.isReady()
 | 
						|
					&& func.call(this)
 | 
						|
 | 
						|
				// started...
 | 
						|
				Math.max(
 | 
						|
						evt.indexOf('stopped'), 
 | 
						|
						evt.indexOf('stopped.pre'), 
 | 
						|
						evt.indexOf('stopped.post')) >= 0 
 | 
						|
					&& this.isStopped()
 | 
						|
					&& func.call(this) } }],
 | 
						|
 | 
						|
	// helpers...
 | 
						|
	//
 | 
						|
	restart: ['System/Soft restart',
 | 
						|
		doc`Soft restart
 | 
						|
 | 
						|
		This will stop, clear and then start ImageGrid.
 | 
						|
		`,
 | 
						|
		function(){
 | 
						|
			this
 | 
						|
				.stop()
 | 
						|
				.clear()
 | 
						|
				.start() }],
 | 
						|
})
 | 
						|
 | 
						|
var LifeCycle = 
 | 
						|
module.LifeCycle = ImageGridFeatures.Feature({
 | 
						|
	title: '',
 | 
						|
	doc: '',
 | 
						|
 | 
						|
	tag: 'lifecycle',
 | 
						|
	suggested: [
 | 
						|
		'logger',
 | 
						|
	],
 | 
						|
	priority: 'high',
 | 
						|
 | 
						|
	actions: LifeCycleActions,
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
 | 
						|
//---------------------------------------------------------------------
 | 
						|
// Serialization...
 | 
						|
 | 
						|
var SerializationActions = actions.Actions({
 | 
						|
	clone: ['- System/',
 | 
						|
		function(full){ 
 | 
						|
			return actions.MetaActions.clone.call(this, full) }],
 | 
						|
	json: ['- System/',
 | 
						|
		function(){ return {} }],
 | 
						|
	load: ['- System/',
 | 
						|
		function(data, merge){ !merge && this.clear() }],
 | 
						|
	clear: ['- Sustem/',
 | 
						|
		function(){ }],
 | 
						|
})
 | 
						|
 | 
						|
var Serialization = 
 | 
						|
module.Serialization = ImageGridFeatures.Feature({
 | 
						|
	title: '',
 | 
						|
 | 
						|
	tag: 'serialization',
 | 
						|
 | 
						|
	actions: SerializationActions,
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
 | 
						|
//---------------------------------------------------------------------
 | 
						|
// Cache...
 | 
						|
	
 | 
						|
// XXX revise: cache group naming...
 | 
						|
// 		currently the used groups are:
 | 
						|
// 			Session groups -- cleared on .clear() ('cache')
 | 
						|
// 				session-*
 | 
						|
// 				view-*
 | 
						|
// 			View groups -- cleared by crop/collection ('crop', 'collections')
 | 
						|
// 				view-*
 | 
						|
// 			Changes groups -- cleared when specific changes are made ('changes')
 | 
						|
// 				*-data
 | 
						|
// 				*-images
 | 
						|
// 				...
 | 
						|
// 		This approach seems not flexible enough...
 | 
						|
// 		Ideas:
 | 
						|
// 			- use keywords in group names??
 | 
						|
// XXX should we consider persistent caches -- localStorage???
 | 
						|
// XXX would be nice to have a simple cachedAction(name, cache-tag, expire, func) 
 | 
						|
// 		action wrapper that would not require anything from the action and 
 | 
						|
// 		just not call it if already called...
 | 
						|
// 		...to do this we'll need to be able to select a value by args 
 | 
						|
// 		from the cache this will require a diff mattch or something 
 | 
						|
// 		similar...
 | 
						|
var CacheActions = actions.Actions({
 | 
						|
	config: {
 | 
						|
		// Enable/disable caching...
 | 
						|
		'cache': true,
 | 
						|
 | 
						|
		// Control pre-caching...
 | 
						|
		//
 | 
						|
		// This can be:
 | 
						|
		// 		true	- sync pre-cache (recursion)
 | 
						|
		// 		0		- semi-sync pre-cache
 | 
						|
		// 		number	- delay in milliseconds between pre-cache chunks
 | 
						|
		// 		false	- pre-caching disabled
 | 
						|
		'pre-cache': 0,
 | 
						|
 | 
						|
		// Cache chunk length in ms...
 | 
						|
		//
 | 
						|
		// Caching is done in a series of chunks set by this separated by 
 | 
						|
		// timeouts set by .config['pre-cache'] to let other stuff run...
 | 
						|
		'pre-cache-chunk': 8,
 | 
						|
 | 
						|
		// Control pre-cache progress display...
 | 
						|
		//
 | 
						|
		// This can be:
 | 
						|
		// 	false		- never show progress
 | 
						|
		// 	true		- always show progress
 | 
						|
		// 	number		- show progress if number of milliseconds has 
 | 
						|
		// 					passed and we are not done yet...
 | 
						|
		//
 | 
						|
		// NOTE: progress will only be displayed if .showProgress(..) 
 | 
						|
		// 		action is available...
 | 
						|
		'pre-cache-progress': 3000,
 | 
						|
 | 
						|
 | 
						|
		// Groups to be cleared at the longest on session change...
 | 
						|
		//
 | 
						|
		// These include by default:
 | 
						|
		// 	'session'			- will live through the whole session.
 | 
						|
		// 	'view'				- cleared when view changes
 | 
						|
		//
 | 
						|
		'cache-session-groups': [
 | 
						|
			'session',
 | 
						|
			'view',
 | 
						|
		],
 | 
						|
 | 
						|
		// XXX handler cache..
 | 
						|
	},
 | 
						|
 | 
						|
	__cache: null,
 | 
						|
	cache: doc('Get or set cache value',
 | 
						|
		doc`Get or set cache value
 | 
						|
 | 
						|
			Get cached value in global group...
 | 
						|
			.cache(title)
 | 
						|
			.cache('global', title)
 | 
						|
				-> value
 | 
						|
				-> undefined
 | 
						|
 | 
						|
			Get cached value in a specific group...
 | 
						|
			.cache(group, title)
 | 
						|
				-> value
 | 
						|
				-> undefined
 | 
						|
		
 | 
						|
 | 
						|
			Get/set cached value in the global group...
 | 
						|
			.cache(title, handler)
 | 
						|
			.cache('global', title, handler)
 | 
						|
				-> value
 | 
						|
		
 | 
						|
			Get/set cached value in a specific group...
 | 
						|
			.cache(group, title, handler)
 | 
						|
				-> value
 | 
						|
 | 
						|
 | 
						|
			handler(value)
 | 
						|
				-> value
 | 
						|
		
 | 
						|
 | 
						|
		Handler calls will overwrite the cached value with the handler 
 | 
						|
		returned value on every call, this is different to pure getters 
 | 
						|
		that will only fetch a value if it exists.
 | 
						|
 | 
						|
 | 
						|
		Currently the used groups are:
 | 
						|
			Global group -- default group
 | 
						|
				global
 | 
						|
			Session groups -- cleared on .clear() (feature: 'cache')
 | 
						|
				session-*
 | 
						|
				view-*
 | 
						|
			View groups -- cleared by crop/collection (feature: 'crop', 'collections')
 | 
						|
				view-*
 | 
						|
			Changes groups -- cleared when specific changes are made (feature: 'changes')
 | 
						|
				*-data
 | 
						|
				*-images
 | 
						|
				...
 | 
						|
		
 | 
						|
 | 
						|
		Example use:
 | 
						|
			someAction: [
 | 
						|
				function(){
 | 
						|
					return this.cache('someAction', 
 | 
						|
						function(data){
 | 
						|
							if(data){
 | 
						|
								// clone/update the data...
 | 
						|
								// NOTE: this should be faster than the construction
 | 
						|
								//		branch below or this will defeat the purpose 
 | 
						|
								//		of caching...
 | 
						|
								...
 | 
						|
		
 | 
						|
							} else {
 | 
						|
								// get the data...
 | 
						|
								...
 | 
						|
							}
 | 
						|
							return data
 | 
						|
						}) }],
 | 
						|
		
 | 
						|
		
 | 
						|
		NOTE: since this is here to help speed things up, introducing a 
 | 
						|
			small but not necessary overhead by making this an action is
 | 
						|
			not logical...
 | 
						|
		`,
 | 
						|
		function(title, handler){
 | 
						|
			var args = [...arguments]
 | 
						|
			var handler = args.pop()
 | 
						|
			var group = 'global'
 | 
						|
			args.length == 2
 | 
						|
				&& ([group, title] = args)
 | 
						|
 | 
						|
			// caching disabled...
 | 
						|
			if(!(this.config || {}).cache){
 | 
						|
				return typeof(handler) != 'function' ?
 | 
						|
					undefined
 | 
						|
					: handler.call(this) }
 | 
						|
			// get...
 | 
						|
			if(typeof(handler) != 'function'){
 | 
						|
				return ((this.__cache || {})[group] || {})[handler]
 | 
						|
			// handle...
 | 
						|
			} else {
 | 
						|
				var cache = this.__cache = this.__cache || {}
 | 
						|
				cache = cache[group] = cache[group] || {}
 | 
						|
				return (cache[title] = 
 | 
						|
					title in cache ? 
 | 
						|
						// pass the cached data for cloning/update to the handler...
 | 
						|
						handler.call(this, cache[title])
 | 
						|
						: handler.call(this)) } }),
 | 
						|
	clearCache: ['System/Clear cache',
 | 
						|
		doc`
 | 
						|
 | 
						|
			Clear cache fully...
 | 
						|
			.clearCache()
 | 
						|
			
 | 
						|
			Clear title (global group)...
 | 
						|
			.clearCache(title)
 | 
						|
 | 
						|
			Clear title from group...
 | 
						|
			.clearCache(group, title)
 | 
						|
 | 
						|
			Clear out the full group...
 | 
						|
			.clearCache(group, '*')
 | 
						|
 | 
						|
 | 
						|
		NOTE: a group can be a string, list or a regexp object.
 | 
						|
		`,
 | 
						|
		function(title){
 | 
						|
			var that = this
 | 
						|
			// full clear...
 | 
						|
			if(arguments.length == 0 
 | 
						|
					|| (arguments[0] == '*' 
 | 
						|
						&& arguments[1] == '*')){
 | 
						|
				delete this.__cache
 | 
						|
			// partial clear...
 | 
						|
			} else {
 | 
						|
				var group = 'global'
 | 
						|
				// both group and title given...
 | 
						|
				arguments.length > 1
 | 
						|
					&& ([group, title] = arguments)
 | 
						|
 | 
						|
				// regexp...
 | 
						|
				// NOTE: these are only supported in groups...
 | 
						|
				if(group != '*' && group.includes('*')){
 | 
						|
					group = new RegExp('^'+ group +'$', 'i')
 | 
						|
					group = Object.keys(this.__cache || {})
 | 
						|
						.filter(function(g){
 | 
						|
							return group.test(g) }) }
 | 
						|
 | 
						|
				// clear title from each group...
 | 
						|
				if(group == '*' || group instanceof Array || group instanceof RegExp){
 | 
						|
					;(group instanceof Array ?
 | 
						|
						group
 | 
						|
					: group instanceof RegExp ? 
 | 
						|
						Object.keys(this.__cache || {})
 | 
						|
							.filter(function(g){
 | 
						|
								return group.test(g) })
 | 
						|
					: Object.keys(this.__cache || {}))
 | 
						|
						.forEach(function(group){
 | 
						|
							that.clearCache(group, title) })
 | 
						|
				// clear multiple titles...
 | 
						|
				} else if(title instanceof Array){
 | 
						|
					title.forEach(function(title){
 | 
						|
						delete ((that.__cache || {})[group] || {})[title] })
 | 
						|
				// clear group...
 | 
						|
				} else if(title == '*'){
 | 
						|
					delete (this.__cache || {})[group]
 | 
						|
				// clear title from group...
 | 
						|
				} else {
 | 
						|
					delete ((this.__cache || {})[group] || {})[title] } } }],
 | 
						|
 | 
						|
	// special caches...
 | 
						|
	//
 | 
						|
	sessionCache: ['- System/',
 | 
						|
		doc`Add to session cache...
 | 
						|
 | 
						|
			.sessionCache(title, handler)
 | 
						|
				-> value
 | 
						|
 | 
						|
 | 
						|
		This is a shorthand to:
 | 
						|
 | 
						|
			.cache('session', title, handler)
 | 
						|
				-> value
 | 
						|
 | 
						|
 | 
						|
		NOTE: also see .cache(..)
 | 
						|
		`,
 | 
						|
		'cache: "session" ...'],
 | 
						|
 | 
						|
 | 
						|
	// XXX doc: what are we precaching???
 | 
						|
	preCache: ['System/Run pre-cache',
 | 
						|
		doc`Run pre-cache...
 | 
						|
 | 
						|
			Do an async pre-cache...
 | 
						|
			.preCache()
 | 
						|
 | 
						|
			Do a sync pre-cache...
 | 
						|
			.preCache(true)
 | 
						|
 | 
						|
		NOTE: both "modes" of doing a pre-cache run in the main thread,
 | 
						|
			the difference is that the "async" version lets JS run frames
 | 
						|
			between processing sync chunks...
 | 
						|
		NOTE: this will not drop the existing cache, to do this run 
 | 
						|
			.clearCache() first or run .reCache(..).
 | 
						|
		`,
 | 
						|
		function(t){
 | 
						|
			if(this.config.cache){
 | 
						|
				var t = t || this.config['pre-cache'] || 0
 | 
						|
				var c = this.config['pre-cache-chunk'] || 8
 | 
						|
				var done = 0
 | 
						|
				var attrs = []
 | 
						|
				for(var k in this){
 | 
						|
					attrs.push(k) }
 | 
						|
				var l = attrs.length
 | 
						|
 | 
						|
				var started = Date.now()
 | 
						|
				var show = this.config['pre-cache-progress']
 | 
						|
 | 
						|
				var tick = function(){
 | 
						|
					var a = Date.now()
 | 
						|
					var b = a
 | 
						|
					if(attrs.length == 0){
 | 
						|
						return }
 | 
						|
					while(b - a < c){
 | 
						|
						this[attrs.pop()]
 | 
						|
						b = Date.now()
 | 
						|
						done += 1
 | 
						|
						this.showProgress
 | 
						|
							&& (show === true || (show && b - started > show))
 | 
						|
							&& this.showProgress('Caching', done, l) }
 | 
						|
					t === true ?
 | 
						|
						tick()
 | 
						|
						: setTimeout(tick, t) }.bind(this)
 | 
						|
 | 
						|
				tick() } }],
 | 
						|
	reCache: ['System/Re-cache',
 | 
						|
		function(t){
 | 
						|
			this
 | 
						|
				.clearCache()
 | 
						|
				.preCache(t) }],
 | 
						|
 | 
						|
	toggleHandlerCache: ['System/Action handler cache',
 | 
						|
		makeConfigToggler('action-handler-cache', 
 | 
						|
			['off', 'on']/*,
 | 
						|
			function(state){}*/)],
 | 
						|
	resetHanlerCache: ['System/Reset action handler cache',
 | 
						|
		function(){
 | 
						|
			delete this.__handler_cache }],
 | 
						|
})
 | 
						|
 | 
						|
var Cache = 
 | 
						|
module.Cache = ImageGridFeatures.Feature({
 | 
						|
	title: '',
 | 
						|
	doc: '',
 | 
						|
 | 
						|
	tag: 'cache',
 | 
						|
	// NOTE: we use .showProgress(..) of 'ui-progress' but we do not 
 | 
						|
	// 		need it to work, thus we do not declare it as a dependency...
 | 
						|
	//depends: [],
 | 
						|
 | 
						|
	actions: CacheActions,
 | 
						|
 | 
						|
	handlers: [
 | 
						|
		// System...
 | 
						|
		['start.pre', 
 | 
						|
			function(){ 
 | 
						|
				this.clearCache()
 | 
						|
				var t = this.config['pre-cache']
 | 
						|
				t === true ?
 | 
						|
					this.preCache('now') 
 | 
						|
				: t >= 0 ?
 | 
						|
					this.preCache() 
 | 
						|
				: false }],
 | 
						|
		['start',
 | 
						|
			function(){
 | 
						|
				// XXX this breaks loading...
 | 
						|
				// 		...not sure why, but when switched on manually 
 | 
						|
				// 		there seems to be no problems...
 | 
						|
				//this.toggleHandlerCache(this.config['action-handler-cache'] || 'on')
 | 
						|
			}],
 | 
						|
		/*/ XXX clear cache when feature/action topology changes...
 | 
						|
		[[
 | 
						|
			'inlineMixin',
 | 
						|
			'inlineMixout',
 | 
						|
 | 
						|
			// XXX not sure about this...
 | 
						|
			'mixout',
 | 
						|
		],
 | 
						|
			function(){
 | 
						|
				// XXX should this trigger a recache???
 | 
						|
				this.clearCache()
 | 
						|
			}],
 | 
						|
		//*/
 | 
						|
 | 
						|
 | 
						|
		// clear session cache...
 | 
						|
		['clear',
 | 
						|
			//'clearCache: "(session|view)(-.*)?" "*" -- Clear session cache'],
 | 
						|
			function(){
 | 
						|
				this.clearCache(`(${ 
 | 
						|
					(this.config['cache-session-groups'] 
 | 
						|
							|| ['session', 'view'])
 | 
						|
						.join('|') })(-.*)?`) }],
 | 
						|
	],
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
 | 
						|
//---------------------------------------------------------------------
 | 
						|
// Timers...
 | 
						|
 | 
						|
// Create a debounced action...
 | 
						|
//
 | 
						|
// 	debounce(<func>)
 | 
						|
// 	debounce(<timeout>, <func>)
 | 
						|
// 	debounce(<options>, <func>)
 | 
						|
// 		-> function
 | 
						|
//
 | 
						|
// options format:
 | 
						|
// 	{
 | 
						|
// 		timeout: number,
 | 
						|
// 		returns: 'cached' | 'dropped',
 | 
						|
// 		callback: function(retriggered, args),
 | 
						|
//
 | 
						|
// 		postcall: true,
 | 
						|
// 	}
 | 
						|
//
 | 
						|
// XXX might be a good ide to move this someplace generic...
 | 
						|
// XXX this is not debouncing pre/post calls, just the base action...
 | 
						|
var debounce =
 | 
						|
module.debounce =
 | 
						|
function(options, func){
 | 
						|
	// parse args...
 | 
						|
	var args = [...arguments]
 | 
						|
	func = args.pop()
 | 
						|
	options = args.pop() || {} 
 | 
						|
 | 
						|
	typeof(options) == typeof(123)
 | 
						|
		&& (options.timeout = options)
 | 
						|
 | 
						|
	// closure state...
 | 
						|
	var res
 | 
						|
	var last_args
 | 
						|
	var debounced = false
 | 
						|
	var retriggered = 0
 | 
						|
 | 
						|
	// call the action...
 | 
						|
	var call = function(context, ...args){
 | 
						|
		return func instanceof Function ?
 | 
						|
			func.call(context, ...args)
 | 
						|
			// alias...
 | 
						|
			: this.parseStringAction.callAction(context, func, ...args) }
 | 
						|
 | 
						|
	return object.mixin(
 | 
						|
		function(...args){
 | 
						|
			var retrigger
 | 
						|
			// call...
 | 
						|
			if(!debounced){
 | 
						|
				res = call(this, ...args)
 | 
						|
				res = options.returns != 'cahced' ? 
 | 
						|
					res 
 | 
						|
					: undefined
 | 
						|
 | 
						|
				// start the timer...
 | 
						|
				debounced = setTimeout(
 | 
						|
					function(){
 | 
						|
						var c
 | 
						|
						// callback...
 | 
						|
						options.callback instanceof Function
 | 
						|
							&& (c = options.callback.call(this, retriggered, args))
 | 
						|
						// retrigger...
 | 
						|
						options.postcall
 | 
						|
							&& retriggered > 0
 | 
						|
							&& c !== false
 | 
						|
							// XXX should this be a debounced call or a normal call...
 | 
						|
							// XXX this is not the actual action thus no 
 | 
						|
							// 		handlers will be triggered...
 | 
						|
							&& call(this, ...last_args)
 | 
						|
						// cleanup...
 | 
						|
						retriggered = 0
 | 
						|
						res = undefined
 | 
						|
						debounced = false }.bind(this), 
 | 
						|
					options.timeout 
 | 
						|
						|| this.config['debounce-action-timeout'] 
 | 
						|
						|| 200)
 | 
						|
			// skip...
 | 
						|
			} else {
 | 
						|
				retriggered++
 | 
						|
				last_args = args
 | 
						|
				return res } },
 | 
						|
		{
 | 
						|
			toString: function(){
 | 
						|
				return `// debounced...\n${
 | 
						|
					doc([ func instanceof Function ? 
 | 
						|
						func.toString() 
 | 
						|
						: func ])}` },
 | 
						|
		}) }
 | 
						|
 | 
						|
 | 
						|
 | 
						|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 | 
						|
 | 
						|
var TimersActions = actions.Actions({
 | 
						|
	config: {
 | 
						|
		//
 | 
						|
		// Format:
 | 
						|
		// 	{
 | 
						|
		// 		<id>: {
 | 
						|
		// 			// action code (string)...
 | 
						|
		// 			action: <action>,
 | 
						|
		// 			// interval in milliseconds...
 | 
						|
		// 			ms: <ms>,
 | 
						|
		// 		},
 | 
						|
		// 		...
 | 
						|
		// 	}
 | 
						|
		'persistent-intervals': null,
 | 
						|
 | 
						|
 | 
						|
		// A timeout to wait between calls to actions triggered via 
 | 
						|
		// .debounce(..)
 | 
						|
		'debounce-action-timeout': 200,
 | 
						|
	},
 | 
						|
 | 
						|
	// XXX should we store more metadata (ms?) and provide introspection 
 | 
						|
	// 		for these???
 | 
						|
	__timeouts: null,
 | 
						|
	__intervals: null,
 | 
						|
	__persistent_intervals: null,
 | 
						|
 | 
						|
 | 
						|
	// Introspection...
 | 
						|
	//
 | 
						|
	// NOTE: these are not editable...
 | 
						|
	get timeouts(){
 | 
						|
		return Object.assign({}, this.__timeouts || {}) },
 | 
						|
	get intervals(){
 | 
						|
		return {
 | 
						|
			volatile: Object.assign({}, this.__intervals || {}),
 | 
						|
			persistent: JSON.parse(JSON.stringify(
 | 
						|
				this.config['persistent-intervals'] || {})),
 | 
						|
		} },
 | 
						|
 | 
						|
	// XXX should these be  actions???
 | 
						|
	isTimeout: function(id){
 | 
						|
		return id in (this.__timeouts || {}) },
 | 
						|
	isInterval: function(id){
 | 
						|
		return id in (this.__intervals || {}) },
 | 
						|
	isPersistentInterval: function(id){
 | 
						|
		return id in (this.config['persistent-intervals'] || {}) },
 | 
						|
	isPersistentIntervalActive: function(id){
 | 
						|
		return this.isPersistentInterval(id) 
 | 
						|
			&& (id in (this.__persistent_intervals || {})) },
 | 
						|
	isTimer: function(id){
 | 
						|
		return this.isInterval(id) 
 | 
						|
			|| this.isPersistentInterval(id)
 | 
						|
			|| this.isTimeout(id) },
 | 
						|
 | 
						|
 | 
						|
	// General API...
 | 
						|
	// 
 | 
						|
	// NOTE: we are not trying to re-implement the native scheduler here
 | 
						|
	// 		just extend it and unify it's uses...
 | 
						|
	setTimeout: ['- System/', 
 | 
						|
		function(id, func, ms){
 | 
						|
			var timeouts = this.__timeouts = this.__timeouts || {}
 | 
						|
 | 
						|
			this.clearTimeout(id)
 | 
						|
 | 
						|
			timeouts[id] = setTimeout(
 | 
						|
				function(){
 | 
						|
					// cleanup...
 | 
						|
					// NOTE: we are doing this before we run to avoid 
 | 
						|
					// 		leakage due to errors...
 | 
						|
					delete timeouts[id]
 | 
						|
 | 
						|
					// run...
 | 
						|
					func instanceof Function ? 
 | 
						|
						func.call(this) 
 | 
						|
						: this.call(func) }.bind(this),
 | 
						|
				ms || 0) }],
 | 
						|
	clearTimeout: ['- System/',
 | 
						|
		function(id){
 | 
						|
			var timeouts = 
 | 
						|
				this.__timeouts = 
 | 
						|
					this.__timeouts 
 | 
						|
					|| {}
 | 
						|
			clearTimeout(timeouts[id])
 | 
						|
			delete timeouts[id]	}],
 | 
						|
 | 
						|
	setInterval: ['- System/',
 | 
						|
		function(id, func, ms){
 | 
						|
			var intervals = this.__intervals = this.__intervals || {}
 | 
						|
 | 
						|
			id in  intervals
 | 
						|
				&& clearInterval(intervals[id])
 | 
						|
 | 
						|
			intervals[id] = setInterval(
 | 
						|
				(func instanceof Function ? func : function(){ this.call(func) })
 | 
						|
					.bind(this),
 | 
						|
				ms || 0) }],
 | 
						|
	clearInterval: ['- System/',
 | 
						|
		function(id){
 | 
						|
			var intervals = this.__intervals = this.__intervals || {}
 | 
						|
			clearInterval(intervals[id])
 | 
						|
			delete intervals[id] }],
 | 
						|
 | 
						|
	setPersistentInterval: ['- System/',
 | 
						|
		doc`
 | 
						|
 | 
						|
			Restart interval id...
 | 
						|
			.setPersistentInterval(id)
 | 
						|
 | 
						|
			Save/start interval id...
 | 
						|
			.setPersistentInterval(id, action, ms)
 | 
						|
 | 
						|
		`,
 | 
						|
		function(id, action, ms){
 | 
						|
			var intervals = 
 | 
						|
				this.__persistent_intervals = 
 | 
						|
					this.__persistent_intervals || {}
 | 
						|
			// NOTE: we set this later iff we make a change...
 | 
						|
			var cfg = this.config['persistent-intervals'] || {}
 | 
						|
 | 
						|
			// get defaults...
 | 
						|
			action = action ? action : cfg[id].action
 | 
						|
			ms = ms ? ms : cfg[id].ms
 | 
						|
 | 
						|
			// checks...
 | 
						|
			if(!ms || !action){
 | 
						|
				console.error('Persistent interval: both action and ms must be set.')
 | 
						|
				return }
 | 
						|
			if(typeof(action) != typeof('str')){
 | 
						|
				console.error('Persistent interval: handler must be a string.')
 | 
						|
				return }
 | 
						|
 | 
						|
			id in  intervals
 | 
						|
				&& clearInterval(intervals[id])
 | 
						|
 | 
						|
			this.config['persistent-intervals'] = cfg
 | 
						|
			cfg[id] = {
 | 
						|
				action: action, 
 | 
						|
				ms: ms,
 | 
						|
			}
 | 
						|
 | 
						|
			intervals[id] = setInterval(
 | 
						|
				function(){ 
 | 
						|
					this.call(action) }.bind(this), 
 | 
						|
				ms || 0) }],
 | 
						|
	clearPersistentInterval: ['- System/',
 | 
						|
		function(id, stop_only){
 | 
						|
			var intervals = 
 | 
						|
				this.__persistent_intervals = 
 | 
						|
					this.__persistent_intervals || {}
 | 
						|
			clearInterval(intervals[id])
 | 
						|
			delete intervals[id]	
 | 
						|
			if(!stop_only){
 | 
						|
				delete this.config['persistent-intervals'][id] } }],
 | 
						|
	// XXX revise name (???)
 | 
						|
	// XXX do we need actions other than start/stop ???
 | 
						|
	persistentIntervals: ['- System/',
 | 
						|
		doc`
 | 
						|
 | 
						|
			Start/restart all persistent interval timers...
 | 
						|
			.persistentIntervals('start')
 | 
						|
			.persistentIntervals('restart')
 | 
						|
 | 
						|
			Stop all persistent interval timers...
 | 
						|
			.persistentIntervals('stop')
 | 
						|
		
 | 
						|
		NOTE: 'start' and 'restart' are the same, both exist for mnemonics.
 | 
						|
		`,
 | 
						|
		function(action){
 | 
						|
			var ids = Object.keys(this.config['persistent-intervals'] || {})
 | 
						|
 | 
						|
			// start/restart...
 | 
						|
			;(action == 'start' || action == 'restart') ?
 | 
						|
				ids.forEach(function(id){
 | 
						|
					this.setPersistentInterval(id) }.bind(this))
 | 
						|
			// stop...
 | 
						|
			: action == 'stop' ?
 | 
						|
				ids.forEach(function(id){
 | 
						|
					this.clearPersistentInterval(id, true) }.bind(this))
 | 
						|
			// unknown action...
 | 
						|
			: console.error('persistentIntervals: unknown action:', action) }],
 | 
						|
 | 
						|
	// Events...
 | 
						|
	//
 | 
						|
	// XXX should these be "aligned" to real time???
 | 
						|
	// 		...i.e. everyHour is triggered on the XX:00:00 and not relative
 | 
						|
	// 		to start time?
 | 
						|
	// XXX should we macro these???
 | 
						|
	/*/ XXX would be nice to trigger these ONLY if there are handlers...
 | 
						|
	everySecond: ['- System/',
 | 
						|
		Event(function(){
 | 
						|
			// XXX
 | 
						|
		})],
 | 
						|
	//*/
 | 
						|
	everyMinute: ['- System/',
 | 
						|
		Event(function(){
 | 
						|
			// XXX
 | 
						|
		})],
 | 
						|
	every2Minutes: ['- System/',
 | 
						|
		Event(function(){
 | 
						|
			// XXX
 | 
						|
		})],
 | 
						|
	every5Minutes: ['- System/',
 | 
						|
		Event(function(){
 | 
						|
			// XXX
 | 
						|
		})],
 | 
						|
	every10Minutes: ['- System/',
 | 
						|
		Event(function(){
 | 
						|
			// XXX
 | 
						|
		})],
 | 
						|
	every30Minutes: ['- System/',
 | 
						|
		Event(function(){
 | 
						|
			// XXX
 | 
						|
		})],
 | 
						|
	everyHour: ['- System/',
 | 
						|
		Event(function(){
 | 
						|
			// XXX
 | 
						|
		})],
 | 
						|
 | 
						|
 | 
						|
	// Action debounce...
 | 
						|
	//
 | 
						|
	debounce: ['- System/',
 | 
						|
		doc`Debounce action call...
 | 
						|
 | 
						|
		Debouncing prevents an action from being called more than once 
 | 
						|
		every timeout milliseconds.
 | 
						|
 | 
						|
			Debounce call an action...
 | 
						|
			.debounce(action, ...)
 | 
						|
			.debounce(timeout, action, ...)
 | 
						|
			.debounce(tag, action, ...)
 | 
						|
			.debounce(timeout, tag, action, ...)
 | 
						|
 | 
						|
			Debounce call a function...
 | 
						|
			.debounce(tag, func, ...)
 | 
						|
			.debounce(timeout, tag, func, ...)
 | 
						|
 | 
						|
			Generic debounce:
 | 
						|
			.debounce(options, action, ...)
 | 
						|
			.debounce(options, func, ...)
 | 
						|
 | 
						|
		options format:
 | 
						|
			{
 | 
						|
				// debounce timeout...
 | 
						|
				timeout: <milliseconds>,
 | 
						|
 | 
						|
				// tag to group action call debouncing (optional)
 | 
						|
				tag: <string>,
 | 
						|
 | 
						|
				// controls how the return value is handled:
 | 
						|
				// 	'cached'	- during the timeout the first return value
 | 
						|
				// 					is cached and re-returned on each call
 | 
						|
				// 					during the timeout.
 | 
						|
				// 	'dropped'	- all return values are ignored/dropped
 | 
						|
				//
 | 
						|
				// NOTE: these, by design, enable only stable/uniform behavior
 | 
						|
				// 		without introducing any special cases and gotchas...
 | 
						|
				returns: 'cached' | 'dropped',
 | 
						|
 | 
						|
				// if true the action will get retriggered after the timeout
 | 
						|
				// is over but only if it was triggered during the timeout...
 | 
						|
				//
 | 
						|
				// NOTE: if the action is triggered more often than timeout/200
 | 
						|
				// 		times, then it will not retrigger, this prevents an extra 
 | 
						|
				// 		call after, for example, sitting on a key and triggering
 | 
						|
				// 		key repeat...
 | 
						|
				retrigger: <bool>,
 | 
						|
 | 
						|
				// a function, if given will be called when the timeout is up.
 | 
						|
				callback: function(<retrigger-count>, <args>),
 | 
						|
			}
 | 
						|
 | 
						|
		NOTE: when using a tag, it must not resolve to and action, i.e.
 | 
						|
			this[tag] must not be callable...
 | 
						|
		NOTE: this ignores action return value and returns this...
 | 
						|
		NOTE: this uses core.debounce(..) adding a retrigger option to it...
 | 
						|
		`,
 | 
						|
		function(...args){
 | 
						|
			// parse the args...
 | 
						|
			if(!(args[0] instanceof Function 
 | 
						|
					|| typeof(args[0]) == typeof(123)
 | 
						|
					|| typeof(args[0]) == typeof('str'))){
 | 
						|
				var options = args.shift()
 | 
						|
				var tag = options.tag || args[0].name || args[0]
 | 
						|
 | 
						|
			} else {
 | 
						|
				var options = {
 | 
						|
					timeout: typeof(args[0]) == typeof(123) ?
 | 
						|
						args.shift()
 | 
						|
						: (this.config['debounce-action-timeout'] || 200),
 | 
						|
				}
 | 
						|
 | 
						|
				// NOTE: this[tag] must not be callable, otherwise we treat it
 | 
						|
				// 		as an action...
 | 
						|
				var tag = (args[0] instanceof Function 
 | 
						|
						|| this[args[0]] instanceof Function) ? 
 | 
						|
					args[0] 
 | 
						|
					: args.shift() }
 | 
						|
 | 
						|
			// sanity check: when debouncing a function a tag is required...
 | 
						|
			if(tag instanceof Function){
 | 
						|
				throw new TypeError(
 | 
						|
					'debounce: when passing a function a tag is required.') }
 | 
						|
 | 
						|
			var action = args.shift()
 | 
						|
			var attr = '__debounce_'+ tag
 | 
						|
 | 
						|
			options = Object.assign(Object.create(options), {
 | 
						|
				callback: function(retriggered, args){
 | 
						|
					// cleanup...
 | 
						|
					delete this[attr]
 | 
						|
 | 
						|
					// call the original callback...
 | 
						|
					options.__proto__.callback
 | 
						|
						&& options.__proto__.callback.call(that, ...args)
 | 
						|
 | 
						|
					if(options.retrigger 
 | 
						|
							&& retriggered > 0 
 | 
						|
							// this prevents an extra action after "sitting" 
 | 
						|
							// on the keyboard and triggering key repeat...
 | 
						|
							&& retriggered < (options.timeout || 200) / 200){
 | 
						|
						var func = this[attr] = this[attr] || debounce(options, action)
 | 
						|
						func.call(this, ...args) } },
 | 
						|
			})
 | 
						|
 | 
						|
			var func = this[attr] = this[attr] || debounce(options, action)
 | 
						|
 | 
						|
			return func.call(this, ...args) }],
 | 
						|
})
 | 
						|
 | 
						|
var Timers = 
 | 
						|
module.Timers = ImageGridFeatures.Feature({
 | 
						|
	title: '',
 | 
						|
	doc: '',
 | 
						|
 | 
						|
	tag: 'timers',
 | 
						|
	depends: [
 | 
						|
	],
 | 
						|
 | 
						|
	actions: TimersActions,
 | 
						|
 | 
						|
	handlers: [
 | 
						|
		// start persistent timers...
 | 
						|
		// XXX should this be start or ready???
 | 
						|
		['start', 
 | 
						|
			function(){ this.persistentIntervals('start') }],
 | 
						|
		// stop all timers...
 | 
						|
		['stop', 
 | 
						|
			function(){ 
 | 
						|
				Object.keys(this.__intervals || {})
 | 
						|
					.forEach(function(id){ this.clearInterval(id) }.bind(this))
 | 
						|
				Object.keys(this.__timeouts || {})
 | 
						|
					.forEach(function(id){ this.clearTimeout(id) }.bind(this))
 | 
						|
 | 
						|
				this.persistentIntervals('stop') }],
 | 
						|
 | 
						|
		// fixed timer actions...
 | 
						|
		// XXX not sure about these...
 | 
						|
		['start',
 | 
						|
			function(){
 | 
						|
				var m = 1000*60
 | 
						|
				this
 | 
						|
					.setInterval('everyMinute', 'everyMinute', m)
 | 
						|
					.setInterval('every2Minutes', 'every2Minutes', m*2)
 | 
						|
					.setInterval('every5Minutes', 'every5Minutes', m*5)
 | 
						|
					.setInterval('every10Minutes', 'every10Minutes', m*10)
 | 
						|
					.setInterval('every30Minutes', 'every30Minutes', m*30)
 | 
						|
					.setInterval('everyHour', 'everyHour', m*60) }],
 | 
						|
	],
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
 | 
						|
//---------------------------------------------------------------------
 | 
						|
// Journal...
 | 
						|
//
 | 
						|
// This feature logs actions that either have the journal attribute set 
 | 
						|
// to true or have an undo method/alias...
 | 
						|
// 
 | 
						|
// Example:
 | 
						|
// 		someAction: ['Path/to/Some action',
 | 
						|
// 			// just journal the action, but it can't be undone...
 | 
						|
// 			{journal: true},
 | 
						|
// 			function(){ 
 | 
						|
// 				... 
 | 
						|
// 			}],
 | 
						|
//
 | 
						|
// 		otherAction: ['Path/to/Other action',
 | 
						|
// 			// journal and provide undo functionality...
 | 
						|
// 			{undo: function(){ 
 | 
						|
// 				...
 | 
						|
// 			}},
 | 
						|
// 			function(){ 
 | 
						|
// 				... 
 | 
						|
// 			}],
 | 
						|
//
 | 
						|
// 		someOtherAction: ['Path/to/Some other action',
 | 
						|
// 			// use .otherAction(..) to undo...
 | 
						|
// 			{undo: 'otherAction'
 | 
						|
//
 | 
						|
// 				// test if action can be undone (returns bool)...
 | 
						|
// 				// NOTE: this is called before the <action>...
 | 
						|
// 				// NOTE: this can be an alias...
 | 
						|
// 				undoable: function(data){
 | 
						|
// 					...
 | 
						|
// 				},
 | 
						|
//
 | 
						|
// 				// if true do not group nested action calls (default: store)
 | 
						|
// 				// this can be:
 | 
						|
// 				//	'store'		- store nested journal in .nested
 | 
						|
// 				//	'drop'		- drop nested actions from journal
 | 
						|
// 				//	'keep'		- keep nested actions in journal
 | 
						|
// 				// XXX currently store/drop modes may include deferred or
 | 
						|
// 				//		triggered by external events actions...
 | 
						|
// 				nestedUndo: 'store',
 | 
						|
//
 | 
						|
// 				// store aditional undo state in the data, to be used by <action>.undo(..)...
 | 
						|
// 				// NOTE: this is called after the <action>...
 | 
						|
// 				// NOTE: this can be an alias...
 | 
						|
// 				getUndoState: function(data){
 | 
						|
// 					...
 | 
						|
// 				}},
 | 
						|
// 			function(){ 
 | 
						|
// 				... 
 | 
						|
// 			}],
 | 
						|
//
 | 
						|
// NOTE: .undo has priority over .journal, so there is no point of 
 | 
						|
// 		defining both .journal and .undo action attributes, one is 
 | 
						|
// 		enough.
 | 
						|
//
 | 
						|
//
 | 
						|
 | 
						|
// XXX need a mechanism to store the journal in sync (localStorage/fs)
 | 
						|
// 		and be able to execute the journal from a save position (which one?) 
 | 
						|
// 		if recovering from close/crash...
 | 
						|
// 		XXX should this be a separate feature???
 | 
						|
//
 | 
						|
// XXX would be great to add a mechanism define how to reverse actions...
 | 
						|
// 		...one way to do this at this point is to revert to last state
 | 
						|
// 		and re-run the journal until the desired event...
 | 
						|
// XXX need to define a clear journaling strategy in the lines of:
 | 
						|
// 		- save state clears journal and adds a state load action
 | 
						|
// 		- .load(..) clears journal
 | 
						|
// XXX needs careful testing...
 | 
						|
// XXX add a ui...
 | 
						|
var JournalActions = actions.Actions({
 | 
						|
 | 
						|
	clone: [function(full){
 | 
						|
		return function(res){
 | 
						|
			res.rjournal = null
 | 
						|
			res.journal = null
 | 
						|
			if(full && this.hasOwnProperty('journal') && this.journal){
 | 
						|
				res.journal = JSON.parse(JSON.stringify(this.journal)) } } }],
 | 
						|
 | 
						|
	// for format docs see: .updateJournalableActions(..)
 | 
						|
	journal: null,
 | 
						|
	rjournal: null,
 | 
						|
 | 
						|
	// XXX revise... 
 | 
						|
	get journalUnsaved(){
 | 
						|
		var res = []
 | 
						|
		//for(var e of (this.journal || []).slice().reverse()){
 | 
						|
		for(var i=(this.journal || []).length-1; i >= 0; i--){
 | 
						|
			var e = this.journal[i]
 | 
						|
			// everything until a load or a save event...
 | 
						|
			if(e == 'SAVED' 
 | 
						|
					|| e.type == 'save'
 | 
						|
					|| e.action == 'load'){
 | 
						|
				break }
 | 
						|
			res.unshift(e) }
 | 
						|
		return res },
 | 
						|
 | 
						|
	get journalable(){
 | 
						|
		return this.cache('journalable-actions', function(data){
 | 
						|
			return data ?
 | 
						|
				data.slice()
 | 
						|
				: this.updateJournalableActions() }) },
 | 
						|
 | 
						|
	// XXX RACE?: can things get on the journal while an action is running???
 | 
						|
	// 		...this would make the way nested actions are collected wrong 
 | 
						|
	// 		...this could happen for long and deferred actions...
 | 
						|
	// 		...not sure how handlers outside the action can be handled here 
 | 
						|
	// 		...investigate pushing undo to either top (explicitly user-called 
 | 
						|
	// 		action) or the bottom (actual data manipulation) levels...
 | 
						|
	// 		...let the client action configure things???
 | 
						|
	// 		...can we automate this -- marking nested actions???
 | 
						|
	// 		...a way to indirectly go around this is to investigate/document
 | 
						|
	// 		the possibilities and conditions of undo usage providing 
 | 
						|
	// 		appropriate API for all cases...
 | 
						|
	// XXX <action>.getUndoState(..) should be called for every action 
 | 
						|
	// 		in chain???
 | 
						|
	// XXX should aliases support explicit undo??? (test)
 | 
						|
	updateJournalableActions: ['- System/',
 | 
						|
		doc`Update journalable actions
 | 
						|
 | 
						|
		This will setup the action journal handler as a .pre handler 
 | 
						|
		(tagged: 'journal-handler').
 | 
						|
 | 
						|
		NOTE: calling this again will reset the existing handlers and add 
 | 
						|
			new ones.
 | 
						|
		NOTE: the list of journalable actions is cached and accessible via
 | 
						|
			.journalable prop and the cache API, e.g. via .cache('journalable-actions').
 | 
						|
		NOTE: action aliases can not handle undo.
 | 
						|
 | 
						|
		.journal / .rjournal format:
 | 
						|
			[
 | 
						|
				// journaled action..
 | 
						|
				{
 | 
						|
					type: 'basic' | ...,
 | 
						|
					date: <timestamp>,
 | 
						|
		
 | 
						|
					action: <action-name>,
 | 
						|
					args: [ ...	],
 | 
						|
		
 | 
						|
					// the current image before the action...
 | 
						|
					current: undefined | <gid>
 | 
						|
		
 | 
						|
					// the target (current) image after action...
 | 
						|
					target: undefined | <gid>
 | 
						|
 | 
						|
					// action state, only set on undoable actions when undone.
 | 
						|
					undone: true | false,
 | 
						|
 | 
						|
					// nested action journal (optional)
 | 
						|
					// this contains actions called from within the current
 | 
						|
					// action that can be undone.
 | 
						|
					nested: [ ... ],
 | 
						|
		
 | 
						|
					// additional data, can be set via: 
 | 
						|
					//		<action>.getUndoState(<data>)...
 | 
						|
					...
 | 
						|
				},
 | 
						|
 | 
						|
				...
 | 
						|
			]
 | 
						|
 | 
						|
		NOTE: newer journal items are pushed to the .journal tail...
 | 
						|
		`,
 | 
						|
		function(){
 | 
						|
			var that = this
 | 
						|
			var handler = function(action){
 | 
						|
				return function(){
 | 
						|
					var len = (this.journal || []).length
 | 
						|
					var data = {
 | 
						|
						type: 'basic',
 | 
						|
						date: Date.now(),
 | 
						|
 | 
						|
						action: action, 
 | 
						|
						args: [...arguments],
 | 
						|
 | 
						|
						current: this.current, 
 | 
						|
						// NOTE: we set this after the action is done...
 | 
						|
						target: undefined, 
 | 
						|
					}
 | 
						|
 | 
						|
					// test if we need to journal this action signature...
 | 
						|
					var test = that.getActionAttrAliased(action, 'undoable')
 | 
						|
					if(test === false 
 | 
						|
							|| (test && !test.call(that, data))){
 | 
						|
						return }
 | 
						|
 | 
						|
					// journal after the action is done...
 | 
						|
					return function(){ 
 | 
						|
						data.target = this.current
 | 
						|
						// collect nested journal data...
 | 
						|
						var nestedUndo = 
 | 
						|
							this.getActionAttr(action, 'nestedUndo') 
 | 
						|
							|| 'store'
 | 
						|
						if(nestedUndo != 'keep'
 | 
						|
								&& (this.journal || []).length > len){
 | 
						|
							var nested = (this.journal || []).splice(len) 
 | 
						|
							nestedUndo == 'store'
 | 
						|
								&& (data.nested = nested) }
 | 
						|
						// prep to get additional undo state...
 | 
						|
						// XXX this should be called for all actions in chain...
 | 
						|
						var update = that.getActionAttrAliased(action, 'getUndoState')
 | 
						|
						update 
 | 
						|
							&& update instanceof Function
 | 
						|
							&& update.call(that, data)
 | 
						|
						this.journalPush(data) } } }
 | 
						|
 | 
						|
			return this
 | 
						|
				// NOTE: we will overwrite the cache on every call...
 | 
						|
				.cache('journalable-actions', function(){ 
 | 
						|
					return this.actions
 | 
						|
						.filter(function(action){
 | 
						|
							// remove all existing journal handlers before we setup again...
 | 
						|
							that.off(action+'.pre', 'journal-handler')
 | 
						|
							// skip aliases...
 | 
						|
							return !(that[action] instanceof actions.Alias)
 | 
						|
								&& (!!that.getActionAttr(action, 'undo') 
 | 
						|
									|| !!that.getActionAttr(action, 'journal')) })
 | 
						|
						// set the handler
 | 
						|
						.map(function(action){
 | 
						|
							that.on(action+'.pre', 'journal-handler', handler(action))
 | 
						|
							return action }) }) }],
 | 
						|
 | 
						|
	// XXX unify names (globally) -> .journal<Action>(..) or .<action>Journal(..)
 | 
						|
	journalPush: ['- System/Journal/Add an item to journal',
 | 
						|
		function(data){
 | 
						|
			// clear the reverse journal...
 | 
						|
			// XXX we do not want to do this on redo...
 | 
						|
			this.rjournal
 | 
						|
				&& (this.rjournal = null)
 | 
						|
 | 
						|
			this.journal = 
 | 
						|
				(this.hasOwnProperty('journal') || this.journal) ? 
 | 
						|
					this.journal || []
 | 
						|
					: []
 | 
						|
			this.journal.push(data) }],
 | 
						|
	clearJournal: ['System/Journal/Clear the action journal',
 | 
						|
		function(){
 | 
						|
			// NOTE: overwriting here is better as it will keep
 | 
						|
			// 		shadowing the parent's .journal in case we 
 | 
						|
			// 		are cloned.
 | 
						|
			// NOTE: either way this will have no effect as we 
 | 
						|
			// 		only use the local .journal but the user may
 | 
						|
			// 		get confused...
 | 
						|
			//delete this.journal
 | 
						|
			this.journal
 | 
						|
				&& (this.journal = null)
 | 
						|
			this.rjournal
 | 
						|
				&& (this.rjournal = null) }],
 | 
						|
	// XXX not sure about the filter arg...
 | 
						|
	runJournal: ['- System/Journal/Run journal',
 | 
						|
		//{journal: true},
 | 
						|
		function(journal, filter){
 | 
						|
			var that = this
 | 
						|
			journal.forEach(function(e){
 | 
						|
				;(typeof(filter) == 'function'?
 | 
						|
						filter.call(that, e)
 | 
						|
						: true)
 | 
						|
					&& that
 | 
						|
						.focusImage(e.current)
 | 
						|
						// run action...
 | 
						|
						[e.action].apply(that, e.args) }) }],
 | 
						|
 | 
						|
	// XXX needs very careful revision...
 | 
						|
	// 		- should this be thread safe??? (likely not)
 | 
						|
	// 		- revise actions...
 | 
						|
	// XXX should we stop at non-undoable actions???
 | 
						|
	// 		...intuitively, yes, as undoing past these may result in an 
 | 
						|
	// 		inconsistent state...
 | 
						|
	// XXX should we implement redo as an undo of undo?
 | 
						|
	// XXX use .journalUnsaved???
 | 
						|
	undo: ['Edit/Undo',
 | 
						|
		doc`Undo last action(s) from .journal that can be undone
 | 
						|
 | 
						|
			.undo()
 | 
						|
			.undo(<count>)
 | 
						|
			.undo('<time-period>')
 | 
						|
			.undo('unsaved')
 | 
						|
			.undo('all')
 | 
						|
 | 
						|
 | 
						|
		This will shift the action from .journal to .rjournal preparing 
 | 
						|
		it for .redo()
 | 
						|
 | 
						|
		NOTE: this counts undoable actions only.
 | 
						|
		NOTE: actions when undone (i.e. undoable) are marked with .undone = true
 | 
						|
			while unundoable actions are simply copied over to .rjournal
 | 
						|
		`,
 | 
						|
		{mode: function(){ 
 | 
						|
			return (this.journal && this.journal.length > 0) || 'disabled' }},
 | 
						|
		function(count=1){
 | 
						|
			count = count == 'all' ?
 | 
						|
				Infinity
 | 
						|
				: count
 | 
						|
			var to = 
 | 
						|
				// time period...
 | 
						|
				(typeof(count) == 'string' 
 | 
						|
						&& Date.isPeriod(count)) ?
 | 
						|
					Date.now() - Date.str2ms(count)
 | 
						|
				// Date...
 | 
						|
				: count instanceof Date ?
 | 
						|
					count.valueOf()
 | 
						|
				: false
 | 
						|
			// NOTE: these are isolated from any other contexts and will 
 | 
						|
			// 		be saved as own attributes...
 | 
						|
			var journal = (this.journal || []).slice() || []
 | 
						|
			var rjournal = (this.rjournal || []).slice() || [] 
 | 
						|
 | 
						|
			for(var i = journal.length-1; i >= 0; i--){
 | 
						|
				var a = journal[i]
 | 
						|
 | 
						|
				// stop at save point...
 | 
						|
				if(count == 'unsaved'
 | 
						|
						&& (a == 'SAVED' 
 | 
						|
							|| a.type == 'save')){
 | 
						|
					break }
 | 
						|
				// stop at date...
 | 
						|
				if(to && a.date*1 < to){
 | 
						|
					break }
 | 
						|
				// stop at load...
 | 
						|
				// XXX not sure if this is correct....
 | 
						|
				if(a.action == 'load'){
 | 
						|
					break }
 | 
						|
				// stop at explicitly undoable actions...
 | 
						|
				var undoable = this.getActionAttrAliased(a.action, 'undoable')
 | 
						|
				if(undoable === false 
 | 
						|
						|| (undoable
 | 
						|
							&& !undoable.call(this, a))){
 | 
						|
					break }
 | 
						|
 | 
						|
				// see if the action has an explicit undo attr...
 | 
						|
				var undo = this.getActionAttr(a.action, 'undo')
 | 
						|
 | 
						|
				// general undo...
 | 
						|
				if(undo){
 | 
						|
					// restore focus to where it was when the action 
 | 
						|
					// was called...
 | 
						|
					this.focusImage(a.current)
 | 
						|
 | 
						|
					// call the undo method/action...
 | 
						|
					// NOTE: this is likely to have side-effect on the 
 | 
						|
					// 		journal and maybe even rjournal...
 | 
						|
					// NOTE: these side-effects are cleaned out later.
 | 
						|
					// XXX should we cache this???
 | 
						|
					undo instanceof Function ?
 | 
						|
						// pass the action name...
 | 
						|
						undo.call(this, a)
 | 
						|
					: typeof(undo) == typeof('str') ? 
 | 
						|
						// XXX pass journal structure as-is... (???)
 | 
						|
						this[undo].apply(this, a.args)
 | 
						|
					: null 
 | 
						|
 | 
						|
					a.undone = true } 
 | 
						|
 | 
						|
				// push the action to the reverse journal...
 | 
						|
				rjournal.push(journal.pop()) 
 | 
						|
			
 | 
						|
				// stop when done...
 | 
						|
				if(undo 
 | 
						|
						&& typeof(count) == 'number'
 | 
						|
						&& --count <= 0){
 | 
						|
					break } } 
 | 
						|
 | 
						|
			// restore journal state...
 | 
						|
			// NOTE: calling the undo action would have cleared
 | 
						|
			// 		the rjournal and added stuff to the journal
 | 
						|
			// 		so we will need to restore things...
 | 
						|
			this.journal = journal
 | 
						|
			this.rjournal = rjournal }],
 | 
						|
	// NOTE: we do not have to care about .nested actions on the redo
 | 
						|
	// 		level as they will be nested again by the root action...
 | 
						|
	redo: ['Edit/Redo',
 | 
						|
		doc`Redo an action from .rjournal
 | 
						|
 | 
						|
			.redo()
 | 
						|
			.redo(<count>)
 | 
						|
			.redo('all')
 | 
						|
 | 
						|
		Essentially this will remove and re-run the last action in .rjournal
 | 
						|
 | 
						|
		NOTE: this will clear the .undone attr of redoable actions
 | 
						|
		`,
 | 
						|
		{mode: function(){ 
 | 
						|
			return (this.rjournal && this.rjournal.length > 0) || 'disabled' }},
 | 
						|
		function(count=1){
 | 
						|
			if(!this.rjournal || this.rjournal.length == 0){
 | 
						|
				return }
 | 
						|
 | 
						|
			var journal = this.journal
 | 
						|
			var rjournal = this.rjournal
 | 
						|
			var l = rjournal.length
 | 
						|
 | 
						|
			if(count == 'all'){
 | 
						|
				count = Infinity
 | 
						|
			} else {
 | 
						|
				var t = 0
 | 
						|
				var c = 0
 | 
						|
				// count only undoable actions, i.e. the ones we undid...
 | 
						|
				for(var a of rjournal.slice().reverse()){
 | 
						|
					c++
 | 
						|
					a.undone	
 | 
						|
						&& t++
 | 
						|
					if(t >= count){
 | 
						|
						break } }
 | 
						|
				count = c }
 | 
						|
 | 
						|
			this.runJournal(
 | 
						|
				rjournal.splice(l-count || 0, count),
 | 
						|
				// skip actions not undoable and push them back to the journal...
 | 
						|
				function(e){
 | 
						|
					var redo = e.undone
 | 
						|
					!redo
 | 
						|
						&& journal.push(e)
 | 
						|
					delete e.undone
 | 
						|
					return redo })
 | 
						|
 | 
						|
			// restore .rjournal after actions are run...
 | 
						|
			// NOTE: this is done to compensate for .journalPush(..) clearing
 | 
						|
			// 		the .rjournal in normal operation...
 | 
						|
			// XXX HACK???
 | 
						|
			this.rjournal = rjournal }],
 | 
						|
 | 
						|
	//undoUnsaved: ['Edit/Undo unsaved',
 | 
						|
	//	'undo: "unsaved"'],
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
var Journal = 
 | 
						|
module.Journal = ImageGridFeatures.Feature({
 | 
						|
	title: 'Action Journal',
 | 
						|
 | 
						|
	tag: 'journal',
 | 
						|
	depends: [
 | 
						|
		'serialization',
 | 
						|
	],
 | 
						|
 | 
						|
	actions: JournalActions,
 | 
						|
 | 
						|
	// XXX need to drop journal on save...
 | 
						|
	// XXX rotate/truncate journal???
 | 
						|
	// XXX need to check that all the listed actions are clean -- i.e.
 | 
						|
	// 		running the journal will produce the same results as user 
 | 
						|
	// 		actions that generated the journal.
 | 
						|
	handlers: [
 | 
						|
		// log state, action and its args... 
 | 
						|
		['start',
 | 
						|
			'updateJournalableActions'],
 | 
						|
 | 
						|
		// clear journal when clearing...
 | 
						|
		// XXX we should be loading new journal instead...
 | 
						|
		// XXX is this a good idea???
 | 
						|
		['load clear',
 | 
						|
			'clearJournal'],
 | 
						|
 | 
						|
		// log saved event to journal...
 | 
						|
		['saved',
 | 
						|
			function(res, ...args){
 | 
						|
				// XXX
 | 
						|
				//this.journal.push('SAVED')
 | 
						|
				this.journalPush({
 | 
						|
					type: 'save',
 | 
						|
					// XXX should use the actual save timestamp...
 | 
						|
					date: Date.now(),
 | 
						|
					current: this.current, 
 | 
						|
					target: this.current, 
 | 
						|
				}) }],
 | 
						|
	],
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 | 
						|
 | 
						|
// XXX persistent journal...
 | 
						|
// 		- on journal -- save journal to localStorage
 | 
						|
// 		- on clear/load/timer -- save journal to file (auto-save)
 | 
						|
// 			...fs???
 | 
						|
// 		- on load -> load journal after last save
 | 
						|
// XXX need to revise journaling actions before shipping this...
 | 
						|
// XXX EXPERIMENTAL...
 | 
						|
var PersistentJournalActions = actions.Actions({
 | 
						|
	// XXX undoUnsaved(..) / reloadSaved(..)
 | 
						|
})
 | 
						|
 | 
						|
var PersistentJournal = 
 | 
						|
module.PersistentJournal = ImageGridFeatures.Feature({
 | 
						|
	title: 'Action persistent Journal',
 | 
						|
 | 
						|
	tag: 'journal-persistent',
 | 
						|
	depends: [
 | 
						|
		'journal',
 | 
						|
	],
 | 
						|
 | 
						|
	actions: PersistentJournalActions,
 | 
						|
 | 
						|
	handlers: [
 | 
						|
		// XXX
 | 
						|
	],
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
 | 
						|
//---------------------------------------------------------------------
 | 
						|
// Changes... 
 | 
						|
 | 
						|
var ChangesActions = actions.Actions({
 | 
						|
	// This can be:
 | 
						|
	// 	- null/undefined	- write all
 | 
						|
	// 	- true				- write all
 | 
						|
	// 	- false				- write nothing
 | 
						|
	// 	- {
 | 
						|
	//		// write/skip data...
 | 
						|
	//		data: <bool>,
 | 
						|
	//
 | 
						|
	//		// write/skip images or write a diff including the given 
 | 
						|
	//		// <gid>s only...
 | 
						|
	//		images: <bool> | [ <gid>, ... ],
 | 
						|
	//
 | 
						|
	//		// write/skip tags...
 | 
						|
	//		tags: <bool>,
 | 
						|
	//
 | 
						|
	//		// write/skip bookmarks...
 | 
						|
	//		bookmarked: <bool>,
 | 
						|
	//
 | 
						|
	//		// write/skip selected...
 | 
						|
	//		selected: <bool>,
 | 
						|
	//
 | 
						|
	//		// feature specific custom flags...
 | 
						|
	//		...
 | 
						|
	// 	  }
 | 
						|
	//
 | 
						|
	// NOTE: in the complex format all fields ar optional; if a field 
 | 
						|
	// 		is not included it is not written (same as when set to false)
 | 
						|
	// NOTE: .current is written always.
 | 
						|
	//
 | 
						|
	// XXX this should be a prop to enable correct changes tracking via 
 | 
						|
	// 		events...
 | 
						|
	chages: null,
 | 
						|
 | 
						|
	get _changes(){
 | 
						|
		return this.__changes },
 | 
						|
	// XXX proxy to .markChanged(..)
 | 
						|
	set _changes(value){},
 | 
						|
 | 
						|
	clone: [function(full){
 | 
						|
		return function(res){
 | 
						|
			res.changes = null
 | 
						|
			if(full 
 | 
						|
					&& this.hasOwnProperty('changes') 
 | 
						|
					&& this.changes){
 | 
						|
				res.changes = JSON.parse(JSON.stringify(this.changes)) } } }],
 | 
						|
 | 
						|
	// XXX this should also track .changes...
 | 
						|
	// 		...would also need to make this applicable to changes, 
 | 
						|
	// 		i.e. x.markChanged(x.changes)
 | 
						|
	markChanged: ['- System/',
 | 
						|
		doc`Mark data sections as changed...
 | 
						|
 | 
						|
			Mark everything changed...
 | 
						|
			.markChanged('all')
 | 
						|
 | 
						|
			Mark nothing changed...
 | 
						|
			.markChanged('none')
 | 
						|
 | 
						|
			Mark section(s) as changed...
 | 
						|
			.markChanged(<section>)
 | 
						|
			.markChanged(<section>, ..)
 | 
						|
			.markChanged([<section>, ..])
 | 
						|
 | 
						|
			Mark item(s) of section as changed...
 | 
						|
			.markChanged(<section>, [<item>, .. ])
 | 
						|
				NOTE: items must be strings...
 | 
						|
 | 
						|
		NOTE: when marking section items, the new items will be added to
 | 
						|
			the set of already marked items.
 | 
						|
		NOTE: when .changes is null (i.e. everything changed, marked via
 | 
						|
			.markChanged('all')) then calling this with anything other 
 | 
						|
			than 'none' will have no effect.
 | 
						|
		`,
 | 
						|
		function(section, items){
 | 
						|
			var that = this
 | 
						|
			var args = section instanceof Array ? 
 | 
						|
				section 
 | 
						|
				: [...arguments]
 | 
						|
			//var changes = this.changes = 
 | 
						|
			var changes = 
 | 
						|
				this.hasOwnProperty('changes') ?
 | 
						|
					this.changes || {}
 | 
						|
					: {}
 | 
						|
 | 
						|
			// all...
 | 
						|
			if(args.length == 1 && args[0] == 'all'){
 | 
						|
				this.changes = true
 | 
						|
 | 
						|
			// none...
 | 
						|
			} else if(args.length == 1 && args[0] == 'none'){
 | 
						|
				this.changes = false 
 | 
						|
 | 
						|
			// everything is marked changed, everything will be saved
 | 
						|
			// anyway...
 | 
						|
			// NOTE: to reset this use .markChanged('none') and then 
 | 
						|
			// 		manually add the desired changes...
 | 
						|
			} else if(this.changes == null){
 | 
						|
				return
 | 
						|
 | 
						|
			// section items...
 | 
						|
			} else if(items instanceof Array) {
 | 
						|
				if(changes[section] === true){
 | 
						|
					return }
 | 
						|
				changes[section] = (changes[section] || [])
 | 
						|
					.concat(items)
 | 
						|
					.unique()
 | 
						|
				this.changes = changes
 | 
						|
 | 
						|
			// section(s)...
 | 
						|
			} else {
 | 
						|
				args.forEach(function(arg){
 | 
						|
					changes[arg] = true })
 | 
						|
				this.changes = changes } }],
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
var Changes = 
 | 
						|
module.Changes = ImageGridFeatures.Feature({
 | 
						|
	title: '',
 | 
						|
	doc: '',
 | 
						|
 | 
						|
	tag: 'changes',
 | 
						|
	depends: [
 | 
						|
		'serialization',
 | 
						|
	],
 | 
						|
 | 
						|
	actions: ChangesActions,
 | 
						|
 | 
						|
	handlers: [
 | 
						|
		// handle changes...
 | 
						|
		['json',
 | 
						|
			function(res, mode){
 | 
						|
				if(this.changes != null){
 | 
						|
					res.changes = JSON.parse(JSON.stringify(this.changes)) } }],
 | 
						|
		['load',
 | 
						|
			function(_, data){
 | 
						|
				if(data.changes){
 | 
						|
					this.changes = JSON.parse(JSON.stringify(data.changes)) } }],
 | 
						|
 | 
						|
		// clear caches relating to stuff we just changed...
 | 
						|
		['markChanged',
 | 
						|
			function(_, section){
 | 
						|
				section = (section instanceof Array ?
 | 
						|
						section
 | 
						|
						: [section])
 | 
						|
					.map(function(section){ 
 | 
						|
						return '.*-'+section })
 | 
						|
				this.clearCache(section, '*') }],
 | 
						|
	],
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
 | 
						|
//---------------------------------------------------------------------
 | 
						|
// Workspace...
 | 
						|
//
 | 
						|
// Basic protocol:
 | 
						|
// 	A participating feature should:
 | 
						|
// 	- react to .saveWorkspace(..) by saving it's relevant state data to the 
 | 
						|
// 		object returned by the .saveWorkspace() action.
 | 
						|
// 		NOTE: it is recommended that a feature save its relevant .config
 | 
						|
// 			data as-is.
 | 
						|
// 		NOTE: no other action or state change should be triggered by this.
 | 
						|
// 	- react to .loadWorkspace(..) by loading it's state from the returned
 | 
						|
// 		object...
 | 
						|
// 		NOTE: this can be active, i.e. a feature may call actions when 
 | 
						|
// 			handling this.
 | 
						|
// 	- react to .toggleChrome(..) and switch on and off the chrome 
 | 
						|
// 		visibility... (XXX)
 | 
						|
//
 | 
						|
//
 | 
						|
 | 
						|
 | 
						|
// Helpers...
 | 
						|
var makeWorkspaceConfigWriter =
 | 
						|
module.makeWorkspaceConfigWriter = function(keys, callback){
 | 
						|
	return function(workspace){
 | 
						|
		var that = this
 | 
						|
 | 
						|
		var data = keys instanceof Function ? 
 | 
						|
			keys.call(this) 
 | 
						|
			: keys
 | 
						|
 | 
						|
		// store data...
 | 
						|
		data.forEach(function(key){
 | 
						|
			workspace[key] = JSON.parse(JSON.stringify(that.config[key])) })
 | 
						|
 | 
						|
		callback && callback.call(this, workspace) } }
 | 
						|
 | 
						|
// XXX should this delete a prop if it's not in the loading workspace???
 | 
						|
// XXX only replace a prop if it has changed???
 | 
						|
// XXX handle defaults -- when a workspace was just created...
 | 
						|
var makeWorkspaceConfigLoader =
 | 
						|
module.makeWorkspaceConfigLoader = function(keys, callback){
 | 
						|
	return function(workspace){
 | 
						|
		var that = this
 | 
						|
 | 
						|
		var data = keys instanceof Function ? 
 | 
						|
			keys.call(this) 
 | 
						|
			: keys
 | 
						|
 | 
						|
		// load data...
 | 
						|
		data.forEach(function(key){
 | 
						|
			// the key exists...
 | 
						|
			if(key in workspace){
 | 
						|
				that.config[key] = JSON.parse(JSON.stringify(workspace[key]))
 | 
						|
 | 
						|
			// no key set...
 | 
						|
			// XXX is this the right way to go???
 | 
						|
			} else {
 | 
						|
				delete that.config[key] } })
 | 
						|
 | 
						|
		callback 
 | 
						|
			&& callback.call(this, workspace) } }
 | 
						|
 | 
						|
 | 
						|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 | 
						|
 | 
						|
// XXX need a way to handle defaults...
 | 
						|
var WorkspaceActions = actions.Actions({
 | 
						|
	config: {
 | 
						|
		'load-workspace': 'default',
 | 
						|
 | 
						|
		'workspace': 'default',
 | 
						|
		'workspaces': {},
 | 
						|
	},
 | 
						|
 | 
						|
	get workspace(){
 | 
						|
		return this.config.workspace },
 | 
						|
	set workspace(value){
 | 
						|
		this.loadWorkspace(value) },
 | 
						|
 | 
						|
	get workspaces(){
 | 
						|
		return this.config.workspaces },
 | 
						|
 | 
						|
	getWorkspace: ['- Workspace/',
 | 
						|
		function(){ return this.saveWorkspace(null) }],
 | 
						|
 | 
						|
	// NOTE: these are mainly triggers for other features to save/load
 | 
						|
	// 		their specific states...
 | 
						|
	// NOTE: handlers should only set data on the workspace object passively,
 | 
						|
	// 		no activity is recommended.
 | 
						|
	// NOTE: if null is passed this will only get the data, but will 
 | 
						|
	// 		save nothing. this us useful for introspection and temporary
 | 
						|
	// 		context storage.
 | 
						|
	//
 | 
						|
	// XXX for some reason this does not get saved with .config...
 | 
						|
	saveWorkspace: ['Workspace/Save Workspace',
 | 
						|
		function(name){
 | 
						|
			if(!this.config.hasOwnProperty('workspaces')){
 | 
						|
				this.config['workspaces'] = 
 | 
						|
					JSON.parse(JSON.stringify(this.config['workspaces'])) }
 | 
						|
 | 
						|
			var res = {}
 | 
						|
 | 
						|
			if(name !== null){
 | 
						|
				this.config['workspaces'][name || this.config.workspace] = res }
 | 
						|
 | 
						|
			return res }],
 | 
						|
	// NOTE: merging the state data is the responsibility of the feature
 | 
						|
	// 		...this is done so as not to restrict the feature to one 
 | 
						|
	// 		specific way to do stuff...
 | 
						|
	loadWorkspace: ['Workspace/Load Workspace',
 | 
						|
		function(name){
 | 
						|
			name = name || this.config.workspace
 | 
						|
 | 
						|
			// get a workspace by name and load it...
 | 
						|
			if(typeof(name) == typeof('str')){
 | 
						|
				this.config.workspace = name
 | 
						|
 | 
						|
				return this.workspaces[name] || {}
 | 
						|
 | 
						|
			// we got the workspace object...
 | 
						|
			} else {
 | 
						|
				return name } }],
 | 
						|
 | 
						|
	// NOTE: this will not save the current workspace...
 | 
						|
	toggleWorkspace: ['Workspace/workspace',
 | 
						|
		makeConfigToggler('workspace',
 | 
						|
			function(){ return Object.keys(this.config['workspaces']) },
 | 
						|
			function(state){ this.loadWorkspace(state) })],
 | 
						|
 | 
						|
	// XXX should we keep the stack unique???
 | 
						|
	pushWorkspace: ['- Workspace/',
 | 
						|
		function(name){
 | 
						|
			name = name || this.workspace
 | 
						|
			var stack = this.__workspace_stack = this.__workspace_stack || []
 | 
						|
 | 
						|
			this.saveWorkspace()
 | 
						|
 | 
						|
			if(stack.slice(-1)[0] == name){
 | 
						|
				return }
 | 
						|
 | 
						|
			this.workspace != name && this.loadWorkspace(name)
 | 
						|
			stack.push(name) }],
 | 
						|
	popWorkspace: ['- Workspace/',
 | 
						|
		function(){
 | 
						|
			var stack = this.__workspace_stack
 | 
						|
 | 
						|
			if(!stack || stack.length == 0){
 | 
						|
				return }
 | 
						|
 | 
						|
			this.saveWorkspace()
 | 
						|
			this.loadWorkspace(stack.pop()) }],
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
var Workspace = 
 | 
						|
module.Workspace = ImageGridFeatures.Feature({
 | 
						|
	title: '',
 | 
						|
 | 
						|
	tag: 'workspace',
 | 
						|
 | 
						|
	depends: [
 | 
						|
		'lifecycle',
 | 
						|
	],
 | 
						|
 | 
						|
	actions: WorkspaceActions,
 | 
						|
 | 
						|
	handlers: [
 | 
						|
		['start', 
 | 
						|
			function(){ 
 | 
						|
				this.loadWorkspace(this.config['load-workspace'] || 'default') }],
 | 
						|
		// NOTE: this needs to be done before the .config is saved...
 | 
						|
		['stop.pre', 
 | 
						|
			function(){ 
 | 
						|
				this.saveWorkspace() }],
 | 
						|
	],
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
 | 
						|
//---------------------------------------------------------------------
 | 
						|
// Tasks and Queues...
 | 
						|
 | 
						|
// Task wrapper...
 | 
						|
//
 | 
						|
// This simply makes tasks actions discoverable...
 | 
						|
var Task =
 | 
						|
module.Task =
 | 
						|
function(func){
 | 
						|
	func.__task__ = true
 | 
						|
	return func }
 | 
						|
 | 
						|
 | 
						|
// Task action helpers...
 | 
						|
//
 | 
						|
// NOTE: for examples see:
 | 
						|
// 		features/examples.js: 
 | 
						|
// 			ExampleActions.exampleTask(..)
 | 
						|
// 			ExampleActions.exampleSessionTask(..)
 | 
						|
// NOTE: we can pass sync/async to this in two places, in definition:
 | 
						|
// 			var action = taskAction('some title', 'sync', function(..){ .. })
 | 
						|
// 		or
 | 
						|
// 			var action = taskAction('sync', 'some title', function(..){ .. })
 | 
						|
// 		and on call:
 | 
						|
// 			action('sync', ..)
 | 
						|
// 		during the later form 'sync' is passed to .Task(..) in the correct
 | 
						|
// 		position...
 | 
						|
// 		(see ig-types' runner.TaskManager(..) for more info)
 | 
						|
var taskAction =
 | 
						|
module.taskAction =
 | 
						|
function(title, func){
 | 
						|
	var pre_args = [...arguments]
 | 
						|
	func = pre_args.pop()
 | 
						|
	title = pre_args
 | 
						|
		.filter(function(t){ 
 | 
						|
			return t != 'sync' && t != 'async' })
 | 
						|
		.pop()
 | 
						|
 | 
						|
	var action
 | 
						|
	return (object.mixin(
 | 
						|
		action = Task(function(...args){
 | 
						|
			if(args[0] == 'sync' || args[0] == 'async'){
 | 
						|
				pre_args = [args.shift(), title] }
 | 
						|
			return Object.assign(
 | 
						|
				this.tasks.Task(...pre_args, func.bind(this), ...args), 
 | 
						|
				// make this searchable by .tasks.named(..)...
 | 
						|
				{ 
 | 
						|
					__session_task__: !!action.__session_task__,
 | 
						|
					name: action.name, 
 | 
						|
				}) }),
 | 
						|
		{
 | 
						|
			title,
 | 
						|
			toString: function(){
 | 
						|
				return `core.taskAction('${ action.name }', \n\t${ 
 | 
						|
					object.normalizeIndent('\t'+func.toString()) })` },
 | 
						|
		})) }
 | 
						|
 | 
						|
var sessionTaskAction =
 | 
						|
module.sessionTaskAction =
 | 
						|
function(title, func){
 | 
						|
	return object.mixin(
 | 
						|
		taskAction(...arguments),
 | 
						|
		{ __session_task__: true }) }
 | 
						|
 | 
						|
 | 
						|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 | 
						|
 | 
						|
// Queued wrapper...
 | 
						|
var Queued =
 | 
						|
module.Queued =
 | 
						|
function(func){
 | 
						|
	func.__queued__ = true
 | 
						|
	return Task(func) }
 | 
						|
 | 
						|
 | 
						|
// Queued action...
 | 
						|
// 
 | 
						|
//	queuedAction(title, func)
 | 
						|
//	queuedAction(title, options, func)
 | 
						|
//		-> action
 | 
						|
//
 | 
						|
//	func(..)
 | 
						|
//		-> res
 | 
						|
//
 | 
						|
//	action(..)
 | 
						|
//		-> promise(res)
 | 
						|
//
 | 
						|
//
 | 
						|
// The idea here is that each time a queued action is called it is run 
 | 
						|
// in a queue, and while it is running all consecutive calls are queued
 | 
						|
// and run according to the queue policy.
 | 
						|
//
 | 
						|
//
 | 
						|
// NOTE: for examples see:
 | 
						|
// 		features/examples.js: 
 | 
						|
// 			ExampleActions.exampleQueuedAction(..)
 | 
						|
// 			ExampleActions.exampleMultipleQueuedAction(..)
 | 
						|
//
 | 
						|
// XXX handle errors... (???)
 | 
						|
// XXX revise logging and logger passing...
 | 
						|
var queuedAction = 
 | 
						|
module.queuedAction =
 | 
						|
function(title, func){
 | 
						|
	var args = [...arguments]
 | 
						|
	func = args.pop()	
 | 
						|
	var [title, opts] = args
 | 
						|
 | 
						|
	var action
 | 
						|
	return object.mixin(
 | 
						|
		action = Queued(function(...args){
 | 
						|
			var that = this
 | 
						|
			return new Promise(function(resolve, reject){
 | 
						|
				Object.assign(
 | 
						|
					that.queue(title, opts || {})
 | 
						|
						.push(function(){
 | 
						|
							var res = func.call(that, ...args) 
 | 
						|
							resolve(res)
 | 
						|
							return res }),
 | 
						|
			   		{ 
 | 
						|
						__session_task__: !!action.__session_task__,
 | 
						|
						title: action.name, 
 | 
						|
					}) }) }),
 | 
						|
   		{
 | 
						|
			title,
 | 
						|
			toString: function(){
 | 
						|
				return `core.queuedAction('${action.name}',\n\t${ 
 | 
						|
					object.normalizeIndent( '\t'+ func.toString() ) })` },
 | 
						|
		}) }
 | 
						|
 | 
						|
var sessionQueueAction =
 | 
						|
module.sessionQueueAction =
 | 
						|
function(title, func){
 | 
						|
	return object.mixin(
 | 
						|
		queuedAction(...arguments),
 | 
						|
		{ __session_task__: true }) }
 | 
						|
 | 
						|
 | 
						|
// Queue action handler...
 | 
						|
//
 | 
						|
//	queueHandler(title[, opts], func)
 | 
						|
//	queueHandler(title[, opts], arg_handler, func)
 | 
						|
//		-> action
 | 
						|
//
 | 
						|
//	Chained queue handler...
 | 
						|
//	queueHandler(title[, opts], queueHandler(..), func)
 | 
						|
//		-> action
 | 
						|
//
 | 
						|
//
 | 
						|
//	Prepare args...
 | 
						|
//	arg_handler(queue, items, ...args)
 | 
						|
//		-> [items, ...args]
 | 
						|
//
 | 
						|
//	Prepare args in sync mode...
 | 
						|
//	arg_handler('sync', items, ...args)
 | 
						|
//		-> [items, ...args]
 | 
						|
//
 | 
						|
//
 | 
						|
//	Call action...
 | 
						|
//	action(items, ...args)
 | 
						|
//		-> promise
 | 
						|
//
 | 
						|
//	Call action in sync mode...
 | 
						|
//	action('sync', items, ...args)
 | 
						|
//		-> promise
 | 
						|
//
 | 
						|
//
 | 
						|
//	Action function...
 | 
						|
//	func(item, ...args)
 | 
						|
//		-> res
 | 
						|
//
 | 
						|
//
 | 
						|
// This is different from queuedAction(..) in that what is queued is not
 | 
						|
// the action itself but rather the first argument to that action and the
 | 
						|
// action is used by the queue to handle each item. The rest of the 
 | 
						|
// arguments are passed to each call.
 | 
						|
//
 | 
						|
// In 'sync' mode the action is run outside of queue/task right away, this
 | 
						|
// is done because for a queue we can only control the sync start, i.e. 
 | 
						|
// the first task execution, the rest depends on queue configuration 
 | 
						|
// thus making the final behaviour unpredictable.
 | 
						|
//
 | 
						|
//
 | 
						|
// NOTE: promise results are .flat()-tened, thus if it is needed to return 
 | 
						|
// 		a list of arrays then one must wrap the handler return value in an
 | 
						|
// 		array...
 | 
						|
// NOTE: sync-mode actions do not externally log anything, basic progress 
 | 
						|
// 		logging is handled by the queue/task which is not created in sync
 | 
						|
// 		mode.
 | 
						|
// NOTE: since the sync-mode can block it must be used very carefully.
 | 
						|
// NOTE: for an example of chaining several queues see features/examples's:
 | 
						|
// 			.exampleChainedQueueHandler(..)
 | 
						|
// NOTE: when chaining queues, in 'sync' mode all queues in the chain will
 | 
						|
// 		be run sync...
 | 
						|
// NOTE: when chaining arg_handler(..) will get one queue per level of 
 | 
						|
// 		chaining, but in 'sync' mode only one 'sync' is passed...
 | 
						|
// NOTE: when calling this multiple times for the same queue each call 
 | 
						|
// 		will call all the stages but since items are processes async the 
 | 
						|
// 		later calls' later stages may end up with empty input queues, 
 | 
						|
// 		e.g. for:
 | 
						|
// 			[1,2,3].map(e => ig.exampleChainedQueueHandler(e))
 | 
						|
// 		.exampleChainedQueueHandler(..) is called once per input and thus
 | 
						|
// 		the first two stages are called sync and by the time the last 
 | 
						|
// 		stage of the first call is triggered (async) all the inputs are 
 | 
						|
// 		ready thus the first call will process all the inputs and the 
 | 
						|
// 		later calls will get empty inputs (unless any new inputs are while 
 | 
						|
// 		processing added)...
 | 
						|
// 		i.e. within a queue/task async processing model there is no guarantee
 | 
						|
// 		that the item will be processed in the same call tree that it 
 | 
						|
// 		was added in...
 | 
						|
//
 | 
						|
// XXX might be a good idea to split this into a generic and domain parts 
 | 
						|
// 		and move the generic part into types/runner...
 | 
						|
// XXX check if item is already in queue (???)
 | 
						|
// 		...how do we identify item uniqueness??
 | 
						|
var queueHandler =
 | 
						|
module.queueHandler =
 | 
						|
function(title, func){
 | 
						|
	var args = [...arguments]
 | 
						|
	func = args.pop()	
 | 
						|
	var arg_handler = 
 | 
						|
		typeof(args.last()) == 'function' 
 | 
						|
			&& args.pop()
 | 
						|
	var [title, opts] = args
 | 
						|
 | 
						|
	var action
 | 
						|
	return object.mixin(
 | 
						|
		action = Queued(function(items, ...args){
 | 
						|
			var that = this
 | 
						|
			var inputs = [...arguments]
 | 
						|
 | 
						|
			// sync start...
 | 
						|
			if(inputs[0] == 'sync' || inputs[0] == 'async'){
 | 
						|
				var [sync, [items, ...args]] = [inputs.shift(), inputs] }
 | 
						|
 | 
						|
			// XXX see arg_handler(..) note below...
 | 
						|
			var q
 | 
						|
 | 
						|
			// pre-process args (sync)...
 | 
						|
			arg_handler
 | 
						|
				&& (inputs = arg_handler.call(this, 
 | 
						|
					sync == 'sync' ? 
 | 
						|
						sync 
 | 
						|
						// XXX should this be a queue???
 | 
						|
						// 		...seems like this is either 'sync' or 
 | 
						|
						// 		undefined but never a queue at this stage...
 | 
						|
						: q, 
 | 
						|
					...inputs))
 | 
						|
			// special-case: empty inputs -- no need to handle anything...
 | 
						|
			if(inputs instanceof Array 
 | 
						|
					&& (inputs.length == 0
 | 
						|
						|| (inputs[0] ?? []).length == 0)){
 | 
						|
				return Promise.resolve(inputs[0] ?? []) }
 | 
						|
 | 
						|
			// Define the runner and prepare...
 | 
						|
			//
 | 
						|
			// sync mode -- run action outside of queue...
 | 
						|
			// NOTE: running the queue in sync mode is not practical as
 | 
						|
			// 		the results may depend on queue configuration and 
 | 
						|
			// 		size...
 | 
						|
			if(sync == 'sync'){
 | 
						|
				var run = function([items, ...args]){
 | 
						|
					return Promise.all(
 | 
						|
						(items instanceof Array ? 
 | 
						|
							items 
 | 
						|
							: [items])
 | 
						|
						.map(function(item){
 | 
						|
							var res = func.call(that, item, ...args) 
 | 
						|
							return res === runner.SKIP ? 
 | 
						|
								[]
 | 
						|
								: [res] })
 | 
						|
						.flat()) }
 | 
						|
			// queue mode...
 | 
						|
			} else {
 | 
						|
				// prep queue...
 | 
						|
				q = that.queue(title,
 | 
						|
					Object.assign(
 | 
						|
						{},
 | 
						|
						opts || {},
 | 
						|
						{ 
 | 
						|
							__session_task__: !!action.__session_task__,
 | 
						|
							handler: function([item, args]){
 | 
						|
								// XXX is this correct in all cases...
 | 
						|
								item = item instanceof Array ?
 | 
						|
									item.flat()
 | 
						|
									: item
 | 
						|
								return func.call(that, item, ...(args || [])) }, 
 | 
						|
						}))
 | 
						|
				q.title = action.name 
 | 
						|
 | 
						|
				var run = function([items, ...args]){
 | 
						|
					// fill the queue...
 | 
						|
					// NOTE: we are also adding a ref to args here to keep things consistent...
 | 
						|
					args.length > 0
 | 
						|
						&& (args = [args])
 | 
						|
					q.add(items instanceof Array ? 
 | 
						|
						items
 | 
						|
							// move the inputs out of the input array...
 | 
						|
							// NOTE: this will prevent the items from getting 
 | 
						|
							// 		processed multiple times when the action 
 | 
						|
							// 		is called multiple times...
 | 
						|
							.splice(0, items.length)
 | 
						|
							.map(function(e){ 
 | 
						|
								return [e, ...args] }) 
 | 
						|
						: [[items, ...args]])
 | 
						|
					return q
 | 
						|
			   			.then(function(res){ 
 | 
						|
							// XXX we need to flatten this once and in-place...
 | 
						|
							// 		...if we keep this code it will copy res 
 | 
						|
							// 		on each call...
 | 
						|
							// 		...if we splice the flattened data back 
 | 
						|
							// 		into res it will be done on each call...
 | 
						|
							//return res && res.flat() }) } } 
 | 
						|
							// NOTE: we are compensating for this not being flat
 | 
						|
							// 		in the queue handler above...
 | 
						|
							return res }) } } 
 | 
						|
 | 
						|
			// run...
 | 
						|
			return (inputs instanceof Promise 
 | 
						|
					|| inputs instanceof runner.FinalizableQueue) ?
 | 
						|
				inputs.then(
 | 
						|
					function(items){
 | 
						|
						return run([items, ...args]) },
 | 
						|
					function(){
 | 
						|
						q && q.abort() })
 | 
						|
				: run(inputs) }),
 | 
						|
   		{
 | 
						|
			title,
 | 
						|
			arg_handler,
 | 
						|
			handler: func,
 | 
						|
 | 
						|
			toString: function(){
 | 
						|
				// XXX add opts if given...
 | 
						|
				return `core.queueHandler('${action.name}',\n${ 
 | 
						|
					(arg_handler ?
 | 
						|
						object.normalizeIndent('\t'+arg_handler.toString()).indent('\t') + ',\n'
 | 
						|
						: '')
 | 
						|
					+ object.normalizeIndent('\t'+func.toString()).indent('\t') })` },
 | 
						|
		}) }
 | 
						|
 | 
						|
var sessionQueueHandler =
 | 
						|
module.sessionQueueHandler =
 | 
						|
function(title, func){
 | 
						|
	return object.mixin(
 | 
						|
		queueHandler(...arguments),
 | 
						|
		{ __session_task__: true }) }
 | 
						|
 | 
						|
 | 
						|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 | 
						|
 | 
						|
// XXX EXPERIMENTAL...
 | 
						|
var LinkContext = 
 | 
						|
module.LinkContext = ImageGridFeatures.Feature({
 | 
						|
	title: '',
 | 
						|
	tag: 'link-context',
 | 
						|
	depends: [
 | 
						|
		'changes',
 | 
						|
	],
 | 
						|
 | 
						|
	actions: actions.Actions({
 | 
						|
		title: null,
 | 
						|
		// NOTE: we need .__parent to be able to test if we are fully 
 | 
						|
		// 		detached in .type...
 | 
						|
		// NOTE: this is maintained by .detachLink(..)...
 | 
						|
		__parent: null,
 | 
						|
		parent: null,
 | 
						|
 | 
						|
		get type(){
 | 
						|
			return this.parent ?
 | 
						|
					'link' 
 | 
						|
				: (this.__parent
 | 
						|
						&& (this.data !== this.__parent.data 
 | 
						|
							|| this.images !== this.__parent.images)) ?
 | 
						|
					'link-detached'
 | 
						|
				: 'link-partial' },
 | 
						|
 | 
						|
		__changes: null, 
 | 
						|
		get changes(){
 | 
						|
			return this.parent ?
 | 
						|
				this.parent.changes 
 | 
						|
				: this.__changes },
 | 
						|
		set changes(value){
 | 
						|
			this.parent ? 
 | 
						|
				(this.parent.changes = value)
 | 
						|
	   			: (this.__changes = value)},
 | 
						|
 | 
						|
		// NOTE: .detachLink(false) is not intended for direct use as it
 | 
						|
		// 		will create a partial link...
 | 
						|
		detachLink: ['- System/',
 | 
						|
			doc``,
 | 
						|
			function(full=true){
 | 
						|
				// partial detach...
 | 
						|
				if(this.type == 'link'){
 | 
						|
					// copy over .changes
 | 
						|
					this.__changes = this.changes === undefined ?
 | 
						|
						undefined
 | 
						|
						: JSON.parse(JSON.stringify([this.changes]))[0]
 | 
						|
					this.__parent = this.parent
 | 
						|
					delete this.parent }
 | 
						|
				// full detach...
 | 
						|
				if(this.type != 'link-detached' && full){
 | 
						|
					Object.assign(
 | 
						|
						this,
 | 
						|
						this.clone(true)) 
 | 
						|
					// cleanup...
 | 
						|
					// NOTE: we do not need to cleanup things outside of 
 | 
						|
					// 		the full detach as this will be done in .links
 | 
						|
					this.links.current === this
 | 
						|
						&& (delete this.links.current)
 | 
						|
					this.links.previous === this
 | 
						|
						&& (delete this.links.previous) } }],
 | 
						|
	}), })
 | 
						|
 | 
						|
 | 
						|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 | 
						|
 | 
						|
// XXX add ability to trigger actions when:
 | 
						|
// 		- all tasks are done and/or fail
 | 
						|
// 		- all session tasks are done and/or fail
 | 
						|
// 		...in theory this can be done via:
 | 
						|
// 			ig.tasks
 | 
						|
// 				.then(function(){ .. })
 | 
						|
// 		but this is a bit too cumbersome...
 | 
						|
// 		...do this via .chain(..)
 | 
						|
// XXX revise logging and logger passing...
 | 
						|
// XXX add a task manager UI...
 | 
						|
// XXX might be a good idea to confirm session task stops when loading a 
 | 
						|
// 		new index...
 | 
						|
var TaskActions = actions.Actions({
 | 
						|
	config: {
 | 
						|
		'context-exclude-attrs': [
 | 
						|
			// NOTE: these are commented out so as to reuse contexts where 
 | 
						|
			// 		possible... (XXX)
 | 
						|
			//'__links',
 | 
						|
			//'__isolated',
 | 
						|
 | 
						|
			// keep all the tasks/queues in one pool...
 | 
						|
			//
 | 
						|
			// NOTE: a linked context in here can stop main tasks and 
 | 
						|
			// 		vise versa...
 | 
						|
			// XXX what else should we isolate from the clone???
 | 
						|
			'__tasks',
 | 
						|
			'__queues',
 | 
						|
 | 
						|
			// NOTE: we link the changes directly to the parent so no need to 
 | 
						|
			//		copy them...
 | 
						|
			'changes',
 | 
						|
 | 
						|
			// keep the local features as they are not the same as .parent.features
 | 
						|
			'features',
 | 
						|
		],
 | 
						|
	},
 | 
						|
 | 
						|
	// Tasks...
 | 
						|
	//
 | 
						|
	isTask: function(action){
 | 
						|
		return !!this.getActionAttr(action, '__task__') },
 | 
						|
	isSessionTask: function(action){
 | 
						|
		return !!this.getActionAttr(action, '__session_task__') },
 | 
						|
	// list actions that generate tasks...
 | 
						|
	// XXX cache these???
 | 
						|
	get taskActions(){
 | 
						|
		return this.cache('taskActions', function(data){
 | 
						|
			return data 
 | 
						|
				|| this.actions.filter(this.isTask.bind(this)) }) },
 | 
						|
	get sessionTaskActions(){
 | 
						|
		return this.cache('sessionTaskActions', function(data){
 | 
						|
			return data 
 | 
						|
				|| this.actions.filter(this.isSessionTask.bind(this)) }) },
 | 
						|
 | 
						|
	// task manager...
 | 
						|
	//
 | 
						|
	__task_manager__: runner.TaskManager,
 | 
						|
	__tasks: null,
 | 
						|
	get tasks(){
 | 
						|
		return (this.__tasks = 
 | 
						|
			this.__tasks 
 | 
						|
				|| this.__task_manager__()) },
 | 
						|
	// NOTE: session tasks are stopped when the index is cleared...
 | 
						|
	// XXX do we need to cache this...
 | 
						|
	// 		...if yes then we'll need to also clear/update the cache 
 | 
						|
	// 		every time a task is run/stopped...
 | 
						|
	get sessionTasks(){
 | 
						|
		return this.tasks
 | 
						|
			.filter(function(task){
 | 
						|
				return task.__session_task__ }) },
 | 
						|
 | 
						|
 | 
						|
	// Queue (task)...
 | 
						|
	//
 | 
						|
	isQueued: function(action){
 | 
						|
		return !!this.getActionAttr(action, '__queued__') },
 | 
						|
	// XXX cache this???
 | 
						|
	// XXX need to get running tasks by action name...
 | 
						|
	get queuedActions(){
 | 
						|
		return this.cache('queuedActions', function(data){
 | 
						|
			return data 
 | 
						|
				|| this.actions.filter(this.isQueued.bind(this)) }) },
 | 
						|
 | 
						|
	// XXX need a way to reference the queue again...
 | 
						|
	// 		.tasks.titled(name) will return a list...
 | 
						|
	__queues: null,
 | 
						|
	get queues(){
 | 
						|
		return (this.__queues = this.__queues || {}) },
 | 
						|
 | 
						|
	// XXX test hidden progress...
 | 
						|
	// XXX revise logging and logger passing...
 | 
						|
	// XXX need better error flow...
 | 
						|
	queue: doc('Get or create a queue task',
 | 
						|
		doc`Get or create a queue task...
 | 
						|
 | 
						|
			.queue(name)
 | 
						|
			.queue(name, options)
 | 
						|
				-> queue
 | 
						|
 | 
						|
		If a queue with the given name already exits it will be returned 
 | 
						|
		and options and logger are ignored.
 | 
						|
 | 
						|
		options format:
 | 
						|
			{
 | 
						|
				nonAbortable: <bool>,
 | 
						|
				quiet: <bool>,
 | 
						|
				hideProgress: <bool>,
 | 
						|
				...
 | 
						|
			}
 | 
						|
 | 
						|
 | 
						|
		NOTE: when a task queue is stopped it will clear and cleanup, this is 
 | 
						|
			different to how normal queue behaves.
 | 
						|
		NOTE: for queue-specific options see ig-types/runner's Queue(..)
 | 
						|
		`,
 | 
						|
		function(name, options){
 | 
						|
			var that = this
 | 
						|
 | 
						|
			var queue = this.queues[name]
 | 
						|
 | 
						|
			// create a new queue...
 | 
						|
			if(queue == null){
 | 
						|
				var abort = function(){
 | 
						|
					options.nonAbortable
 | 
						|
						|| queue
 | 
						|
							.abort() }
 | 
						|
				var cleanup = function(){
 | 
						|
					return function(){
 | 
						|
						queue.stop()
 | 
						|
						// XXX handle error state...
 | 
						|
						//logger
 | 
						|
						//	&& logger.emit('close')
 | 
						|
						delete that.queues[name] } }
 | 
						|
 | 
						|
				options = options || {}
 | 
						|
				var logger = options.logger || this.logger
 | 
						|
				//logger = logger && logger.push(name)
 | 
						|
				logger = logger 
 | 
						|
					&& logger.push(name, {onclose: abort, quiet: !!options.quiet})
 | 
						|
				logger 
 | 
						|
					&& (options.logger = logger)
 | 
						|
 | 
						|
				queue = this.queues[name] = 
 | 
						|
					runner.FinalizableQueue(options || {})
 | 
						|
 | 
						|
				// setup logging...
 | 
						|
				var suffix = (options || {}).hideProgress ? 
 | 
						|
					' (hidden)' 
 | 
						|
					: ''
 | 
						|
				queue
 | 
						|
					.on('tasksAdded', function(evt, t){ 
 | 
						|
						this.logger && this.logger.emit('added'+suffix, t) })
 | 
						|
					// NOTE: t can be anything including an array, so to 
 | 
						|
					// 		avoid confusion we wrap it in an array this 
 | 
						|
					// 		one call means one emit...
 | 
						|
					.on('taskCompleted', function(evt, t, r){ 
 | 
						|
						this.logger && this.logger.emit('done'+suffix, [t]) }) 
 | 
						|
					.on('taskFailed', function(evt, t, err){ 
 | 
						|
						this.logger && this.logger.emit('skipped'+suffix, t, err) }) 
 | 
						|
					.on('stop', function(){
 | 
						|
						this.logger && this.logger.emit('reset') })
 | 
						|
					.on('abort', function(){
 | 
						|
						this.logger && this.logger.emit('reset') })
 | 
						|
				// cleanup...
 | 
						|
				queue
 | 
						|
					.then(
 | 
						|
						cleanup('done'), 
 | 
						|
						cleanup('error')) }
 | 
						|
 | 
						|
			// add queue as task...
 | 
						|
			this.tasks.includes(queue)
 | 
						|
				|| this.tasks.Task(name, queue) 
 | 
						|
 | 
						|
			return queue }),
 | 
						|
 | 
						|
 | 
						|
 | 
						|
	// contexts (XXX EXPERIMENTAL)
 | 
						|
	//
 | 
						|
	// XXX would be nice to have a context manager:
 | 
						|
	// 		- context id's (index? ...sparse array?)
 | 
						|
	// 		- manager API
 | 
						|
	// 			- create/remove
 | 
						|
	// 		- context api (feature) 
 | 
						|
	// 			.then(..)/.catch(..)/.finally(..)
 | 
						|
	// XXX is peer stuff just a special context???
 | 
						|
	// 		...feels like yes
 | 
						|
	// XXX is context manager a special case of task manager???
 | 
						|
	// XXX move to a separate feature... (???)
 | 
						|
	__contexts: null,
 | 
						|
	get contexts(){},
 | 
						|
 | 
						|
	// XXX this should delete the clone when done...
 | 
						|
	// XXX need a common context API to make management possible...
 | 
						|
	ContextTask: ['- System/',
 | 
						|
		doc``,
 | 
						|
		function(type, action, ...args){
 | 
						|
			var that = this
 | 
						|
			var context = this[type]
 | 
						|
 | 
						|
			var res = context[action](...args)
 | 
						|
 | 
						|
			var cleanup = function(){
 | 
						|
				// XXX
 | 
						|
			}
 | 
						|
 | 
						|
			res.finally ?
 | 
						|
				res.finally(cleanup)
 | 
						|
				: cleanup()
 | 
						|
 | 
						|
			return res === context ? 
 | 
						|
				undefined 
 | 
						|
				: res }],
 | 
						|
	
 | 
						|
	// Links...
 | 
						|
	//
 | 
						|
	// NOTE: all links to current state in .links will be detached on .clear()
 | 
						|
	__links: null,
 | 
						|
	get links(){
 | 
						|
		var links = this.__links = this.__links || {}
 | 
						|
		// remove 'current' if it does not match the current index...
 | 
						|
		// XXX revise the test...
 | 
						|
		var c = links.current
 | 
						|
		if(c && (c.data !== this.data || c.images !== this.images)){
 | 
						|
			links.previous = c
 | 
						|
			delete links.current }
 | 
						|
		return links },
 | 
						|
	get linked(){
 | 
						|
		return this.link() },
 | 
						|
	// XXX go through ImageGrid instance data and re-check what needs to 
 | 
						|
	// 		be cloned...
 | 
						|
	// XXX should this be a constructor???
 | 
						|
	link: ['- System/',
 | 
						|
		doc`Get/create links...
 | 
						|
 | 
						|
			Get/create link to current state...
 | 
						|
			.link()
 | 
						|
			.link('current')
 | 
						|
				-> current-link
 | 
						|
 | 
						|
			Get link to previous state if present...
 | 
						|
			.link('previous')
 | 
						|
				-> previous-link
 | 
						|
				-> undefined
 | 
						|
 | 
						|
			Get/create a titled link...
 | 
						|
			.link(title)
 | 
						|
				-> link
 | 
						|
 | 
						|
		A link is a separate ImageGrid instance that links to the parent's
 | 
						|
		state and explicitly disabled ui features.
 | 
						|
 | 
						|
		A link will reflect the data changes but when the main index is 
 | 
						|
		cleared or reloaded it will retain the old data.
 | 
						|
		Care must be taken as this is true in both directions and changes 
 | 
						|
		to link state are reflected on the link .parent, this is useful
 | 
						|
		when updating state in the background but can bite the user if not
 | 
						|
		used carefully. 
 | 
						|
 | 
						|
		This effectively enables us to isolate a context for long running 
 | 
						|
		actions/tasks and make them independent of the main state.
 | 
						|
 | 
						|
		Example:
 | 
						|
			ig.linked.readAllMetadata()
 | 
						|
 | 
						|
 | 
						|
		NOTE: links are relatively cheap as almost no data is copied but
 | 
						|
			they can be a source of a memory "leak" if not cleaned out 
 | 
						|
			as they prevent data from being garbage collected...
 | 
						|
		NOTE: 'current' and 'previous' links are reserved.
 | 
						|
		NOTE: 'previous' are a special case as they can not be created 
 | 
						|
			via .link(..).
 | 
						|
		`,
 | 
						|
		function(title='current'){
 | 
						|
			var that = this
 | 
						|
			var links = this.links
 | 
						|
			// get link already created...
 | 
						|
			// NOTE: 'current' and 'previous' links are handled by the 
 | 
						|
			// 		.links prop...
 | 
						|
			var link = links[title]
 | 
						|
			if(link){
 | 
						|
				return link }
 | 
						|
			// prevent creating previous links...
 | 
						|
			if(title == 'previous'){
 | 
						|
				return actions.UNDEFINED }
 | 
						|
			// create a link...
 | 
						|
			// NOTE: we intentionally disable ui here and do not trigger .start()...
 | 
						|
			return (links[title] = 
 | 
						|
				Object.assign(
 | 
						|
					// new base object...
 | 
						|
					// XXX add a 'link' feature...
 | 
						|
					ImageGridFeatures.setup([
 | 
						|
						...this.features.input, 
 | 
						|
						'-ui',
 | 
						|
						'link-context',
 | 
						|
					]),
 | 
						|
					// clone data...
 | 
						|
					// NOTE: this can shadow parts of the new base object 
 | 
						|
					// 		so we'll need to exclude some stuff...
 | 
						|
					Object.assign({}, this)
 | 
						|
						.run(function(){
 | 
						|
							// remove excluded attrs...
 | 
						|
							;(that.config['context-exclude-attrs'] 
 | 
						|
									|| [ 'features' ])
 | 
						|
								.forEach(function(key){
 | 
						|
									delete this[key] }.bind(this)) }),
 | 
						|
					// context-specific data...
 | 
						|
					{
 | 
						|
						// link metadata...
 | 
						|
						parent: this,
 | 
						|
						title: title,
 | 
						|
						// link configuration...
 | 
						|
						logger: that.logger
 | 
						|
							.push(`Linked ${ Object.keys(links).length }`),
 | 
						|
					})
 | 
						|
				// detach link on parent .clear(..)...
 | 
						|
				.run(function(){
 | 
						|
					var link = this
 | 
						|
					that.one('clear.pre', function(){
 | 
						|
						// NOTE: we are doing a partial detach here as the 
 | 
						|
						// 		parent is overwriting its data and we do not
 | 
						|
						// 		need to clone it...
 | 
						|
						link.detachLink(false) }) })) }],
 | 
						|
 | 
						|
 | 
						|
	// XXX would be nice to have an ability to partially clone the instance...
 | 
						|
	// 		...currently we can do a full clone and remove things we do 
 | 
						|
	// 		not want...
 | 
						|
	// XXX this does not copy aliases...
 | 
						|
	// XXX might be a good idea to add a 'IsolatedTask' feature/mixin to
 | 
						|
	// 		handle cleanup (via .done() action)
 | 
						|
	// XXX should this be a prop -- .isolated???
 | 
						|
	__isolated: null,
 | 
						|
	get isolated(){
 | 
						|
		return (this.__isolated = this.__isolated || []) },
 | 
						|
	// XXX should this be a constructor???
 | 
						|
	isolate: ['- System/',
 | 
						|
		function(){
 | 
						|
			var clones = this.isolated
 | 
						|
 | 
						|
			var clone = this.clone(true)
 | 
						|
			// reset actions to exclude UI...
 | 
						|
			clone.__proto__ = ImageGridFeatures.setup([...this.features.input, '-ui'])
 | 
						|
			clone.parent = this
 | 
						|
			// link clone in...
 | 
						|
			clone.logger = this.logger.push(['Task', clones.length].join(' '))
 | 
						|
 | 
						|
			clone.context_id = clones.push(clone)
 | 
						|
			return clone }],
 | 
						|
 | 
						|
})
 | 
						|
 | 
						|
var Tasks = 
 | 
						|
module.Tasks = ImageGridFeatures.Feature({
 | 
						|
	title: '',
 | 
						|
 | 
						|
	tag: 'tasks',
 | 
						|
 | 
						|
	depends: [ ],
 | 
						|
 | 
						|
	actions: TaskActions,
 | 
						|
 | 
						|
	handlers: [
 | 
						|
		// stop session tasks...
 | 
						|
		['clear',
 | 
						|
			'sessionTasks.abort'],
 | 
						|
	],
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
 | 
						|
//---------------------------------------------------------------------
 | 
						|
// Self test framework...
 | 
						|
 | 
						|
// Indicate an action to be a self-test action...
 | 
						|
// 
 | 
						|
// Self test actions are run by .selfTest(..)
 | 
						|
// 
 | 
						|
// XXX should we set an action attr or a func attr here???
 | 
						|
var selfTest =
 | 
						|
module.selfTest = function(func){
 | 
						|
	func.__self_test__ = true
 | 
						|
	return func }
 | 
						|
 | 
						|
var SelfTestActions = actions.Actions({
 | 
						|
	config: {
 | 
						|
		'run-selftest-on-start': true,
 | 
						|
	},
 | 
						|
 | 
						|
	selfTest: ['System/Run self test',
 | 
						|
		selfTest(function(mode){
 | 
						|
			var that = this
 | 
						|
			var logger = this.logger && this.logger.push('Self test')
 | 
						|
 | 
						|
			var tests = this.actions
 | 
						|
				.filter(function(action){ 
 | 
						|
					return action != 'selfTest'
 | 
						|
			   			&& (that[action].func.__self_test__ 
 | 
						|
							|| that.getActionAttr(action, 'self_test'))})
 | 
						|
			logger 
 | 
						|
				&& tests.forEach(function(action){ 
 | 
						|
					logger.emit('found', action) })
 | 
						|
			tests.forEach(function(action){
 | 
						|
				that[action]()
 | 
						|
				logger 
 | 
						|
					&& logger.emit('done', action) }) })],
 | 
						|
})
 | 
						|
 | 
						|
var SelfTest = 
 | 
						|
module.SelfTest = ImageGridFeatures.Feature({
 | 
						|
	title: '',
 | 
						|
	doc: '',
 | 
						|
 | 
						|
	tag: 'self-test',
 | 
						|
	depends: [
 | 
						|
		'lifecycle'	
 | 
						|
	],
 | 
						|
	suggested: [
 | 
						|
		'logger',
 | 
						|
	],
 | 
						|
	priority: 'low',
 | 
						|
 | 
						|
	actions: SelfTestActions, 
 | 
						|
 | 
						|
	handlers: [
 | 
						|
		['start',
 | 
						|
			function(){ 
 | 
						|
				this.config['run-selftest-on-start'] 
 | 
						|
					&& this.selfTest() }]
 | 
						|
	],
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
 | 
						|
 | 
						|
/**********************************************************************
 | 
						|
* vim:set ts=4 sw=4 nowrap :                        */ return module })
 |