/********************************************************************** * * * **********************************************************************/ ((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') // XXX //var walk = require('lib/walk') var walk = require('../../node_modules/generic-walk/walk').walk /*********************************************************************/ // 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.children = 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(){} //--------------------------------------------------------------------- // Event system... // // XXX might be a good idea to make this a generic module... // Generate an event method... // // Trigger an event // .event() // .event(arg, ..) // -> this // // Bind an event handler... // .event(func) // -> this // // // XXX make the event object customizable... // XXX STUB event object... var makeEventMethod = function(event, handler){ return Object.assign( function(item){ // register handler... if(item instanceof Function){ return this.on(event, item) } // XXX STUB: 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(){ //}, } handler && handler.call(this, evt, ...arguments) return this .trigger(evt, ...arguments) }, { event: event, }) } // Call item event handlers... // // callItemEventHandlers(item, event_name, event_object, ...) // -> null // // XXX should this call item.parent.trigger(..) ??? var callItemEventHandlers = function(item, event, evt, ...args){ ;(item[event] ? [item[event]] : []) .concat((item.events || {})[event] || []) .forEach(function(handler){ // XXX revise call signature... handler.call(item, evt, item, ...args) }) } // Generate item event method... // // This extends makeEventMethod(..) by adding an option to pass an item // when triggering the event, the rest of the signature is identical. // // Trigger an event on item(s)... // .event(item, ..) // .event([item, ..], ..) // -> this // // NOTE: item is compatible to .search(item, ..) spec, see that for more // details... // // XXX need to trigger item parent's event too... // Q: trigger once, per call or once per item??? // Q: should this be done here or in the client??? // // XXX problems with event propagation: // - the item can be within a nested browser/list and that // container's events will not be triggered from here, // just the item's, this and parent's... // - if we get item.parent and trigger it's event directly // then that event may propogate up and this' and this.parent's // events may get triggered more than once... // - we can not rely on full bottom/up propagation as some // containers in the path may be basic Arrays... // - need to make this work with .trigger(..) for non-item events // i.e. currently .trigger(..) propagates the event to parent and // this may conflict with us triggering events on the path... // XXX one idea to do event propagation is to use the actual .search(..) // mechanic to handle each found item and collect their direct // parents... // we need: // - trigger each item event // - propagate the event through the path for each item // - trigger each parent only once, passing it a list of only // relevant items (i.e. items in its sub-tree only) // - handle .stopPropagation(..) correnctly // - stop propagation up but finish the level??? // - trigger the root.parent's event when done // XXX Q: do we need item event grouping??? // ...e.g. should .select('*') trigger a 'select' on each item and // then on groups of contained items per container, or should it be // triggered once per item per relevant container??? // ....considering that I see no obvious way to implement grouping // up the tree in a recursive manner I'm starting to think that we // should go the simple route and trigger 1:1 from each leaf and up... // NOTE: this approach would mean that .trigger(..) itself would NOT // call any handlers on the current level, it would just propagate // the event down, and the handlers would get called on the way // back up (unless .stopPropagation(..) is called) // NOTE: this would also make item and container events behave in // a different manner: // - container event calls handlers here and propagates up // - item event propagates down then triggers container events up // XXX how do we distinguish these (down/up) events??? // ...different events, event state/mode, ...??? var makeItemEventMethod = function(event, handler, options){ options = Object.assign( // NOTE: we need to be able to pass item objects, so we can not // use queries at the same time as there is not way to // distinguish one from the other... { noQueryCheck: true }, options || {}) // base event method... // NOTE: this is not returned directly as we need to query the items // and pass those on to the handlers rather than the arguments // as-is... var base = makeEventMethod(event, function(evt, item, ...args){ handler && handler.call(this, evt, item.slice(), ...args) item.forEach(function(item){ callItemEventHandlers(item, event, evt, ...args) }) }) return Object.assign( // the actual method we return... function(item, ...args){ var that = this return base.call(this, // event handler... item instanceof Function ? item // array of queries... : item instanceof Array ? item .map(function(e){ return that.search(e, options) }) .flat() .unique() // explicit item or query... : item != null ? this.search(item, options) : [], ...args) }, // get base method attributes -- keep the event method format... base) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 .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.index.A.children.index.B.children... // would be nice to be closer to: // this.A.B... __item_index: null, get index(){ this.__item_index || this.make() return this.__item_index }, // XXX should this exist??? set index(value){ this.__item_index = value }, // XXX should we cache the value here???? get focused(){ return this.get('focused') }, set focused(value){ this.focus(value) }, // XXX should we cache the value here???? get selected(){ return this.search('selected') }, set selected(value){ this.deselect('selected') this.select(value) }, // Length... // // visible only... get length(){ return this.map().length }, // include collapsed elements... get lengthTree(){ return this.map({iterateCollapsed: true}).length }, // include non-iterable elements... get lengthAll(){ return this.map({iterateAll: true}).length }, // 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 // .index (keyed via .id or JSONified .value) // // Each of the above structures is reset on each call to .make(..) // // options format: // { // id: , // value: | , // // children: | , // // 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... // // Get list of nodes in tree... // .walk() // -> list // // Walk the tree passing each node to func(..) // .walk(func(..)[, options]) // -> list // // Walk tree passing each node to func(..) using method name to // walk nested browsers... // NOTE: 'walk' is used as name if name is not present in the object... // .walk(func(..), name, args(..)[, options]) // .walk(func(..), name, args(..), walkable(..)[, options]) // -> list // // Walk tree passign each node to func(..) and handle nested browser // walking in recursion(..) optionally testing if walkable with walkable(..) // .walk(func(..), recursion(..)[, options]) // -> list // // // Item handler... // func(node, index, path, next(..), stop(..), children) // -> list // // Trigger next/nested item handling... // next(children) // -> list // // Stop handling... // stop(result) // // // Handle walkable node children (recursively)... // recursion(children, index, path, options, context, func(..), stop(..), walk()) // -> list // // // Prepare arguments for call of name function on nested browser... // args(list, index, path, options, context, func(..), stop(..)) // -> list // // // Test if node is walkable... // walkable(node) // -> bool // // // For examples see: .text(..), .paths(..) and .map(..) // // // 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.children of items with .collapsed // // set to true... // iterateCollapsed: , // // If true skip iterating nested items... // skipNested: , // // // XXX not yet supported... // skipInlined: , // // // 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: , // } // // // NOTE: if recursion(..) is not given then .walk(..) is used to // handle all the nested elements (children)... // NOTE: if walkable(..) is not given then we check for .walk(..) // availability... // NOTE: children arrays are handled internally... // // // XXX which of the forms should be documented in the signature??? // NOTE: it does not matter which is used as we manually // parse arguments... // XXX passing both index directly and context containing index // (context.index) feels excessive... // + this is done so as to provide the user a simpler // .map(..)-like form... // Ex: // .walk((e, i, p, next, stop) => p.join('/')) // // vs. // .walk((e, c, next, stop) => c.path.join('/')) // - two ways to get index and one to update it... // ...if this can produce errors we need to simplify... // XXX add docs: // - maintaining context to implement/extend walkers... // - correctly stopping recursive calls (call root stop(..)) // XXX can this be simpler??? walk: function(func, recursion, walkable, options){ var that = this // parse args... var args = [...arguments] func = (args[0] instanceof Function || args[0] == null) ? args.shift() : undefined var recursion = (args[0] instanceof Function || typeof(args[0]) == typeof('str') || args[0] == null) ? args.shift() : undefined var formArgs = (typeof(recursion) == typeof('str') && args[0] instanceof Function) ? args.shift() : null // sanity check... if(formArgs == null && typeof(recursion) == typeof('str')){ throw new Error(`.walk(func, name, formArgs, ..): ` +`expected function as third argument, got: ${formArgs}.`) } var walkable = (!formArgs && (args[0] instanceof Function || args[0] == null)) ? args.shift() : null options = args.shift() || {} // get/build context... var context = args.shift() context = context instanceof Array ? { path: context } : (context || {}) context.root = context.root || this context.index = context.index || 0 // options specifics... var iterateNonIterable = options.iterateAll || options.iterateNonIterable var iterateCollapsed = options.iterateAll || options.iterateCollapsed var skipNested = !options.iterateAll && options.skipNested var skipInlined = !options.iterateAll && options.skipInlined var reverse = options.reverse === true ? (options.defaultReverse || 'tree') : options.reverse var isWalkable = walkable ? function(node){ return node instanceof Array || walkable(node) } : function(node){ return node && (node instanceof Array // requested method name is available... || (typeof(recursion) == typeof('str') && node[recursion]) || node.walk ) } return walk( function(state, node, next, stop){ // keep only the root stop(..) -> stop the entire call tree... stop = context.stop = context.stop || stop // skip non-iterable items... if(!iterateNonIterable && node.noniterable){ return state } var nested = false var doNested = function(list){ // this can be called only once -> return cached results... if(nested !== false){ return nested } // calling this on a node without .children is a no-op... if(children == null){ return [] } // normalize... list = list === true ? children : (!iterateCollapsed && node.collapsed) ? [] : list == null ? children : list // call .walk(..) recursively... var useWalk = function(){ return list.walk( func, recursion, ...(formArgs instanceof Function ? [formArgs] : [walkable]), options, context) } return (list === false ? [] // handle arrays internally... : list instanceof Array ? // NOTE: this gets the path and i from context... next('do', [], ...(reverse ? list.slice().reverse() : list)) // user-defined recursion... : recursion instanceof Function ? recursion.call(that, list, context.index, p, options, context, func, useWalk) // method with arg forming... : formArgs instanceof Function && list[recursion] ? list[recursion]( ...(formArgs( list, context.index, p, options, context, func, useWalk) || [])) // .walk(..) : useWalk()) // normalize and merge to state... .run(function(){ return (nested = this instanceof Array ? this : [this]) }) } // prepare context... var id = node.id || node.value var path = context.path = context.path || [] var [inline, p, children] = // inline... isWalkable(node) ? [true, path, node] // nested... : (!skipNested && isWalkable(node.children)) ? [false, // update context for nested items... path.push(id) && path, node.children] // leaf... : [false, path.concat([id]), undefined] if(inline && skipInlined){ return state } // go through the elements... state.splice(state.length, 0, ...[ // reverse -> do children... reverse == 'flat' && children && doNested() || [], // do element... func ? (func.call(that, ...(inline ? [null, context.index] : [node, context.index++]), p, // NOTE: when calling this it is the // responsibility of the caller to return // the result to be added to state... doNested, stop, children) || []) : [node], // normal order -> do children... children && nested === false && doNested() || [], ].flat()) // restore path context... children && context.path.pop() return state }, [], // input items... ...(reverse ? this.items .slice() .reverse() : this.items)) }, // Test/Example Text renders... // // Recursively render the browser as text tree... // ._test_texttree(..) // -> string // // Recursively render the browser as text tree with manual nesting... // ._test_texttree_manual(..) // -> string // // Build a nested object tree from the browser... // ._test_tree(..) // -> object // _test_texttree: function(options, context){ // NOTE: here we do not care about the topology (other than path // depth) and just handle items... return this .walk( function(node, i, path){ return node ? path.slice(1) .map(e => ' ') .join('') + (node.value || node) : [] }, '_test_texttree', function(func, i, path, options, context){ return [options, context] }, options, context) .join('\n') }, _test_texttree_manual: function(options, context){ // NOTE: here we do basic topology -- append children to their // respective node... return this .walk( function(node, i, path, next){ return node == null ? [] // make a node... : [path.slice(1) .map(e => ' ') .join('') + (node.value || node)] // append child nodes if present... .concat(node.children ? next() : []) }, '_test_texttree_manual', function(func, i, path, options, context){ return [options, context] }, options, context) .join('\n') }, _test_tree: function(options, context){ var toObject = function(res, e){ if(e == null || e[0] == null){ return res } res[e[0]] = e[1] instanceof Array ? // handle nested arrays... // NOTE: these did not get through the .reduce(..) below // as they are simple arrays that do not implement // either .walk(..) or ._test_tree(..) e.slice(1).reduce(toObject, {}) : e[1] return res } return this // build [key, children] pairs... .walk( function(node, i, path, next){ return node == null ? [] // make a node... : [[(node.value || node)] // append child nodes if present... .concat(node.children ? next() : null) ] }, '_test_tree', function(func, i, path, options, context){ return [options, context] }, options, context) // construct the object... .reduce(toObject, {}) }, paths: function(options, context){ return this.walk( function(n, i, p){ return n && [(options || {}).joinPaths !== false ? p.join('/') : p] }, 'paths', function(_, i, path, options, context){ // NOTE: for paths and indexes to be consistent between // levels we need to thread the context on, here and // into the base .walk(..) call below... return [options, context] }, options, context) }, // 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 Q: should we have an option to treat groups as elements??? map: function(func, options){ var that = this // parse args... var args = [...arguments] func = (args[0] instanceof Function || args[0] === undefined) ? args.shift() : undefined options = args.shift() || {} options = !options.defaultReverse ? Object.assign({}, options, { defaultReverse: 'flat' }) : options var context = args.shift() return this.walk( function(elem, i, path){ 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)] : [] }, 'map', function(_, i, p, options, context){ return [func, options, context] }, options, context) }, // // Get list of matching elements... // NOTE: this is similar to .filter(..) // .search(test[, options]) // -> items // // Map func to list of matching elements and return results... // NOTE: this is similar to .filter(..).map(func) // .search(test, func[, options]) // -> items // // // test can be: // predicate(..) - function returning true or false // index - element index // NOTE: index can be positive or negative to // access items from the end. // path - array of path elements or '*' (matches any element) // regexp - regexp object to test item path // query - object to test against the element // keyword - // // // predicate(elem, i, path) // -> bool // // // query format: // { // // match if exists and is true... // // XXX revise... // : true, // // // match if does not exist or is false... // // XXX revise... // : false, // // // match if equals value... // : , // // // match if func() return true... // : , // // ... // } // // // supported keywords: // 'first' - get first item (same as 0) // 'last' - get last item (same as -1) // 'selected' - get selected items (shorthand to {selected: true}) // 'focused' - get focused items (shorthand to {focused: true}) // // // options format: // { // noIdentityCheck: , // // noQueryCheck: , // // ... // } // // // // __search_test_generators__ format: // { // // NOTE: generator order is significant as patterns are testen // // in order the generators are defined... // // NOTE: testGenerator(..) is called in the context of // // __search_test_generators__ (XXX ???) // // NOTE: is only used for documentation... // : testGenerator(..), // // ... // } // // testGenerator(pattern) // -> test(elem, i, path) // -> false // // // XXX add support for 'next'/'prev', ... keywords... (here or in .get(..)???) // XXX add support for fuzzy match search -- match substring by default // and exact title if using quotes... // XXX do we actually need to stop this as soon as we find something, // i.e. options.firstOnly??? // XXX add diff support... __search_test_generators__: { // regexp path test... regexp: function(pattern){ return pattern instanceof RegExp && function(elem, i, path){ return pattern.test(elem.value) || pattern.test('/'+ path.join('/')) } }, // string path test... // XXX should 'B' be equivalent to '/B' or should it be more like '**/B'? strPath: function(pattern){ if(typeof(pattern) == typeof('str')){ pattern = pattern instanceof Array ? pattern : pattern .split(/[\\\/]/g) .filter(function(e){ return e.trim().length > 0 }) return this.path(pattern) } return false }, // path test... // NOTE: this does not go down branches that do not match the path... path: function(pattern){ if(pattern instanceof Array){ // XXX add support for '**' ??? var cmp = function(a, b){ return a.length == b.length && !a .reduce(function(res, e, i){ return res || !( e == '*' || (e instanceof RegExp && e.test(b[i])) || e == b[i]) }, false) } var onPath = function(path){ return pattern.length >= path.length && cmp( pattern.slice(0, path.length), path) } return function(elem, i, path, next){ // do not go down branches beyond pattern length or // ones that are not on path... ;(pattern.length == path.length || !onPath(path)) && next(false) // do the test... return path.length > 0 && pattern.length == path.length && cmp(pattern, path) } } return false }, // item index test... index: function(pattern){ return typeof(pattern) == typeof(123) && function(elem, i, path){ return i == pattern } }, // XXX add diff support... // object query.. // NOTE: this must be last as it will return a test unconditionally... query: function(pattern){ var that = this return function(elem){ return Object.entries(pattern) .reduce(function(res, [key, pattern]){ return res && (elem[key] == pattern // bool... || ((pattern === true || pattern === false) && pattern === !!elem[key]) // predicate... || (pattern instanceof Function && pattern.call(that, elem[key])) // regexp... || (pattern instanceof RegExp && pattern.test(elem[key])) // type... // XXX problem, we can't distinguish this // and a predicate... // ...so for now use: // .search(v => v instanceof Array) //|| (typeof(pattern) == typeof({}) // && pattern instanceof Function // && elem[key] instanceof pattern) ) }, true) } }, }, search: function(pattern, func, options){ var that = this // parse args... var args = [...arguments] pattern = args.length == 0 ? true : args.shift() func = (args[0] instanceof Function || args[0] === undefined) ? args.shift() : undefined options = args.shift() || {} var context = args.shift() // pattern -- normalize and do pattern keywords... pattern = options.ignoreKeywords ? pattern : typeof(pattern) == typeof('str') ? ((pattern === 'all' || pattern == '*') ? true : pattern == 'first' ? 0 : pattern == 'last' ? -1 : pattern == 'selected' ? {selected: true} : pattern == 'focused' ? {focused: true} : pattern) : pattern // normalize negative index... if(typeof(pattern) == typeof(123) && pattern < 0){ pattern = -pattern - 1 options.reverse = 'flat' } // normalize/build the test predicate... var test = ( // all... pattern === true ? pattern // predicate... : pattern instanceof Function ? pattern // other -> get a compatible test function... : Object.entries(this.__search_test_generators__) .filter(function([key, _]){ return !(options.noQueryCheck && key == 'query') }) .reduce(function(res, [_, get]){ return res || get.call(that.__search_test_generators__, pattern) }, false) ) return this.walk( function(elem, i, path, next, stop){ // match... var res = (elem && (test === true // identity check... || (!options.noIdentityCheck && pattern === elem) // test... || (test // NOTE: we pass next here to provide the // test with the option to filter out // branches that it knows will not // match... && test.call(this, elem, i, path, next)))) ? // handle the passed items... [ func ? func.call(this, elem, i, path, stop) : elem ] : [] return ((options.firstMatch || typeof(pattern) == typeof(123)) && res.length > 0) ? stop(res) : res }, 'search', function(_, i, p, options, context){ return [pattern, func, options, context] }, options, context) }, // XXX EXPERIMENTAL... // // Get focused item... // .get() // .get('focused'[, func]) // -> item // -> undefined // // Get next/prev item relative to focused... // .get('prev'[, offset][, func][, options]) // .get('next'[, offset][, func][, options]) // -> item // -> undefined // // Get first item matching pattern... // .get(pattern[, func][, options]) // -> item // -> undefined // // pattern mostly follows the same scheme as in .select(..) so see // docs for that for more info. // // // NOTE: this is just like a lazy .search(..) that will return the // first result only. // // XXX should we be able to get offset values relative to any match? // XXX should we use .wald2(..) here??? // XXX revise return value... get: function(pattern, options){ var args = [...arguments] pattern = args.shift() pattern = pattern === undefined ? 'focused' : pattern var offset = (pattern == 'next' || pattern == 'prev') && typeof(args[0]) == typeof(123) ? args.shift() : 1 var func = args[0] instanceof Function ? args.shift() // XXX return format... : function(e, i, p){ return e } options = args.pop() || {} // sanity checks... if(offset <= 0){ throw new Error(`.get(..): offset must be a positive number, got: ${offset}.`) } // NOTE: we do not care about return values here as we'll return // via stop(..)... var res = [] return [ // next + offset... pattern == 'next' ? this.search(true, function(elem, i, path, stop){ if(elem.focused == true){ res = offset + 1 // get the offset item... } else if(res <= 0){ stop([func(elem, i, path)]) } // countdown to offset... res = typeof(res) == typeof(123) ? res - 1 : res }, options) // prev + offset... : pattern == 'prev' ? this.search(true, function(elem, i, path, stop){ elem.focused == true && stop([func(res.length >= offset ? res[0] : undefined)]) // buffer the previous offset items... res.push((elem, i, path)) res.length > offset && res.shift() }, options) // base case -> get first match... : this.search(pattern, function(elem, i, path, stop){ stop([func(elem, i, path)]) }, options) ].flat()[0] }, // XXX BROKEN... // Sublist map functions... // // XXX should these return a sparse array... ??? // XXX this does not include inlined sections, should it??? sublists: function(func, options){ return this.search({children: true}, func, options) }, 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, context){ return this.walk( function(e, i, p){ return e && func.call(this, e, i, p) ? [e] : [] }, 'filter', function(_, i, p, options, context){ return [func, options, context] }, options, context) }, reduce: function(func, start, options){ var that = this var context = arguments[3] || {result: start} this.walk( function(e, i, p){ context.result = e ? func.call(that, context.result, e, i, p) : context.result return context.result }, 'reduce', function(_, i, p, options, context){ return [func, context.result, options, context] }, options, context) return context.result }, positionOf: function(item, options){ return this.search(item, function(_, i, p){ return [i, p] }, Object.assign( { firstMatch: true, noQueryCheck: true, }, options || {})).concat([[-1, undefined]]).shift() }, indexOf: function(item, options){ return this.positionOf(item, options)[0] }, pathOf: function(item, options){ return this.positionOf(item, options)[1] }, // Like .select(.., {iterateCollapsed: true}) but will expand all the // path items to reveal the target... // // XXX should this return the item or this??? // XXX make .reveal('all'/'*') only do the actual nodes that need expanding... // ...currently for path 'a/b/c/d' we'll go through: // 'a/b' // 'a/b/c' // 'a/b/c/d' // XXX need a universal item name/value comparison / getter... reveal: function(key, options){ var that = this var seen = new Set() return this.search(key, function(e, i, path){ return [path, e] }, Object.assign( { iterateCollapsed: true, reverse: 'flat', }, options || {})) // sort paths long to short... //.sort(function(a, b){ // return b[0].length - a[0].length }) .map(function([path, e]){ // skip paths we have already seen... if(seen.has(e.id)){ return e } seen.add(e.id) var cur = that path.length > 1 && path .slice(0, -1) .forEach(function(n){ // array children... if(cur instanceof Array){ var e = cur .filter(function(e){ // XXX need a universal item name test... return n == (e.value || e.id) }) .pop() delete e.collapsed cur = e.children } else { // XXX .index feels ugly... delete cur.index[n].collapsed cur = cur.index[n].children } }) return e }) .run(function(){ that.render() }) }, // 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 .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 .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 no 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.children instanceof Browser && (item.children.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, children, 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 children??? // ...this would make it simpler to expand/collapse without // re-rendering the whole list... renderNested: function(header, children, item, context){ return header ? this.renderGroup([ header, children, ]) : children }, 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[, context]]) // -> 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... render: function(options, renderer, context){ context = context || {} renderer = renderer || this options = context.options || Object.assign( Object.create(this.options || {}), { iterateNonIterable: true }, options || {}) context.options = context.options || options // do the walk... var elems = this.walk( function(elem, i, path, nested){ return ( // inline... elem == null ? // NOTE: here we are forcing rendering of the // inline browser/list, i.e. ignoring // options.skipNested for inline stuff... [ renderer.renderGroup(nested(true), context) ] // nested... : elem.children ? [ renderer.renderNested( renderer.renderNestedHeader(elem, i, context), nested(), elem, context) ] // normal elem... : [ renderer.renderItem(elem, i, context) ] ) }, 'render', function(_, i, p, options, context){ return [options, renderer, context] }, options, context) // finalize depending on render mode... return (!options.nonFinalized && context.root === this) ? // root context -> render list and return this... renderer.renderFinalize(elems, context) // nested context -> return item list... : elems }, // Update state (make then render)... // // .update() // -> state // // XXX should this be an event??? update: function(options){ return this .make(options) .render(options) }, // Events... // // Format: // { // // XXX add tagged event support... // // ...i.e. event markers/tags that would enable the user // // to specifically manipulate event sets.... // : [ // , // ... // ], // ... // } // // XXX __event_handlers: null, // generic event infrastructure... // 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, tag){ var handlers = this.__event_handlers = this.__event_handlers || {} handlers = handlers[evt] = handlers[evt] || [] handlers.push(handler) tag && (handler.tag = tag) return this }, one: function(evt, handler){ var func = function(...args){ handler.call(this, ...args) this.off(evt, func) } this.on(evt, func) return this }, // // Clear all event handlers... // .off('*') // // Clear all event handlers from evt(s)... // .off(evt) // .off([evt, ..]) // .off(evt, '*') // .off([evt, ..], '*') // // Clear handler of evt(s)... // .off(evt, handler) // .off([evt, ..], handler) // // Clear all handlers tagged with tag of evt(s)... // .off(evt, tag) // .off([evt, ..], tag) // // NOTE: evt can be '*' or 'all' to indicate all events. off: function(evt, handler){ if(arguments.length == 0){ return } var handlers = this.__event_handlers || {} // parse args... handler = handler || '*' evt = // all events / direct handler... (!(evt in handlers) || evt == '*' || evt == 'all') ? Object.keys(handlers) // list of events... : evt instanceof Array ? evt // explicit event... : [evt] // remove all handlers handler == '*' || handler == 'all' ? evt .forEach(function(evt){ delete handlers[evt] }) // remove tagged handlers... : typeof(handler) == typeof('str') ? evt .forEach(function(evt){ var h = handlers[evt] || [] var l = h.length h .slice() .reverse() .forEach(function(e, i){ e.tag == handler && h.splice(l-i-1, 1) }) }) // remove only the specific handler... : evt .forEach(function(evt){ var h = handlers[evt] || [] do{ var i = h.indexOf(handler) i > -1 && h.splice(i, 1) } while(i > -1) }) return this }, // // Trigger an event by name... // .trigger(, ..) // -> this // // Trigger an event... // .trigger(, ..) // -> this // // // Passing an will do the following: // - if an handler is available call it and return // - the handler should: // - do any specifics that it needs // - create an // - call trigger with // - if has no handler: // - create an // - call the event handlers passing them and args // - call parent's .trigger(, ..) // // format: // { // name: , // // propagationStopped: , // stopPropagation: , // } // // // XXX need to make stopPropagation(..) work even if we got an // externally made event object... // XXX need to make this workable with DOM events... (???) // XXX construct the event in one spot... trigger: function(evt, ...args){ var that = this // trigger the appropriate event handler if available... // NOTE: this makes .someEvent(..) and .trigger('someEvent', ..) // do the same thing by always triggering .someEvent(..) // first and letting it decide how to call .trigger(..)... // NOTE: the event method should pass a fully formed event object // into trigger when it requires to call the handlers... if(typeof(evt) == typeof('str') && this[evt] instanceof Function && this[evt].event == evt){ this[evt](...args) return this } // XXX need to make stopPropagation(..) work even if we got an // externally made event object... 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(){ this.propagationStopped = 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 && !evt.propagationStopped && this.parent && this.parent.trigger instanceof Function // XXX should we trigger with and event object or an event // name??? //&& this.parent.trigger(evt, ...args) && this.parent.trigger(evt.name, ...args) return this }, // List events... // XXX avoid expensive props... get events(){ var that = this return Object.deepKeys(this) .map(function(key){ return (key != 'events' && that[key] instanceof Function && that[key].event) ? that[key].event : [] }) .flat() }, // 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... // XXX should we trigger direct item's parent event??? focus: makeItemEventMethod('focus', function(evt, items){ // blur .focused... this.focused && this.blur(this.focused) // NOTE: if we got multiple matches we care only about the first one... var item = items.shift() item != null && (item.focused = true) }), blur: makeItemEventMethod('blur', function(evt, items){ items.forEach(function(item){ delete item.focused }) }), select: makeItemEventMethod('select', function(evt, items){ items.forEach(function(item){ item.selected = true }) }), deselect: makeItemEventMethod('deselect', function(evt, items){ items.forEach(function(item){ delete item.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, children, 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... children instanceof Node ? e.appendChild(children) // XXX should this add the items to a container??? : children instanceof Array ? children .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, children, context, item, options){ var that = this var nested = children && children .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 })