diff --git a/ui (gen4)/lib/widget/browse2.html b/ui (gen4)/lib/widget/browse2.html index cfef22f3..7f443a0f 100755 --- a/ui (gen4)/lib/widget/browse2.html +++ b/ui (gen4)/lib/widget/browse2.html @@ -44,6 +44,12 @@ body { overflow: visible; } + +.browse-widget .list .text .key-hint { + text-decoration-skip-ink: none; +} + + /* XXX stub... .browse-widget:not(.flat) .list .text:first-child:before { display: inline-block; @@ -186,7 +192,7 @@ requirejs([ dialog = browser.Browser(function(make){ make(['list', 'of', 'text']) make.group( - make('group item 0', + make('$group item 0', function(){ console.log('###', ...arguments) }), 'group item 1 (bare)') // XXX Q: should we show only one if multiple lines are in sequence??? @@ -198,11 +204,11 @@ requirejs([ make(2) })) // basic nested list... - make.nest('nested', [ + make.nest('$nested', [ make('moo', {disabled: true}), 2, // XXX this is not supported by .map(..)... - make.nest('nested', browser.Browser(function(make){ + make.nest('$ne$sted', browser.Browser(function(make){ make('ab') })), ]) diff --git a/ui (gen4)/lib/widget/browse2.js b/ui (gen4)/lib/widget/browse2.js index 1c04d1e1..53156d73 100755 --- a/ui (gen4)/lib/widget/browse2.js +++ b/ui (gen4)/lib/widget/browse2.js @@ -436,21 +436,16 @@ function(event, {handler, action, default_item, filter, options={}, getter='sear // Make event method edit item... // // XXX should this .update() -var makeItemOptionEventMethod = -module.makeItemOptionEventMethod = -function(event, action, {handler, default_item, filter, options, update=true}={}){ +var makeItemEditEventMethod = +module.makeItemEditEventMethod = +function(event, edit, {handler, default_item, filter, options}={}){ return makeItemEventMethod(event, { handler: function(evt, items){ var that = this - var change = false items.forEach(function(item){ - change = action(item) !== false + edit(item) handler - && handler.call(that, item) }) - // need to update for changes to show up... - update - && change - && this.update() }, + && handler.call(that, item) }) }, default_item: default_item || function(){ return this.focused }, @@ -461,20 +456,20 @@ function(event, action, {handler, default_item, filter, options, update=true}={} // var makeItemOptionOnEventMethod = module.makeItemOptionOnEventMethod = -function(event, attr, {handler, default_item, filter, options, update=true}={}){ - return makeItemOptionEventMethod(event, +function(event, attr, {handler, default_item, filter, options}={}){ + return makeItemEditEventMethod(event, function(item){ return item[attr] = true }, - { handler, default_item, filter, options, update }) } + { handler, default_item, filter, options }) } var makeItemOptionOffEventMethod = module.makeItemOptionOffEventMethod = -function(event, attr, {handler, default_item, filter, options, update=true}={}){ - return makeItemOptionEventMethod(event, +function(event, attr, {handler, default_item, filter, options}={}){ + return makeItemEditEventMethod(event, function(item){ change = !!item[attr] delete item[attr] return change }, - { handler, default_item, filter, options, update }) } + { handler, default_item, filter, options }) } // Generate item event/state toggler... @@ -638,6 +633,8 @@ var BaseBrowserPrototype = { options: { // If true item keys must be unique... uniqueKeys: false, + + //skipDisabledMode: 'node', }, // parent widget object... @@ -779,6 +776,7 @@ var BaseBrowserPrototype = { .select(value) }, + // XXX should this return a list or a string??? // XXX should this be cached??? // XXX should this set .options??? // XXX need to normalizePath(..) @@ -998,7 +996,8 @@ var BaseBrowserPrototype = { // // XXX not yet supported... // skipInlined: , // - // skipDisabled: , + // skipDisabledMode: 'node' | 'branch', + // skipDisabled: | 'node' | 'branch', // // // Reverse iteration order... // // @@ -1079,7 +1078,9 @@ var BaseBrowserPrototype = { || args[0] == null)) ? args.shift() : null - options = args.shift() || {} + options = Object.assign( + Object.create(this.options || {}), + args.shift() || {}) // get/build context... var context = args.shift() @@ -1094,7 +1095,12 @@ var BaseBrowserPrototype = { var iterateCollapsed = options.iterateAll || options.iterateCollapsed var skipNested = !options.iterateAll && options.skipNested var skipInlined = !options.iterateAll && options.skipInlined + var skipDisabled = !options.iterateAll && options.skipDisabled + skipDisabled = skipDisabled === true ? + (options.skipDisabledMode || 'node') + : skipDisabled + var reverse = options.reverse === true ? (options.defaultReverse || 'tree') : options.reverse @@ -1118,8 +1124,8 @@ var BaseBrowserPrototype = { // skip non-iterable items... if(!iterateNonIterable && node.noniterable){ return state } - // skip disabled... - if(skipDisabled && node.disabled){ + // skip disabled branch... + if(skipDisabled == 'branch' && node.disabled){ return state } // XXX BUG?: doNested(false) will not count any of the @@ -1224,19 +1230,22 @@ var BaseBrowserPrototype = { && 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], + !(skipDisabled && node.disabled) ? + (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]) + // element is disabled -> handle children... + : [], // normal order -> do children... children && nested === false @@ -1417,6 +1426,8 @@ var BaseBrowserPrototype = { || args[0] === undefined) ? args.shift() : undefined + // NOTE: we do not inherit options from this.options here is it + // will be done in .walk(..) options = args.shift() || {} options = !options.defaultReverse ? Object.assign({}, @@ -1642,6 +1653,8 @@ var BaseBrowserPrototype = { || args[0] === undefined) ? args.shift() : undefined + // NOTE: we do not inherit options from this.options here is it + // will be done in .walk(..) options = args.shift() || {} var context = args.shift() @@ -1812,6 +1825,8 @@ var BaseBrowserPrototype = { args.shift() // XXX return format... : function(e, i, p){ return e } + // NOTE: we do not inherit options from this.options here is it + // will be done in .walk(..) options = args.pop() || {} // special case: path pattern -> include collapsed elements... @@ -2006,6 +2021,9 @@ var BaseBrowserPrototype = { + //__make__: function(item){ + //}, + // Make .items and .index... // // .make() @@ -2040,7 +2058,9 @@ var BaseBrowserPrototype = { // : opts) make: function(options){ var that = this - options = Object.assign(Object.create(this.options || {}), options || {}) + options = Object.assign( + Object.create(this.options || {}), + options || {}) var items = this.items = [] @@ -2131,6 +2151,10 @@ var BaseBrowserPrototype = { && (item.children.parent = this) } + // user extended make... + this.__make__ + && this.__make__(item) + // store the item... items.push(item) ids.add(key) @@ -2176,6 +2200,7 @@ var BaseBrowserPrototype = { && Object.assign(e, old_index[id], e) }) + return this }, @@ -2264,6 +2289,7 @@ var BaseBrowserPrototype = { // // NOTE: the only constrain on to/from is that from must be // // less or equal to to, other than that it's fair game, // // i.e. overflowing values (<0 or >length) are allowed. + // // NOTE: these are not inherited from .options... // from: | , // to: | , // around: | , @@ -2665,24 +2691,14 @@ var BaseBrowserPrototype = { function(){ return this.focused || 0 }, false), // selection... - // XXX these should skip disabled... option??? - select: makeItemEventMethod('select', { - handler: function(evt, items){ - items.forEach(function(item){ - item.selected = true }) }, - // XXX is this a good default??? - default_item: function(){ return this.focused } }), - deselect: makeItemEventMethod('deselect', { - handler: function(evt, items){ - items.forEach(function(item){ - delete item.selected }) }, - default_item: function(){ return this.focused } }), + select: makeItemOptionOnEventMethod('select', 'selected'), + deselect: makeItemOptionOffEventMethod('deselect', 'selected'), toggleSelect: makeItemEventToggler( 'selected', 'select', 'deselect', 'focused'), // topology... - collapse: makeItemOptionOnEventMethod('expand', 'collapsed', { + collapse: makeItemOptionOnEventMethod('collapse', 'collapsed', { filter: function(elem){ return elem.value && elem.children }, options: {iterateCollapsed: true}, }), expand: makeItemOptionOffEventMethod('expand', 'collapsed', { @@ -2748,9 +2764,15 @@ var BaseBrowserPrototype = { : full this .run(function(){ - full && this.make(options) }) + full + && this.make(options) + this.preRender() + }) .render(options) }), - + // this is triggered by .update() just before render... + preRender: makeEventMethod('preRender'), + + // NOTE: if given a path that does not exist this will try and load // the longest existing sub-path... // XXX should level drawing be a feature of the browser or the @@ -2799,13 +2821,14 @@ object.makeConstructor('BaseBrowser', //--------------------------------------------------------------------- // Get actual .item DOM element... +// +// XXX should this be a prop in the element??? var getElem = function(elem){ elem = elem.dom || elem return elem.classList.contains('list') ? elem.querySelector('.item') : elem } - // Make page navigation method... // // XXX this behaves in an odd way with .options.scrollBehavior = 'smooth' @@ -2826,14 +2849,38 @@ var focusPage = function(direction){ // focus top of current page... : this.focus(target) } } +// Update element class... +// +// XXX should we use .renderItem(...) for this??? +var updateElemClass = function(action, cls, handler){ + return function(evt, elem, ...args){ + elem + && getElem(elem).classList[action](cls) + return handler + && handler.call(this, evt, elem, ...args)} } + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var KEYBOARD_CONFIG = module.KEYBOARD_CONFIG = { - // XXX + ItemEdit: { + pattern: '.list .text[contenteditable]', + + // XXX + }, + + PathEdit: { + pattern: '.path[contenteditable]', + + // XXX + }, + Filter: { + pattern: '.path div.cur[contenteditable]', + + // XXX }, General: { @@ -2851,6 +2898,17 @@ module.KEYBOARD_CONFIG = { Home: 'focus: "first"', End: 'focus: "last"', + '#1': 'focus: 0', + '#2': 'focus: 1', + '#3': 'focus: 2', + '#4': 'focus: 3', + '#5': 'focus: 4', + '#6': 'focus: 5', + '#7': 'focus: 6', + '#8': 'focus: 7', + '#9': 'focus: 8', + '#0': 'focus: 9', + Enter: 'open', @@ -2861,7 +2919,15 @@ module.KEYBOARD_CONFIG = { // NOTE: do not bind this key, it is used to jump to buttons // via tabindex... - Tab: 'NEXT', + Tab: 'NEXT!', + }, + + // XXX need to keep this local to each dialog instance... + ItemShortcuts: { + doc: 'Item shortcuts', + pattern: '*', + + // this is where item-specific shortcuts will be set... }, } @@ -2962,10 +3028,10 @@ var BrowserPrototype = { }, - __keyboard_config: KEYBOARD_CONFIG, + // Keyboard... + __keyboard_config: Object.assign({}, KEYBOARD_CONFIG), get keybindings(){ return this.__keyboard_config }, - __keyboard_object: null, get keyboard(){ var that = this @@ -3009,7 +3075,7 @@ var BrowserPrototype = { : this.container.appendChild(value)) this.__dom = value }, - // Extended .get(..) to support: + // Extended .search(..) to support: // - 'pagetop' // - 'pagebottom' // - searching for items via DOM / jQuery objects @@ -3017,14 +3083,14 @@ var BrowserPrototype = { // ...should we add containment search -- match closest item containing obj... // // - // .get('pagetop'[, offset] ..) + // .search('pagetop'[, offset] ..) // - // .get('pagebottom'[, offset] ..) + // .search('pagebottom'[, offset] ..) // // // XXX add support for pixel offset... // XXX - get: function(pattern){ + search: function(pattern){ var args = [...arguments].slice(1) var p = pattern @@ -3050,9 +3116,12 @@ var BrowserPrototype = { && Math.round(edom.offsetTop + edom.offsetHeight) - Math.max(0, st + H + offset) <= 0 && stop(e) }, - { reverse: pos == 'bottom' ? - 'flat' - : false }) + { + reverse: pos == 'bottom' ? + 'flat' + : false, + skipDisabled: true, + }) .run(function(){ return this instanceof Array ? undefined @@ -3079,7 +3148,7 @@ var BrowserPrototype = { : pattern // call parent... - return object.parent(BrowserPrototype.get, this).call(this, pattern, ...args) }, + return object.parent(BrowserPrototype.search, this).call(this, pattern, ...args) }, // Element renderers... @@ -3312,6 +3381,7 @@ var BrowserPrototype = { // // XXX should we trigger the DOM event or the browser event??? // XXX should buttoms be active in disabled state??? + // XXX replace $X with X but only where the X is in item.keys renderItem: function(item, i, context){ var that = this var options = context.options || this.options @@ -3320,7 +3390,7 @@ var BrowserPrototype = { } // special-case: item shorthands... - if(item.value in options.elementShorthand){ + if(item.value in (options.elementShorthand || {})){ // XXX need to merge and not overwrite -- revise... Object.assign(item, options.elementShorthand[item.value]) @@ -3368,6 +3438,17 @@ var BrowserPrototype = { && (item.value instanceof Array ? item.value : [item.value]) // XXX handle $keys and other stuff... .map(function(v){ + // handle key-shortcuts $K... + v = typeof(v) == typeof('str') ? + v.replace(/\$\w/g, + function(k){ + k = k[1] + return (item.keys || []) + .includes(that.keyboard.normalizeKey(k)) ? + `${k}` + : k }) + : v + var value = document.createElement('span') value.classList.add('text') value.innerHTML = v != null ? @@ -3462,6 +3543,51 @@ var BrowserPrototype = { // Custom events handlers... // + // NOTE: this will also kill any user-set keys for disabled/hidden items... + __preRender__: function(){ + var that = this + // reset item shortcuts... + var shortcuts = + this.keybindings.ItemShortcuts = + Object.assign({}, KEYBOARD_CONFIG.ItemShortcuts) + + var i = 0 + this.map(function(e){ + // shortcut number hint... + // NOTE: these are just hints, the actual keys are handled + // in .keybindings... + if(i < 10 && !e.disabled && !e.hidden){ + var attrs = e.attrs = e.attrs || {} + attrs['shortcut-number'] = (++i) % 10 + // cleanup... + } else { + delete (e.attrs || {})['shortcut-number'] + } + + // handle item keys... + if(!e.disabled && !e.hidden){ + ;((e.value instanceof Array ? + e.value + : [e.value]) + .join(' ') + // XXX this does not include non-English chars... + .match(/\$\w/g) || []) + .map(function(k){ + k = that.keyboard.normalizeKey(k[1]) + if(!shortcuts[k]){ + shortcuts[k] = function(){ that.focus(e) } + var keys = e.keys = e.keys || [] + keys.push(k) + } }) + + // cleanup... + // NOTE: this will also kill any user-set keys for disabled/hidden items... + } else { + delete e.keys + } + }, {skipDisabled: false}) + }, + // NOTE: element alignment is done via the browser focus mechanics... __focus__: function(evt, elem){ var that = this @@ -3471,15 +3597,8 @@ var BrowserPrototype = { // NOTE: we will not remove this class on blur as it keeps // the selected element indicated... .run(function(){ - // XXX scroll to element if it's out of bounds... - // XXX - - that.dom - && that.dom.querySelectorAll('.focused') - .forEach(function(e){ - e.classList.remove('focused') }) this.classList.add('focused') - + // take care of visibility... this.scrollIntoView({ behavior: (that.options || {}).scrollBehavior || 'auto', block: 'nearest', @@ -3493,29 +3612,36 @@ var BrowserPrototype = { && getElem(elem) .run(function(){ this.classList.remove('focused') - //this.blur() + // refocus the dialog... that.dom && that.dom.focus() }) }, - // NOTE: these simply update the state... - __select__: function(){ - var selected = new Set(this.selected.map(getElem)) - this.dom - && this.dom.querySelectorAll('.selected') - .forEach(function(e){ - selected.has(e) - || e.classList.remove('selected') }) - selected - .forEach(function(e){ - e.classList.add('selected') }) }, - __deselect__: function(evt, elem){ - this.__select__() }, + // XXX should we only update the current elem??? + __expand__: function(){ this.update() }, + __collapse__: function(){ this.update() }, + + __select__: updateElemClass('add', 'selected'), + __deselect__: updateElemClass('remove', 'selected'), + __disable__: updateElemClass('add', 'disabled'), + __enable__: updateElemClass('remove', 'disabled'), + __hide__: updateElemClass('add', 'hidden'), + __show__: updateElemClass('remove', 'hidden'), + // Custom events... // - // XXX make this different from html event... + // XXX make this different from html event??? // XXX trigger this from kb handler... - keyhandled: makeEventMethod('keyhandled', function(){ + keyPress: makeEventMethod('keypress', function(){ + }), + // XXX + menu: makeEventMethod('menu', function(){ + }), + // XXX + copy: makeEventMethod('copy', function(){ + }), + // XXX + paste: makeEventMethod('paste', function(){ }),