/********************************************************************** * * * **********************************************************************/ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ var toggler = require('../toggler') var keyboard = require('../keyboard') var object = require('../object') var widget = require('./widget') /*********************************************************************/ // Helpers... // Collect a list of literal values and "make(..) calls" into an array... // // collectItems(context, items) // -> values // // // items format: // [ // // explicit value... // value, // // // literal make call... // make(..), // // ... // ] // // NOTE: this will remove the made via make(..) items from .items thus the // caller is responsible for adding them back... // NOTE: this uses the make(..) return value to implicitly infer the items // to collect, thus the items must already be constructed and in // the same order as they are present in .items // ...also, considering that this implicitly identifies the items // passing the make function without calling it can trick the system // and lead to unexpected results. // // XXX would be nice to have a better check/test... // ...this could be done by chaining instances of make instead of // returning an actual function, i.e. each make call would return // a "new" function that would reference the actual item (.item()) // and the previous item created (.prevItem()), ... etc. // ...this would enable us to uniquely identify the actual items // and prevent allot of specific errors... var collectItems = function(make, items){ var made = items .filter(function(e){ return e === make }) // constructed item list... // ...remove each instance from .items made = make.items.splice( make.items.length - made.length, made.length) // get the actual item values... return items .map(function(e){ return e === make ? made.shift() // raw item -> make(..) : (make(e) && make.items.pop()) }) } //--------------------------------------------------------------------- // XXX general design: // - each of these can take either a value or a function (constructor) // - the function has access to Items.* and context // - the constructor can be called from two contexts: // - external // called from the module or as a function... // calls the passed constructor (passing context) // builds the container // - nested // called from constructor function... // calls constructor (if applicable) // builds item(s) // XXX need a way to pass container constructors (a-la ui-widgets dialog containers) // - passing through the context (this) makes this more flexible... // - passing via args fixes the signature which is a good thing... // // // XXX // XXX can't use Object.assign(..) here as it will not copy props... var Items = module.items = function(){} // placeholders... Items.dialog = null Items.items = null // Last item created... // XXX not sure about this... // XXX should this be a prop??? Items.last = function(){ return (this.items || [])[this.items.length - 1] } // Focus last created item... Items.focus = function(){ this.last().current = true } // Group a set of items... // // .group(make(..), ..) // .group([make(..), ..]) // -> make // // // Example: // make.group( // make('made item'), // 'literal item', // ...) // // // NOTE: see notes to collectItems(..) for more info... // // XXX do we need to pass options to groups??? Items.group = function(...items){ var that = this items = items.length == 1 && items[0] instanceof Array ? items[0] : items // replace the items with the group... this.items.splice(this.items.length, 0, collectItems(this, items)) return this } // Place list in a sub-list of item... // // XXX options??? Items.nest = function(item, list, options){ options = options || {} //options = Object.assign(Object.create(this.options || {}), options || {}) options.sublist = list instanceof Array ? collectItems(this, list) : list return this(item, options) } //--------------------------------------------------------------------- // wrappers... Items.Item = function(value, options){} Items.Action = function(value, options){} Items.Heading = function(value, options){} Items.Empty = function(value){} Items.Separator = function(value){} Items.Spinner = function(value){} Items.Selected = function(value){} Items.Editable = function(value){} Items.ConfirmAction = function(value){} // lists... Items.List = function(values){} Items.EditableList = function(values){} Items.EditablePinnedList = function(values){} // Special list components... Items.ListPath = function(){} Items.ListTitle = function(){} //--------------------------------------------------------------------- // Generate an event method... // // Trigger an event // .event() // .event(arg, ..) // -> this // // Bind an event handler... // .event(func) // -> this // // // XXX should this be simply a shorthand to .trigger(..) ??? var makeEventMethod = function(event, handler){ return function(item){ // register handler... if(item instanceof Function){ return this.on(event, item) } // XXX STUG: event object... // XXX can we generate this in one spot??? // ...currently it is generated here and in .trigger(..) var evt = { name: event, // XXX //stopPropagation: function(){ //}, } // XXX handle more of the API??? handler && handler.call(this, evt, ...arguments) // XXX we should get the actual item and pass it on... this.trigger(evt, ...arguments) return this } } var callItemEventHandlers = function(item, event, ...args){ ;(item[event] ? [item[event]] : []) .concat((item.events || {})[event] || []) .forEach(function(handler){ // XXX revise call signature... handler.call(item, evt, item, ...args) }) } // XXX use .find(..) instead of .get(..) here.... var makeItemEventMethod = function(event, handler){ return makeEventMethod(event, function(evt, item, ...args){ item = item ? // XXX this.get(item) : [] item = item instanceof Array ? item : [item] handler && handler.call(this, evt, item, ...args) item.forEach(function(item){ callItemEventHandlers(item, event) }) }) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var BaseBrowserClassPrototype = { } // XXX need a way to identify items... var BaseBrowserPrototype = { // XXX should we mix item/list options or separate them into sub-objects??? options: { noDuplicateValues: false, }, // parent widget object... // // NOTE: this may or may not be a Browser object. parent: null, // // Format: // [ // | , // ... // ] // // format: // { // value: ..., // // ... // } // // NOTE: this can't be a map/dict as we need both order manipulation // and nested structures which would overcomplicate things, as // a compromise we use .item_key_index below for item identification. __items: null, get items(){ this.__items || this.make() return this.__items }, set items(value){ this.__items = value }, // // Format: // { // : , // ... // } // // NOTE: this will get overwritten each tume .make(..) is called. // // XXX need to maintain this over item add/remove/change... // XXX Q: should we be able to add/remove/change items outside of .__list__(..)??? // ...only some item updates (how .collapsed is handled) make // sense at this time -- need to think about this more // carefully + strictly document the result... // XXX can we make the format here simpler with less level // of indirection?? // ...currently to go down a path we need to: // this.item_key_index.A.sublist.item_key_index.B.sublist... // would be nice to be closer to: // this.A.B... __item_index: null, get item_key_index(){ this.__item_index || this.make() return this.__item_index }, set item_key_index(value){ this.__item_index = value }, // XXX what should these return??? (item, id, ...) __focused: undefined, get focused(){ return this.__focused && this.__focused.focused ? this.__focused : (this.__focused = this // XXX should we simple bailout when we find an item??? .filter(function(e){ return e.focused }).shift()) }, set focused(value){ // XXX }, __selected: null, get selected(){ return this.__selected || (this.__selected = this .filter(function(e){ return e.selected })) }, set selected(value){ // XXX }, // Length... // // visible only... get length(){ return this.map({skipNested: true}).length // XXX this is wrong as it will not account for nested nested elements... + this.nested() .reduce(function(res, e){ return e.collapsed ? res + 1 : res + e.sublist.length }, 0) }, // tree -- ignores .collapsed... get lengthTree(){ return this.map({skipNested: true}).length // XXX this is wrong as it will not account for nested nested elements... + this.nested() .reduce(function(res, e){ return res + e.sublist.length }, 0) }, // full -- ignores .collapsed and .noniterable... get lengthAll(){ return this.map({skipNested: true, iterateNonIterable: true}).length // XXX this is wrong as it will not account for nested nested elements... + this.nested() .reduce(function(res, e){ return res + (e.sublist.lengthAll || e.sublist.length) }, 0) }, // Item list constructor... // // .__list__(make, options) // -> undefined // -> list // // // Item constructor: // make(value) // make(value, options) // -> make // // // There are two modes of operation: // 1) call make(..) to create items // 2) return a list of items // // // The if make(..) is called at least once the return value is // ignored (mode #1), otherwise, the returned list is used as the // .items structure. // // // When calling make(..) (mode #1) the item is built by combining // the following in order: // - original item (.items[key]) if present, // - options passed to .make() method calling .__list__(..), // - options passed to make(.., ) constructing the item, // - {value: } where passed to make(, ..) // // Each of the above will override values of the previous sections. // // The resulting item is stored in: // .items // .item_key_index (keyed via .id or JSONified .value) // // Each of the above structures is reset on each call to .make(..) // // options format: // { // id: , // value: | , // // sublist: | , // // focused: , // selected: , // disabled: , // noniterable: , // // // Set automatically... // parent: , // // XXX move this to the appropriate object... // dom: , // } // // // Example: // XXX // // // In mode #2 XXX // // // NOTE: this is not designed to be called directly... __list__: function(make, options){ throw new Error('.__list__(..): Not implemented.') }, // XXX need a better key/path API... // __value2key__: function(key){ //return JSON.stringify(key) return key instanceof Array ? key.join(' ') : key }, // Key getter/generator... // // XXX should these include the path??? // XXX is JSON the best key format??? __key__: function(item){ return item.id // value is a browser -> generate an unique id... // XXX identify via structure... || (item.value instanceof Browser && this.__id__()) || this.__value2key__(item.value) }, // ID generator... // // Format: // "" // " " // // XXX do a better id... // Ex: // "abc" // "abc (1)" // "abc (2)" // XXX not sure about the logic of this, should this take an item as // input and return an id??? // ...should this check for uniqueness??? // think merging this with any of the actual ID generators would be best... __id__: function(prefix){ // id prefix... return (prefix || '') // separator... + (prefix ? ' ' : '') // date... + Date.now() }, // Walk the browser... // // .walk(handleItem[, options]) // -> result // // .walk(handleItem, handleRecursion[, options]) // -> result // // .walk(handleItem, handleRecursion, isWalkable[, options]) // -> result // // // // handleItem(path, elem, index, doNested, sublist) // -> items // // Trigger nested item handling... // doNested([options]) // doNested(list[, options]) // doNested(index[, options]) // doNested(list, index[, options]) // -> items // // Disable automatic nested item handling... // doNested(false) // -> undefined // // Force nested item handling... // doNested(true, ..) // -> items // // NOTE: doNested(..) has no effect of options.reverse is // set to 'flat'... // NOTE: only the first call to doNested(..) as any effect, all // consecutive calls will return cached results of the first // call... // // // // Handle recursion down... // handleRecursion(func, index, path, sublist, options) // -> items // // // // Test if item is walkable... // isWalkable(item) // -> bool // // // // options format: // { // // Partial walking... // // // // XXX not implemented yet... // start: | , // count: , // end: | , // // // // Iterate ALL items... // // // // NOTE: this if true overrides all other iteration coverage // // options... // iterateAll: , // // // If true do not skip items with .noniterable set to true... // iterateNonIterable: , // // If true do not skip item.sublist of items with .collapsed // // set to true... // iterateCollapsed: , // // If true skip iterating nested items... // skipNested: , // // // Reverse iteration order... // // // // modes: // // false | null - normal order (default) // // true | 'tree' - reverse order of levels but keep // // topology order, i.e. containers // // will precede contained elements. // // 'flat' - full flat reverse // // // // NOTE: in 'flat' mode the client loses control over the // // order of processing via doNested(..) as it will be // // called before handleItem(..) // reverse: | 'flat' | 'tree', // // // The value to be used if .reverse is set to true... // defaultReverse: 'tree' (default) | 'flat', // // // // If true include inlined parent id in path... // // XXX not implemented yet -- can we implement this???... // // XXX do we need this?? // inlinedPaths: , // } // // // XXX can we add support for partial walks, i.e. start/end ranges??? // XXX can this support breadth first walking??? // XXX revise protocol... walk: function(func, options){ var that = this // parse args... var args = [...arguments] func = args[0] instanceof Function ? args.shift() : undefined var recursion = (args[0] instanceof Function || typeof(args[0]) == typeof('str')) ? args.shift() : undefined var userIsWalkable = args[0] instanceof Function ? args.shift() : null var i = typeof(args[0]) == typeof(123) ? args.shift() : 0 var path = (args[0] instanceof Array || typeof(args[0]) == typeof('str')) ? args.shift() : [] path = path instanceof Array ? path : [path] options = args.pop() || {} // set call context... options = !options.root ? Object.assign({}, options, {root: this}) : options // options... options = Object.assign(Object.create(this.options || {}), options) /*var start = options.start || 0 var end = options.end || (options.count >= 0 && start + (options.count)) || Infinity //*/ var iterateNonIterable = options.iterateAll || options.iterateNonIterable var iterateCollapsed = options.iterateAll || options.iterateCollapsed var skipNested = !options.iterateAll && options.skipNested var reverse = options.reverse === true ? (options.defaultReverse || 'tree') : options.reverse var isWalkable = userIsWalkable ? function(elem){ return elem instanceof Array || userIsWalkable(elem) } : function(elem){ return elem instanceof Array || elem instanceof Browser } // level walk function... var walk = function(i, path, list){ return list // reverse the items... .run(function(){ return reverse ? // NOTE: we .slice() as we do not want to affect // the actual list... this.slice().reverse() : this }) .map(function(elem){ // skip non-iterable items... if(!iterateNonIterable && elem.noniterable){ return [] } var elem_id = elem.id || elem.value // these will be set in the return expression below... var sublist var p // nested browser/list handler... var nested = false var doNested = function(list, j, opts){ // this can be called only once... if(nested !== false){ return nested } // parse args... var args = [...arguments] list = (args[0] === true || args[0] === false || isWalkable(args[0])) ? args.shift() : undefined j = typeof(args[0]) == typeof(123) ? args.shift() : undefined opts = args.shift() || options // normalize... list = list === true ? sublist : (!iterateCollapsed && elem.collapsed) ? [] : (list || sublist) i = j != null ? j : i return ( // request manual iteration... list === false ? [] : list instanceof Array ? walk(i, p, list) // user-defined recursion... : recursion instanceof Function ? recursion.call(that, func, i, p, list, opts) : list[recursion || 'walk'](func, i, p, opts) ) .run(function(){ var res = this instanceof Array ? this : [this] // XXX BUG: this depends on the length // of the result and not on the number // of func calls... // ...i.e. should depend on input // and not on output... i += res.length nested = res.flat() return nested }) } // setup some context... var inline = false // inline browser or array... if(isWalkable(elem)){ inline = true p = path sublist = elem // nested browser / array... } else if(!skipNested && isWalkable(elem.sublist)){ p = path.concat([elem_id]) sublist = elem.sublist // normal element... } else { p = path.concat([elem_id]) sublist = null doNested = null } return ( // prepend nested elements on flat reverse... (sublist && reverse == 'flat' ? doNested(sublist) : []) // append the actual element... .concat( // NOTE: func MUST return an array... func.call(that, // NOTE: for inline elements we do not need to // count the header as it's not present... inline ? i : i++, p, // NOTE: inlined sets have no header... inline ? null : elem, doNested, sublist)) // append nested elements if not done so... .concat((!sublist || nested !== false) ? [] : doNested(sublist)) ) }) // XXX this here loses the length information we need in doNested(..) // to calc indexes... //.flat() } var res = walk(i, path, this.items) return options.root === this ? res.flat() : res }, // Text render... // // This is mainly here for doc/debug purposes... // // XXX rename this?? text: function(options, base){ var that = this var context = (options == null || options.options == null) ? { root: this, // NOTE: we are not combining this with .options as nested // lists can have their own unique sets of options // independently of the root list... options: Object.assign( Object.create(this.options || {}), // defaults... { iterateNonIterable: true }, options || {}), } : options options = context.options base = base || [] var getValue = function(item){ return item.value || item } var items = this .walk( function(i, path, item, nested, sublist){ var indent = base .concat(path) .slice(1) .map(e => ' ') .join('') return item ? indent + getValue(item) : [] }, function(func, i, path, sublist, options){ return sublist.text(context, base.concat(path)) }, options) return context.root === this ? items.join('\n') : items }, // Extended map... // // Get all items... // .map([options]) // -> items // // Map func to items... // .map(func[, options]) // -> items // // // // func(item, index, path, browser) // -> result // // // // options format: // { // // The value used if .reverse is set to true... // // // // NOTE: the default is different from .walk(..) // defaultReverse: 'flat' (default) | 'tree', // // // For other supported options see docs for .walk(..) // ... // } // // // By default this will not iterate items that are: // - non-iterable (item.noniterable is true) // - collapsed sub-items (item.collapsed is true) // // This extends the Array .map(..) by adding: // - ability to run without arguments // - support for options // // // // XXX should we move the defaults to .config??? // XXX make item access by index lazy... // - index nested stuff and lengths... (.sublist_length) // - stop when target reached... (control callback???) // XXX Q: should we have an option to treat groups as elements??? map: function(func, options){ var that = this // parse args... // // NOTE: in addition to the user signatures documented above this // also supports two variants used internally: // .map(func, path[, options]) // .map(func, i, path[, options]) // these set the "base" path and index passed to func... var args = [...arguments] func = (args[0] instanceof Function || args[0] === undefined) ? args.shift() : undefined options = args[args.length-1] || {} options = !(typeof(options) == typeof(123) || options instanceof Array) ? (args.pop() || {}) : {} options = !options.defaultReverse ? Object.assign({}, options, { defaultReverse: 'flat' }) : options return this.walk( function(i, path, elem, doNested){ return elem != null ? [func === undefined ? elem // XXX should this pass the current or the root // container to func??? : func.call(that, elem, i, path, that)] : [] }, function(_, i, path, sublist, options){ // NOTE: this needs to call the actual func that the user // gave us and not the constructed function that we // pass to .walk(..) above... return sublist.map(func, i, path, options) }, ...args, options) }, // XXX EXPERIMENTAL... // // .search(index[, options]) // .search(path[, options]) // .search(func[, options]) // -> item // -> undefined // // // XXX make this a bit smarter and accept an index or a path... // XXX can this replace .get(..) search: function(pattern, options){ var that = this var args = [...arguments] pattern = args.shift() options = args[args.length-1] || {} options = !(typeof(options) == typeof(123) || options instanceof Array) ? (args.pop() || {}) : {} // normalize the test predicate... var func = ( // predicate... pattern instanceof Function ? pattern // path... // XXX BUG: this for some reason matches ['B', '*'] to ['nested', 'moo'] : pattern instanceof Array ? function(elem, i, path){ return path.length > 0 && pattern.length == path.length && (pattern[path.length-1] == '*' || pattern[path.length-1] == path[path.length-1]) } // index... : function(elem, i, path){ return elem && path.length > 0 && i == pattern } ) /* var Stop = new Error('Target found.') var res try { //*/ return this.walk( function(i, path, elem, doNested){ if(elem && func.call(this, elem, i, path)){ //res = elem //throw Stop return [elem] } return [] }, function(_, i, path, sublist, options){ // XXX does not work yet... if(pattern instanceof Array && pattern[path.length-1] != '*' && pattern[path.length-1] != path[path.length-1]){ return [] } return sublist.search(pattern, i, path, options) }, ...args, options) /* XXX need to do this only in the root call... } catch(err){ if(err === Stop){ return res } throw err } //*/ }, // XXX BROKEN... // Sublist map functions... // // NOTE: there are different from .map(..) in that instead of paths // func(..) will get indexes in the current browser... // NOTE: these will return a sparse array... // // XXX this is broken because it does not account for sublists // beyond the 0'th level... sublists: function(func, options){ var that = this options = Object.assign(Object.create(this.options || {}), options || {}) var skipNested = options.skipNested var skipInlined = options.skipInlined var res = [] this.items .forEach(function(elem, i){ if((!skipInlined && elem.value instanceof Browser) || (!skipNested && elem.sublist)){ res[i] = func ? func.call(that, elem, i, that) : elem } }) return res }, nested: function(func){ return this.sublists(func, {skipInlined: true}) }, inlined: function(func){ return this.sublists(func, {skipNested: true}) }, next: function(){}, prev: function(){}, // XXX should there return an array or a .constructor(..) instance?? // XXX should these call respective methods (.forEach(..), .filter(..), // .reduce(..)) on the nested browsers??? forEach: function(func, options){ this.map(...arguments) return this }, filter: function(func, options){ return this.map(function(e, i, p, b){ return func.call(this, e, i, p, b) ? [e] : [] }) .flat() }, reduce: function(){}, // Get item... // // .get() // .get(id) // .get(index) // .get(path) // -> item // -> undefined // // // options format: // { // ignoreKeywords: , // // // rest of the options are the same as for .map(..) // ... // } // // // XXX this is not too fast for indexing very long lists... // XXX use cache for these -- currently these use .map(..)... get: function(key, options){ key = key == null ? 0 : key key = typeof(key) == typeof('str') ? key.split(/[\\\/]/g) .filter(function(e){ return e.length > 0 }) : key key = typeof(key) == typeof('str') ? [key] : key options = Object.assign(Object.create(this.options || {}), options || {}) var iterateCollapsed = options.iterateAll || options.iterateCollapsed var ignoreKeywords = options.ignoreKeywords // keywords... if(!ignoreKeywords){ // XXX don't like how this feels... if(key == 'next' || key == 'prev'){ var reference = this.focused key = key == 'next' ? (reference ? this.indexOf(reference) + 1 : 0) : (reference ? this.indexOf(reference) - 1 : -1) } if(key == 'first'){ var res = this.items[0] return res.value instanceof Browser ? res.value.get(key, options) : res } else if(key == 'last'){ var res = this.items[this.items.length - 1] return res.value instanceof Browser ? res.value.get(key, options) : res.sublist && (!this.collapsed || iterateCollapsed) ? (res.sublist instanceof Browser ? res.sublist.get(key, options) : res.sublist[res.sublist.length - 1]) : res } } // get path... if(key instanceof Array){ var res = this.item_key_index[key.shift()] return key.length == 0 ? res // nested... : iterateCollapsed || !res.collapsed ? res.sublist.get(key, options) : undefined } // get index... // XXX getting an element by index is o(n) and not o(1)... // ...unless we cache .sublists() not sure if this can be // made better in the general case... // XXX do we need to support negative indexes??? var Stop = new Error('.get(..): Result found exception.') var i = 0 // reverse indexing... options = Object.assign( { reverse: key < 0 }, options || {}) key = key < 0 ? -key - 1 : key var res try { this.map(function(e){ res = key == i ? e : res if(res){ throw Stop } i++ }, options) } catch(e){ if(e === Stop){ return res } throw e } return res }, // XXX move these to a more logical spot... // XXX these are almost identical -- reuse??? indexOf: function(item, options){ item = typeof(item) == typeof('str') ? item.split(/[\\\/]/g) : item var Stop = new Error('.indexOf(..): Result found exception.') var i = 0 try{ this.map(function(e, p){ if(item instanceof Array ? item.cmp(p) : (item === e)){ throw Stop } i++ }, options) } catch(e){ if(e === Stop){ return i } } return -1 }, pathOf: function(item, options){ var Stop = new Error('.pathOf(..): Result found exception.') var path var i = 0 try{ this.map(function(e, p){ path = p if(typeof(item) == typeof(123) ? item == i : (item === e)){ throw Stop } i++ }, options) } catch(e){ if(e === Stop){ return path } } return undefined }, // Like .get(.., {iterateCollapsed: true}) but will expand all the // path items to reveal the target... // XXX should this return the item or this??? reveal: function(key, options){ // get the item... var res = this.get(key, Object.assign({iterateCollapsed: true}, options)) // expand the path up... var cur = res.parent while(cur && cur.parent instanceof Browser){ delete (cur.parent.item_key_index[cur.id] || cur.parent.items .filter(function(e){ return e.sublist === cur }) .shift()).collapsed cur = cur.parent } // re-render... this.render() return res }, // // .find(id[, options]) // .find(index[, options]) // .find(path[, options]) // .find(func[, options]) // -> list // // XXX add '**' patterns... // XXX should this return item paths??? // ...one way to do this is to return an object instead of a list... find: function(query, options){ query = typeof(query) == typeof('str') ? query.split(/[\\\/]/g) .filter(function(e){ return e.length > 0 }) : query query = typeof(query) == typeof('str') ? [query] : query query = query instanceof Array ? query .map(function(d){ return d == '*' ? d : d.indexOf('*') >= 0 ? new RegExp(d .replace(/\*/g, '.*')) : d}) : query var i = -1 return this .filter(function(e, p){ i++ return (query === e || ( // index... typeof(query) == typeof(123) ? query == i // predicate... : query instanceof Function ? // XXX revise signature... query.call(this, e, p, i, this) // regular expression... : query instanceof RegExp ? query.test(p.join('/')) // direct path comparison... : query instanceof Array ? query.cmp(p) || (query.length == p.length && query .filter(function(q, i){ return q == '*' || (q instanceof RegExp && q.test(p[i])) || q == p[i] }) .length == p.length) : false)) }, options) }, // XXX support: up/down/left/right/first/last/next/prev // XXX extend support for screen oriented nav in a subclass... navigate: function(direction){ // XXX get then return element... }, // XXX do we need edit ability here? // i.e. .set(..), .remove(..), .sort(..), ... // ...if we are going to implement editing then we'll need to // callback the user code or update the user state... // Make .items and .item_key_index... // // .make() // .make(options) // -> this // // The items are constructed by passing a make function to .__list__(..) // which in turn will call this make(..) per item created. // // For more doc on item construction see: .__init__(..) // // // NOTE: each call to this will reset both .items and .item_key_index // NOTE: for items with repeating values there is no way to correctly // identify an item thus no state is maintained between .make(..) // calls for such items... // // XXX revise options handling for .__list__(..) make: function(options){ options = Object.assign(Object.create(this.options || {}), options || {}) var items = this.items = [] var old_index = this.__item_index || {} var new_index = this.__item_index = {} // item constructor... // // Make an item... // make(value[, options]) // make(value, func[, options]) // -> make // // Inline a browser instance... // make(browser) // -> make // // // NOTE: when inlining a browser, options are ignored. // NOTE: when inlining a browser it's .parent will be set this // reusing the inlined object browser may mess up this // property... // // XXX problem: make(Browser(..), ..) and make.group(...) produce // different formats -- the first stores {value: browser, ...} // while the latter stores a list of items. // ...would be more logical to store the object (i.e. browser/list) // directly as the element... var make_called = false var make = function(value, opts){ make_called = true // special-case: inlined browser... // // NOTE: we ignore opts here... // XXX not sure if this is the right way to go... // ...for removal just remove the if statement and its // first branch... if(value instanceof Browser){ var item = value item.parent = this // normal item... } else { var args = [...arguments] opts = opts || {} // handle: make(.., func, ..) opts = opts instanceof Function ? {open: opts} : opts // handle trailing options... opts = args.length > 2 ? Object.assign({}, args.pop(), opts) : opts opts = Object.assign( {}, opts, {value: value}) // item id... var key = this.__key__(opts) var id_changed = (old_index[key] || {}).id_changed // handle duplicate ids -> err if found... if(opts.id && opts.id in new_index){ throw new Error(`make(..): duplicate id "${key}": ` +`can't create multiple items with the same key.`) } // handle duplicate keys... // NOTE: we can't reuse an old copy when re-making the list // because there is now way to correctly identify an // object when it's id is tweaked (and we can not rely // on item order)... // ...for this reason all "persistent" state for such // an element will be lost when calling .make(..) again // and re-making the list... // a solution to this would be to manually assign an .id // to such elements in .__list__(..)... // XXX can we go around this without requiring the user // to manage ids??? var k = key while(k in new_index){ // duplicate keys disabled... if(options.noDuplicateValues){ throw new Error(`make(..): duplicate key "${key}": ` +`can't create multiple items with the same key.`) } // mark both the current and the first items as id-mutated... opts.id_changed = true new_index[key].id_changed = true // create a new key... k = this.__id__(key) } key = opts.id = k // build the item... var item = Object.assign( Object.create(options || {}), // get the old item values (only for non duplicate items)... id_changed ? {} : old_index[key] || {}, // XXX inherit from this... opts, { parent: this, }) // XXX do we need both this and the above ref??? item.sublist instanceof Browser && (item.sublist.parent = this) } // store the item... items.push(item) new_index[key] = item return make }.bind(this) make.__proto__ = Items make.dialog = this make.items = items //var res = this.__list__(make) // XXX not sure about this -- options handling... var res = this.__list__(make, options ? Object.assign( Object.create(this.options || {}), options || {}) : null) // if make was not called use the .__list__(..) return value... this.items = make_called ? this.items : res return this }, // Renderers... // // .renderFinalize(items, context) // .renderList(items, context) // .renderNested(header, sublist, item, context) // .renderNestedHeader(item, i, context) // .renderItem(item, i, context) // .renderGroup(items, context) // // renderFinalize: function(items, context){ return this.renderList(items, context) }, renderList: function(items, context){ return items }, // NOTE: to skip rendering an item/list return null... // XXX should this take an empty sublist??? // ...this would make it simpler to expand/collapse without // re-rendering the whole list... renderNested: function(header, sublist, item, context){ return header ? this.renderGroup([ header, sublist, ]) : sublist }, renderNestedHeader: function(item, i, context){ return this.renderItem(item, i, context) }, // NOTE: to skip rendering an item/list return null... renderItem: function(item, i, context){ return item }, renderGroup: function(items, context){ return items }, // Render state... // // .render() // .render(options[, renderer]) // .render(context[, renderer]) // -> state // // // context format: // { // root: , // options: , // } // // // options: // { // nonFinalized: , // // // for more supported options see: .walk(..) // ... // } // // // NOTE: it is not recommended to extend this. all the responsibility // of actual rendering should lay on the renderer methods... // NOTE: calling this will re-render the existing state. to re-make // the state anew that use .update(..)... // NOTE: currently options and context are distinguished only via // the .options attribute... // // XXX would be nice to add ability to do a full render but not // finalize the result... render: function(options, renderer){ var that = this // XXX Q: should options and context be distinguished only via // the .options attr as is the case now??? // ...see no reason why not, though it does not feel right... var context = (options == null || options.options == null) ? { root: this, // NOTE: we are not combining this with .options as nested // lists can have their own unique sets of options // independently of the root list... //options: options || this.options || {}, options: Object.assign( Object.create(this.options || {}), // defaults... // XXX is this the correct way to setup defaults??? { iterateNonIterable: true, }, options || {}), } : options options = context.options renderer = renderer || this // XXX should we control render parameters (range, start, end, ...) // from outside render and pass this info down to nested lists??? // ...if yes how?? // - options // - arg threading var items = this .walk( function(i, path, item, nested, sublist){ return ( // inline... (item == null && sublist) ? // NOTE: here we are forcing rendering of the // inline browser/list, i.e. ignoring // options.skipNested for inline stuff... // NOTE: we here do not distinguish between // inlined lists and browsers... (XXX ???) [ renderer.renderGroup(nested(true), context) ] // nested... : sublist ? [ renderer.renderNested( renderer.renderNestedHeader(item, i, context), nested(), item, context) ] // normal item... : [ renderer.renderItem(item, i, context) ] ) }, function(func, i, path, sublist, options){ return sublist.render(context, renderer, i, path) }, // make the element render less strict... function(elem){ return elem && elem.render instanceof Function }, // pass on i and path if given... ...[...arguments].slice(2), options) // determine the render mode... return (!options.nonFinalized && context.root === this) ? // root context -> render list and return this... renderer.renderFinalize(items, context) // nested context -> return item list... : items }, // Update state (make then render)... // // .update() // -> state // update: function(options){ return this .make(options) .render(options) }, // XXX should these be moved to the HTML class... // Events... // // Format: // { // // XXX add tagged event support... // : [ // , // ... // ], // ... // } // // XXX __event_handlers: null, // generic event infrastructure... // XXX add support for tagged events... // XXX should these be defined on this level or should we use DOM??? // XXX add support for item events... // e.g. item.focus(..) -> root.focus(..) // XXX also need to design a means for this system to interact both // ways with DOM events... // XXX need to bubble the event up through the nested browsers... on: function(evt, handler){ var handlers = this.__event_handlers = this.__event_handlers || {} handlers = handlers[evt] = handlers[evt] || [] handlers.push(handler) return this }, one: function(evt, handler){ var func = function(...args){ handler.call(this, ...args) this.off(evt, func) } this.on(evt, func) return this }, off: function(evt, handler){ // remove all handlers if(handler == '*' || handler == 'all'){ delete (this.__event_handlers || {})[evt] // remove only the specific handler... } else { var handlers = (this.__event_handlers || {})[evt] || [] do{ var i = handlers.indexOf(handler) i > -1 && handlers.splice(i, 1) } while(i > -1) } return this }, trigger: function(evt, ...args){ var that = this var stopPropagation = false var evt = typeof(evt) == typeof('str') ? // XXX construct this in one place... // ...currently it is constructed here and in makeEventMethod(..) { name: evt, stopPropagation: function(){ stopPropagation = true }, } : evt // call the main set of handlers... ;((this.__event_handlers || {})[evt.name] || []) // prevent .off(..) from affecting the call loop... .slice() .forEach(function(handler){ handler.call(that, evt, ...args) }) // trigger the parent's event... !stopPropagation && this.parent && this.parent.trigger(evt, ...args) return this }, // domain events/actions... // XXX need a way to extend these to: // - be able to trigger an external (DOM) event... // - be able to be triggered from an external (DOM) event... focus: makeItemEventMethod('focus', function(evt, items){ // NOTE: if we got multiple matches we care only about the last one... var item = items.pop() if(!item){ return } // blur .focused... this.focused && this.blur(this.focused) item.focused = true }), blur: makeItemEventMethod('blur', function(evt, items){ items.forEach(function(item){ delete item.focused }) }), // XXX update this.selected in a more granular way... select: makeItemEventMethod('select', function(evt, items){ items.forEach(function(item){ item.selected = true // XXX update this.selected in a more granular way... delete this.__selected }) }), deselect: makeItemEventMethod('deselect', function(evt, item){ items.forEach(function(item){ delete item.selected // XXX update this.selected in a more granular way... delete this.__selected }) }), open: makeItemEventMethod('open', function(evt, item){}), enter: makeItemEventMethod('enter', function(evt, item){}), // XXX can/should we unify these??? collapse: makeItemEventMethod('collapse', function(evt, item){}), expand: makeItemEventMethod('expand', function(evt, item){}), // XXX target can be item or path... load: makeEventMethod('load', function(evt, item){}), close: makeEventMethod('close', function(evt, reason){}), // XXX should we update on on init.... __init__: function(func, options){ this.__list__ = func this.options = Object.assign( {}, this.options || {}, options || {}) // XXX should this be here or should this be optional??? //this.update() }, } var BaseBrowser = module.BaseBrowser = object.makeConstructor('BaseBrowser', BaseBrowserClassPrototype, BaseBrowserPrototype) //--------------------------------------------------------------------- var BrowserClassPrototype = { __proto__: BaseBrowser, } // XXX TODO: // - need a way to update some stuff on .update() / .make() -- a way // to selectively merge the old state with the new... // - event handler signature -- pass the item + optionally render... // - keyboard handling... // XXX render of nested lists does not affect the parent list(s)... // ...need to render lists and items both as a whole or independently... // XXX should this use vanilla DOM or jQuery??? // XXX add a left button type/option -- expand/collapse and friends... var BrowserPrototype = { __proto__: BaseBrowser.prototype, options: { hideListHeader: false, renderHidden: false, localEvents: [ // XXX STUB??? 'click', // XXX keyboard stuff... // XXX // XXX custom events... // XXX ], //buttonLocalEvents: [ //], // Format: // [ // ['html', ], // ... // ] itemButtons: [ ], // XXX need to mix these into the header only... headerItemButtons: [ ], // Shorthand elements... // // Format: // { // : { // class: , // html: , // }, // ... // } // // If make(..) gets passed it will construct and element // via with an optional // // NOTE: .class is optional... // NOTE: set this to null to disable shorthands... // NOTE: currently the options in the template will override // anything explicitly given by item options... (XXX revise) elementShorthand: { '---': { 'class': 'separator', 'html': '
', noniterable: true, }, '...': { 'class': 'separator', 'html': '
', noniterable: true, }, }, }, // parent element (optional)... // XXX rename??? // ... should this be .containerDom or .parentDom??? get container(){ return this.__container || (this.__dom ? this.__dom.parentElement : undefined) }, set container(value){ var dom = this.dom this.__container = value // transfer the dom to the new parent... dom && (this.dom = dom) }, // browser dom... get dom(){ return this.__dom }, set dom(value){ this.container && (this.__dom ? this.container.replaceChild(value, this.__dom) : this.container.appendChild(value)) this.__dom = value }, // Element renderers... // // This does tow additional things: // - save the rendered state to .dom // - wrap a list of nodes (nested list) in a div // // Format: // if list of items passed: //
// // ... //
// or same as .renderList(..) // // XXX revise... renderFinalize: function(items, context){ var d = this.renderList(items, context) // wrap the list (nested list) of nodes in a div... if(d instanceof Array){ var c = document.createElement('div') d.forEach(function(e){ c.appendChild(e) }) d = c } this.dom = d return this.dom }, // // Foramt: //
// // ... // // //
// // ... //
//
// // XXX instrument interactions... // XXX register event handlers... renderList: function(items, context){ var that = this var options = context.options || this.options // dialog (container)... var dialog = document.createElement('div') dialog.classList.add('browse-widget') dialog.setAttribute('tabindex', '0') // header... options.hideListHeader || dialog.appendChild(this.renderListHeader(context)) // list... var list = document.createElement('div') list.classList.add('list', 'v-block') items .forEach(function(item){ list.appendChild(item instanceof Array ? that.renderGroup(item) : item) }) dialog.appendChild(list) // XXX event handlers... // XXX return dialog }, // // Foramt: //
//
dir
// ... //
dir
//
// // XXX populate this... // XXX make this an item??? renderListHeader: function(context){ var header = document.createElement('div') header.classList.add('path', 'v-block') // XXX path/search... var dir = document.createElement('div') dir.classList.add('dir', 'cur') dir.setAttribute('tabindex', '0') header.appendChild(dir) return header }, // // Format: //
// // ... // // // ... //
// // XXX register event handlers... renderNested: function(header, sublist, item, context){ var that = this var options = context.options || this.options // container... var e = document.createElement('div') e.classList.add('list') // localize events... var stopPropagation = function(evt){ evt.stopPropagation() } ;(options.localEvents || []) .forEach(function(evt){ e.addEventListener(evt, stopPropagation) }) // header... header && e.appendChild(header) // items... sublist instanceof Node ? e.appendChild(sublist) // XXX should this add the items to a container??? : sublist instanceof Array ? sublist .forEach(function(item){ e.appendChild(item) }) : null // XXX event handlers... (???) // XXX item.dom = e return e }, // NOTE: this is the similar to .renderItem(..) // XXX make collapse action overloadable.... renderNestedHeader: function(item, i, context){ var that = this return this.renderItem(item, i, context) // update dom... .run(function(){ // class... // XXX should be done here or in the config??? this.classList.add('sub-list-header', 'traversable') item.collapsed && this.classList.add('collapsed') // collapse action handler... // XXX make this overloadable... $(this).on('open', function(evt){ item.collapsed = !item.collapsed that.render(context) }) }) }, // // Format: //
// .. //
// // XXX this does not seem to get called by .render(..)... renderGroup: function(items, context){ var e = document.createElement('div') e.classList.add('group') items // XXX is this wrong??? .flat(Infinity) .forEach(function(item){ e.appendChild(item) }) return e }, // // Format: //
// //
value_a
//
value_b
// ... // // //
button_a_html
//
button_b_html
// ... //
// // XXX add custom events: // - open // - select // - update renderItem: function(item, i, context){ var options = context.options || this.options if(options.hidden && !options.renderHidden){ return null } // special-case: item shorthands... if(item.value in options.elementShorthand){ // XXX need to merge and not overwrite -- revise... Object.assign(item, options.elementShorthand[item.value]) // NOTE: this is a bit of a cheat, but it saves us from either // parsing or restricting the format... var elem = item.dom = $(item.html)[0] elem.classList.add( ...(item['class'] instanceof Array ? item['class'] : item['class'].split(/\s+/g))) return elem } // Base DOM... var elem = document.createElement('div') var text = this.__value2key__(item.value || item) // classes... elem.classList.add(...['item'] // user classes... .concat(item['class'] || item.cls || []) // special classes... .concat([ 'selected', 'disabled', 'hidden', ].filter(function(cls){ return !!item[cls] }))) // attrs... item.disabled || elem.setAttribute('tabindex', '0') Object.entries(item.attrs || {}) .forEach(function([key, value]){ elem.setAttribute(key, value) }) elem.setAttribute('value', text) // values... text && (item.value instanceof Array ? item.value : [item.value]) // XXX handle $keys and other stuff... .map(function(v){ var value = document.createElement('span') value.classList.add('text') value.innerHTML = v || item || '' elem.appendChild(value) }) // events... // XXX revise signature... elem.addEventListener('click', function(){ $(elem).trigger('open', [text, item, elem]) }) //elem.addEventListener('tap', function(){ $(elem).trigger('open', [text, item, elem]) }) Object.entries(item.events || {}) // shorthand events... .concat([ 'click', ].map(function(evt){ return [evt, item[evt]] })) // setup the handlers... .forEach(function([evt, handler]){ handler && elem.addEventListener(evt, handler) }) // buttons... // XXX migrate the default buttons functionality and button inheritance... var buttons = (item.buttons || options.itemButtons || []) .slice() // NOTE: keep the order unsurprising... .reverse() var stopPropagation = function(evt){ evt.stopPropagation() } buttons .forEach(function([html, handler]){ var button = document.createElement('div') button.classList.add('button') button.innerHTML = html if(!item.disabled){ button.setAttribute('tabindex', '0') ;(options.buttonLocalEvents || options.localEvents || []) .forEach(function(evt){ button.addEventListener(evt, stopPropagation) }) handler && button.addEventListener('click', handler) } elem.appendChild(button) }) item.dom = elem return elem }, // Custom events... // XXX do we use jQuery event handling or vanilla? // ...feels like jQuery here wins as it provides a far simpler // API + it's a not time critical area... // ....another idea is to force the user to use the provided API // by not implementing ANY direct functionality in DOM -- I do // not like this idea at this point as it violates POLS... //open: function(func){}, //filter: function(){}, //select: function(){}, //get: function(){}, //focus: function(){}, // Navigation... // up: function(){}, down: function(){}, left: function(){}, right: function(){}, next: function(){}, prev: function(){}, collapse: function(){}, // XXX scroll... } // XXX should this be a Widget too??? var Browser = module.Browser = object.makeConstructor('Browser', BrowserClassPrototype, BrowserPrototype) //--------------------------------------------------------------------- // Text tree renderer... // // This is mainly designed for testing. // // XXX Q: how should the header item and it's sub-list be linked??? var TextBrowserClassPrototype = { __proto__: BaseBrowser, } var TextBrowserPrototype = { __proto__: BaseBrowser.prototype, options: { valueSeparator: ' ', renderIndent: '\t', }, // NOTE: we do not need .renderGroup(..) here as a group is not // visible in text... renderList: function(items, options){ var that = this return this.renderNested(null, items, null, null, options) .join('\n') }, renderItem: function(item, i, options){ var value = item.value || item value = value instanceof Array ? value.join(this.options.valueSeparator || ' ') : value return item.current ? `[ ${value} ]` : value }, renderNested: function(header, sublist, context, item, options){ var that = this var nested = sublist && sublist .flat() .map(function(e){ return e instanceof Array ? e.map(function(e){ return (that.options.renderIndent || ' ') + e }) : e }) .flat() return ( // expanded... header && nested ? [ '- ' + header, nested, ] // collapsed... : header ? [ '+ ' + header ] // headerless... : nested )}, } var TextBrowser = module.TextBrowser = object.makeConstructor('TextBrowser', TextBrowserClassPrototype, TextBrowserPrototype) /********************************************************************** * vim:set ts=4 sw=4 : */ return module })