added item keyboard shortcuts + cleanup and some refactoring...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2019-06-16 21:14:23 +03:00
parent 5d35ae1fb0
commit fc8c745ac3
2 changed files with 222 additions and 90 deletions

View File

@ -44,6 +44,12 @@ body {
overflow: visible; overflow: visible;
} }
.browse-widget .list .text .key-hint {
text-decoration-skip-ink: none;
}
/* XXX stub... /* XXX stub...
.browse-widget:not(.flat) .list .text:first-child:before { .browse-widget:not(.flat) .list .text:first-child:before {
display: inline-block; display: inline-block;
@ -186,7 +192,7 @@ requirejs([
dialog = browser.Browser(function(make){ dialog = browser.Browser(function(make){
make(['list', 'of', 'text']) make(['list', 'of', 'text'])
make.group( make.group(
make('group item 0', make('$group item 0',
function(){ console.log('###', ...arguments) }), function(){ console.log('###', ...arguments) }),
'group item 1 (bare)') 'group item 1 (bare)')
// XXX Q: should we show only one if multiple lines are in sequence??? // XXX Q: should we show only one if multiple lines are in sequence???
@ -198,11 +204,11 @@ requirejs([
make(2) make(2)
})) }))
// basic nested list... // basic nested list...
make.nest('nested', [ make.nest('$nested', [
make('moo', {disabled: true}), make('moo', {disabled: true}),
2, 2,
// XXX this is not supported by .map(..)... // XXX this is not supported by .map(..)...
make.nest('nested', browser.Browser(function(make){ make.nest('$ne$sted', browser.Browser(function(make){
make('ab') make('ab')
})), })),
]) ])

View File

@ -436,21 +436,16 @@ function(event, {handler, action, default_item, filter, options={}, getter='sear
// Make event method edit item... // Make event method edit item...
// //
// XXX should this .update() // XXX should this .update()
var makeItemOptionEventMethod = var makeItemEditEventMethod =
module.makeItemOptionEventMethod = module.makeItemEditEventMethod =
function(event, action, {handler, default_item, filter, options, update=true}={}){ function(event, edit, {handler, default_item, filter, options}={}){
return makeItemEventMethod(event, { return makeItemEventMethod(event, {
handler: function(evt, items){ handler: function(evt, items){
var that = this var that = this
var change = false
items.forEach(function(item){ items.forEach(function(item){
change = action(item) !== false edit(item)
handler handler
&& handler.call(that, item) }) && handler.call(that, item) }) },
// need to update for changes to show up...
update
&& change
&& this.update() },
default_item: default_item:
default_item default_item
|| function(){ return this.focused }, || function(){ return this.focused },
@ -461,20 +456,20 @@ function(event, action, {handler, default_item, filter, options, update=true}={}
// //
var makeItemOptionOnEventMethod = var makeItemOptionOnEventMethod =
module.makeItemOptionOnEventMethod = module.makeItemOptionOnEventMethod =
function(event, attr, {handler, default_item, filter, options, update=true}={}){ function(event, attr, {handler, default_item, filter, options}={}){
return makeItemOptionEventMethod(event, return makeItemEditEventMethod(event,
function(item){ function(item){
return item[attr] = true }, return item[attr] = true },
{ handler, default_item, filter, options, update }) } { handler, default_item, filter, options }) }
var makeItemOptionOffEventMethod = var makeItemOptionOffEventMethod =
module.makeItemOptionOffEventMethod = module.makeItemOptionOffEventMethod =
function(event, attr, {handler, default_item, filter, options, update=true}={}){ function(event, attr, {handler, default_item, filter, options}={}){
return makeItemOptionEventMethod(event, return makeItemEditEventMethod(event,
function(item){ function(item){
change = !!item[attr] change = !!item[attr]
delete item[attr] delete item[attr]
return change }, return change },
{ handler, default_item, filter, options, update }) } { handler, default_item, filter, options }) }
// Generate item event/state toggler... // Generate item event/state toggler...
@ -638,6 +633,8 @@ var BaseBrowserPrototype = {
options: { options: {
// If true item keys must be unique... // If true item keys must be unique...
uniqueKeys: false, uniqueKeys: false,
//skipDisabledMode: 'node',
}, },
// parent widget object... // parent widget object...
@ -779,6 +776,7 @@ var BaseBrowserPrototype = {
.select(value) }, .select(value) },
// XXX should this return a list or a string???
// XXX should this be cached??? // XXX should this be cached???
// XXX should this set .options??? // XXX should this set .options???
// XXX need to normalizePath(..) // XXX need to normalizePath(..)
@ -998,7 +996,8 @@ var BaseBrowserPrototype = {
// // XXX not yet supported... // // XXX not yet supported...
// skipInlined: <bool>, // skipInlined: <bool>,
// //
// skipDisabled: <bool>, // skipDisabledMode: 'node' | 'branch',
// skipDisabled: <bool> | 'node' | 'branch',
// //
// // Reverse iteration order... // // Reverse iteration order...
// // // //
@ -1079,7 +1078,9 @@ var BaseBrowserPrototype = {
|| args[0] == null)) ? || args[0] == null)) ?
args.shift() args.shift()
: null : null
options = args.shift() || {} options = Object.assign(
Object.create(this.options || {}),
args.shift() || {})
// get/build context... // get/build context...
var context = args.shift() var context = args.shift()
@ -1094,7 +1095,12 @@ var BaseBrowserPrototype = {
var iterateCollapsed = options.iterateAll || options.iterateCollapsed var iterateCollapsed = options.iterateAll || options.iterateCollapsed
var skipNested = !options.iterateAll && options.skipNested var skipNested = !options.iterateAll && options.skipNested
var skipInlined = !options.iterateAll && options.skipInlined var skipInlined = !options.iterateAll && options.skipInlined
var skipDisabled = !options.iterateAll && options.skipDisabled var skipDisabled = !options.iterateAll && options.skipDisabled
skipDisabled = skipDisabled === true ?
(options.skipDisabledMode || 'node')
: skipDisabled
var reverse = options.reverse === true ? var reverse = options.reverse === true ?
(options.defaultReverse || 'tree') (options.defaultReverse || 'tree')
: options.reverse : options.reverse
@ -1118,8 +1124,8 @@ var BaseBrowserPrototype = {
// skip non-iterable items... // skip non-iterable items...
if(!iterateNonIterable && node.noniterable){ if(!iterateNonIterable && node.noniterable){
return state } return state }
// skip disabled... // skip disabled branch...
if(skipDisabled && node.disabled){ if(skipDisabled == 'branch' && node.disabled){
return state } return state }
// XXX BUG?: doNested(false) will not count any of the // XXX BUG?: doNested(false) will not count any of the
@ -1224,19 +1230,22 @@ var BaseBrowserPrototype = {
&& doNested() && doNested()
|| [], || [],
// do element... // do element...
func ? !(skipDisabled && node.disabled) ?
(func.call(that, (func ?
...(inline ? (func.call(that,
[null, context.index] ...(inline ?
: [node, context.index++]), [null, context.index]
p, : [node, context.index++]),
// NOTE: when calling this it is the p,
// responsibility of the caller to return // NOTE: when calling this it is the
// the result to be added to state... // responsibility of the caller to return
doNested, // the result to be added to state...
stop, doNested,
children) || []) stop,
: [node], children) || [])
: [node])
// element is disabled -> handle children...
: [],
// normal order -> do children... // normal order -> do children...
children children
&& nested === false && nested === false
@ -1417,6 +1426,8 @@ var BaseBrowserPrototype = {
|| args[0] === undefined) ? || args[0] === undefined) ?
args.shift() args.shift()
: undefined : undefined
// NOTE: we do not inherit options from this.options here is it
// will be done in .walk(..)
options = args.shift() || {} options = args.shift() || {}
options = !options.defaultReverse ? options = !options.defaultReverse ?
Object.assign({}, Object.assign({},
@ -1642,6 +1653,8 @@ var BaseBrowserPrototype = {
|| args[0] === undefined) ? || args[0] === undefined) ?
args.shift() args.shift()
: undefined : undefined
// NOTE: we do not inherit options from this.options here is it
// will be done in .walk(..)
options = args.shift() || {} options = args.shift() || {}
var context = args.shift() var context = args.shift()
@ -1812,6 +1825,8 @@ var BaseBrowserPrototype = {
args.shift() args.shift()
// XXX return format... // XXX return format...
: function(e, i, p){ return e } : 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() || {} options = args.pop() || {}
// special case: path pattern -> include collapsed elements... // special case: path pattern -> include collapsed elements...
@ -2006,6 +2021,9 @@ var BaseBrowserPrototype = {
//__make__: function(item){
//},
// Make .items and .index... // Make .items and .index...
// //
// .make() // .make()
@ -2040,7 +2058,9 @@ var BaseBrowserPrototype = {
// : opts) // : opts)
make: function(options){ make: function(options){
var that = this var that = this
options = Object.assign(Object.create(this.options || {}), options || {}) options = Object.assign(
Object.create(this.options || {}),
options || {})
var items = this.items = [] var items = this.items = []
@ -2131,6 +2151,10 @@ var BaseBrowserPrototype = {
&& (item.children.parent = this) && (item.children.parent = this)
} }
// user extended make...
this.__make__
&& this.__make__(item)
// store the item... // store the item...
items.push(item) items.push(item)
ids.add(key) ids.add(key)
@ -2176,6 +2200,7 @@ var BaseBrowserPrototype = {
&& Object.assign(e, && Object.assign(e,
old_index[id], old_index[id],
e) }) e) })
return this return this
}, },
@ -2264,6 +2289,7 @@ var BaseBrowserPrototype = {
// // NOTE: the only constrain on to/from is that from must be // // NOTE: the only constrain on to/from is that from must be
// // less or equal to to, other than that it's fair game, // // less or equal to to, other than that it's fair game,
// // i.e. overflowing values (<0 or >length) are allowed. // // i.e. overflowing values (<0 or >length) are allowed.
// // NOTE: these are not inherited from .options...
// from: <index> | <query>, // from: <index> | <query>,
// to: <index> | <query>, // to: <index> | <query>,
// around: <index> | <query>, // around: <index> | <query>,
@ -2665,24 +2691,14 @@ var BaseBrowserPrototype = {
function(){ return this.focused || 0 }, function(){ return this.focused || 0 },
false), false),
// selection... // selection...
// XXX these should skip disabled... option??? select: makeItemOptionOnEventMethod('select', 'selected'),
select: makeItemEventMethod('select', { deselect: makeItemOptionOffEventMethod('deselect', 'selected'),
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 } }),
toggleSelect: makeItemEventToggler( toggleSelect: makeItemEventToggler(
'selected', 'selected',
'select', 'deselect', 'select', 'deselect',
'focused'), 'focused'),
// topology... // topology...
collapse: makeItemOptionOnEventMethod('expand', 'collapsed', { collapse: makeItemOptionOnEventMethod('collapse', 'collapsed', {
filter: function(elem){ return elem.value && elem.children }, filter: function(elem){ return elem.value && elem.children },
options: {iterateCollapsed: true}, }), options: {iterateCollapsed: true}, }),
expand: makeItemOptionOffEventMethod('expand', 'collapsed', { expand: makeItemOptionOffEventMethod('expand', 'collapsed', {
@ -2748,9 +2764,15 @@ var BaseBrowserPrototype = {
: full : full
this this
.run(function(){ .run(function(){
full && this.make(options) }) full
&& this.make(options)
this.preRender()
})
.render(options) }), .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 // NOTE: if given a path that does not exist this will try and load
// the longest existing sub-path... // the longest existing sub-path...
// XXX should level drawing be a feature of the browser or the // XXX should level drawing be a feature of the browser or the
@ -2799,13 +2821,14 @@ object.makeConstructor('BaseBrowser',
//--------------------------------------------------------------------- //---------------------------------------------------------------------
// Get actual .item DOM element... // Get actual .item DOM element...
//
// XXX should this be a prop in the element???
var getElem = function(elem){ var getElem = function(elem){
elem = elem.dom || elem elem = elem.dom || elem
return elem.classList.contains('list') ? return elem.classList.contains('list') ?
elem.querySelector('.item') elem.querySelector('.item')
: elem } : elem }
// Make page navigation method... // Make page navigation method...
// //
// XXX this behaves in an odd way with .options.scrollBehavior = 'smooth' // XXX this behaves in an odd way with .options.scrollBehavior = 'smooth'
@ -2826,14 +2849,38 @@ var focusPage = function(direction){
// focus top of current page... // focus top of current page...
: this.focus(target) } } : 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 = var KEYBOARD_CONFIG =
module.KEYBOARD_CONFIG = { module.KEYBOARD_CONFIG = {
// XXX ItemEdit: {
pattern: '.list .text[contenteditable]',
// XXX
},
PathEdit: {
pattern: '.path[contenteditable]',
// XXX
},
Filter: { Filter: {
pattern: '.path div.cur[contenteditable]',
// XXX
}, },
General: { General: {
@ -2851,6 +2898,17 @@ module.KEYBOARD_CONFIG = {
Home: 'focus: "first"', Home: 'focus: "first"',
End: 'focus: "last"', 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', Enter: 'open',
@ -2861,7 +2919,15 @@ module.KEYBOARD_CONFIG = {
// NOTE: do not bind this key, it is used to jump to buttons // NOTE: do not bind this key, it is used to jump to buttons
// via tabindex... // 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(){ get keybindings(){
return this.__keyboard_config }, return this.__keyboard_config },
__keyboard_object: null, __keyboard_object: null,
get keyboard(){ get keyboard(){
var that = this var that = this
@ -3009,7 +3075,7 @@ var BrowserPrototype = {
: this.container.appendChild(value)) : this.container.appendChild(value))
this.__dom = value }, this.__dom = value },
// Extended .get(..) to support: // Extended .search(..) to support:
// - 'pagetop' // - 'pagetop'
// - 'pagebottom' // - 'pagebottom'
// - searching for items via DOM / jQuery objects // - searching for items via DOM / jQuery objects
@ -3017,14 +3083,14 @@ var BrowserPrototype = {
// ...should we add containment search -- match closest item containing obj... // ...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 add support for pixel offset...
// XXX // XXX
get: function(pattern){ search: function(pattern){
var args = [...arguments].slice(1) var args = [...arguments].slice(1)
var p = pattern var p = pattern
@ -3050,9 +3116,12 @@ var BrowserPrototype = {
&& Math.round(edom.offsetTop + edom.offsetHeight) && Math.round(edom.offsetTop + edom.offsetHeight)
- Math.max(0, st + H + offset) <= 0 - Math.max(0, st + H + offset) <= 0
&& stop(e) }, && stop(e) },
{ reverse: pos == 'bottom' ? {
'flat' reverse: pos == 'bottom' ?
: false }) 'flat'
: false,
skipDisabled: true,
})
.run(function(){ .run(function(){
return this instanceof Array ? return this instanceof Array ?
undefined undefined
@ -3079,7 +3148,7 @@ var BrowserPrototype = {
: pattern : pattern
// call parent... // call parent...
return object.parent(BrowserPrototype.get, this).call(this, pattern, ...args) }, return object.parent(BrowserPrototype.search, this).call(this, pattern, ...args) },
// Element renderers... // Element renderers...
@ -3312,6 +3381,7 @@ var BrowserPrototype = {
// //
// XXX should we trigger the DOM event or the browser event??? // XXX should we trigger the DOM event or the browser event???
// XXX should buttoms be active in disabled state??? // XXX should buttoms be active in disabled state???
// XXX replace $X with <u>X</u> but only where the X is in item.keys
renderItem: function(item, i, context){ renderItem: function(item, i, context){
var that = this var that = this
var options = context.options || this.options var options = context.options || this.options
@ -3320,7 +3390,7 @@ var BrowserPrototype = {
} }
// special-case: item shorthands... // special-case: item shorthands...
if(item.value in options.elementShorthand){ if(item.value in (options.elementShorthand || {})){
// XXX need to merge and not overwrite -- revise... // XXX need to merge and not overwrite -- revise...
Object.assign(item, options.elementShorthand[item.value]) Object.assign(item, options.elementShorthand[item.value])
@ -3368,6 +3438,17 @@ var BrowserPrototype = {
&& (item.value instanceof Array ? item.value : [item.value]) && (item.value instanceof Array ? item.value : [item.value])
// XXX handle $keys and other stuff... // XXX handle $keys and other stuff...
.map(function(v){ .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)) ?
`<u class="key-hint">${k}</u>`
: k })
: v
var value = document.createElement('span') var value = document.createElement('span')
value.classList.add('text') value.classList.add('text')
value.innerHTML = v != null ? value.innerHTML = v != null ?
@ -3462,6 +3543,51 @@ var BrowserPrototype = {
// Custom events handlers... // 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... // NOTE: element alignment is done via the browser focus mechanics...
__focus__: function(evt, elem){ __focus__: function(evt, elem){
var that = this var that = this
@ -3471,15 +3597,8 @@ var BrowserPrototype = {
// NOTE: we will not remove this class on blur as it keeps // NOTE: we will not remove this class on blur as it keeps
// the selected element indicated... // the selected element indicated...
.run(function(){ .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') this.classList.add('focused')
// take care of visibility...
this.scrollIntoView({ this.scrollIntoView({
behavior: (that.options || {}).scrollBehavior || 'auto', behavior: (that.options || {}).scrollBehavior || 'auto',
block: 'nearest', block: 'nearest',
@ -3493,29 +3612,36 @@ var BrowserPrototype = {
&& getElem(elem) && getElem(elem)
.run(function(){ .run(function(){
this.classList.remove('focused') this.classList.remove('focused')
//this.blur() // refocus the dialog...
that.dom that.dom
&& that.dom.focus() }) }, && that.dom.focus() }) },
// NOTE: these simply update the state... // XXX should we only update the current elem???
__select__: function(){ __expand__: function(){ this.update() },
var selected = new Set(this.selected.map(getElem)) __collapse__: function(){ this.update() },
this.dom
&& this.dom.querySelectorAll('.selected') __select__: updateElemClass('add', 'selected'),
.forEach(function(e){ __deselect__: updateElemClass('remove', 'selected'),
selected.has(e) __disable__: updateElemClass('add', 'disabled'),
|| e.classList.remove('selected') }) __enable__: updateElemClass('remove', 'disabled'),
selected __hide__: updateElemClass('add', 'hidden'),
.forEach(function(e){ __show__: updateElemClass('remove', 'hidden'),
e.classList.add('selected') }) },
__deselect__: function(evt, elem){
this.__select__() },
// Custom events... // Custom events...
// //
// XXX make this different from html event... // XXX make this different from html event???
// XXX trigger this from kb handler... // 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(){
}), }),