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;
}
.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')
})),
])

View File

@ -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: <bool>,
//
// skipDisabled: <bool>,
// skipDisabledMode: 'node' | 'branch',
// skipDisabled: <bool> | '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: <index> | <query>,
// to: <index> | <query>,
// around: <index> | <query>,
@ -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 <u>X</u> 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)) ?
`<u class="key-hint">${k}</u>`
: 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(){
}),