mirror of
https://github.com/flynx/ImageGrid.git
synced 2025-10-28 18:00:09 +00:00
cleanup and minor tweaks...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
parent
21f440ed3b
commit
08d781cde0
@ -396,7 +396,8 @@ object.mixinFlat(function(){}, {
|
||||
.on('focus',
|
||||
function(){
|
||||
e.value = this.pathArray
|
||||
this.renderItem(e) },
|
||||
e.update()
|
||||
},
|
||||
tag)
|
||||
return make },
|
||||
|
||||
@ -442,7 +443,7 @@ object.mixinFlat(function(){}, {
|
||||
e.value = focused.doc
|
||||
|| focused.alt
|
||||
|| ' '
|
||||
this.renderItem(e) },
|
||||
e.update() },
|
||||
tag)
|
||||
return make },
|
||||
|
||||
@ -512,22 +513,22 @@ var BaseItemPrototype = {
|
||||
// multiple ways to get and index...
|
||||
|
||||
get pathArray(){
|
||||
var p = this.parent
|
||||
while(p.parent instanceof BaseBrowser){
|
||||
p = p.parent }
|
||||
return p ?
|
||||
p.pathOf(this)
|
||||
var r = (this.parent || {}).root
|
||||
return r ?
|
||||
r.pathOf(this)
|
||||
: undefined },
|
||||
get path(){
|
||||
return this.pathArray.join('/') },
|
||||
return (this.pathArray || []).join('/') },
|
||||
|
||||
get index(){
|
||||
var r = (this.parent || {}).root
|
||||
return r ?
|
||||
r.indexOf(this)
|
||||
: undefined },
|
||||
|
||||
// XXX local update/render...
|
||||
// XXX should we use these in the main render???
|
||||
update: function(){
|
||||
|
||||
// XXX
|
||||
|
||||
this.parent
|
||||
&& this.parent.render(this)
|
||||
return this
|
||||
},
|
||||
|
||||
@ -3177,10 +3178,8 @@ var BaseBrowserPrototype = {
|
||||
renderer.renderFinalize(null, items, null, context)
|
||||
// nested context -> return item list...
|
||||
: items } },
|
||||
/*/
|
||||
//*/
|
||||
|
||||
// XXX EXPERIMENTAL....
|
||||
//
|
||||
__renderer__: TextRenderer,
|
||||
// XXX need:
|
||||
// - section rendering... (DONE)
|
||||
@ -3195,8 +3194,12 @@ var BaseBrowserPrototype = {
|
||||
// XXX args parsing...
|
||||
// XXX
|
||||
var args = [...arguments]
|
||||
// XXX
|
||||
var list
|
||||
|
||||
// XXX add support for item queries...
|
||||
var list = (args[0] instanceof BaseItem || args[0] instanceof Array) ?
|
||||
[args.shift()].flat()
|
||||
: null
|
||||
|
||||
var filter
|
||||
|
||||
// NOTE: these only apply to the 'items' section...
|
||||
@ -3336,12 +3339,13 @@ var BaseBrowserPrototype = {
|
||||
}, options))
|
||||
// finalize render...
|
||||
.run(function(){
|
||||
return (!options.nonFinalized && render.root === that) ?
|
||||
return (list == null
|
||||
&& !options.nonFinalized
|
||||
&& render.root === that) ?
|
||||
render.finalize(this instanceof Array ?
|
||||
{[section]: this}
|
||||
: this, options)
|
||||
: this, options)
|
||||
: this }) },
|
||||
//*/
|
||||
|
||||
|
||||
// Events...
|
||||
@ -3998,21 +4002,6 @@ var HTMLItemPrototype = {
|
||||
this.dom ?
|
||||
this.elem.replaceWith(value)
|
||||
: (this.dom = value)},
|
||||
|
||||
// XXX EXPERIMENTAL...
|
||||
// XXX should we use these in the main render???
|
||||
update: function(){
|
||||
return object
|
||||
.parent(HTMLItemPrototype.update, this).call(this, ...arguments)
|
||||
.run(function(){
|
||||
var parent = this.parent
|
||||
|
||||
// XXX needs i and context...
|
||||
// XXX see issue with .elem above...
|
||||
this.elem = parent.renderItem(this, 0, {})
|
||||
|
||||
// XXX handle children...
|
||||
}) },
|
||||
}
|
||||
|
||||
var HTMLItem =
|
||||
@ -4136,9 +4125,6 @@ var updateElemClass = function(action, cls, handler){
|
||||
// XXX needs testing...
|
||||
// - partial rendering...
|
||||
// - local re-rendering...
|
||||
// - problems:
|
||||
// - re-rendering loses focus...
|
||||
// XXX doc...
|
||||
var HTMLRenderer =
|
||||
module.HTMLRenderer = {
|
||||
__proto__: BaseRenderer,
|
||||
@ -4486,7 +4472,7 @@ module.HTMLRenderer = {
|
||||
.forEach(function(key){
|
||||
// XXX should we break or warn???
|
||||
if(key in button_keys){
|
||||
throw new Error(`renderItem(..): button key already used: ${key}`) }
|
||||
throw new Error(`.elem(..): button key already used: ${key}`) }
|
||||
button_keys[keyboard.joinKey(keyboard.normalizeKey(key))] = button })
|
||||
// keep focus on the item containing the button -- i.e. if
|
||||
// we tab out of the item focus the item we get to...
|
||||
@ -4642,7 +4628,6 @@ module.HTMLRenderer = {
|
||||
|
||||
return render
|
||||
},
|
||||
// XXX BUG: focus is lost here for some reason...
|
||||
finalize: function(sections, options){
|
||||
var dialog = this.root
|
||||
|
||||
@ -4683,11 +4668,6 @@ module.HTMLRenderer = {
|
||||
dialog.dom = d
|
||||
|
||||
// set the scroll offset...
|
||||
// XXX does not work correctly for all cases yet...
|
||||
// XXX move this to a seporate method -- need to trigger this
|
||||
// on render that can affect scroll position, e.g. partial
|
||||
// render...
|
||||
// XXX need to trigger the setup for this from .render(..) itself...
|
||||
if(this.scroll_offset){
|
||||
var ref = dialog.focused || dialog.pagetop
|
||||
var scrolled = ref.dom.offsetParent
|
||||
@ -4699,7 +4679,7 @@ module.HTMLRenderer = {
|
||||
}
|
||||
|
||||
// keep focus where it is...
|
||||
var focused = this.focused
|
||||
var focused = dialog.focused
|
||||
focused
|
||||
&& focused.elem
|
||||
// XXX this will trigger the focus event...
|
||||
@ -5118,622 +5098,6 @@ var HTMLBrowserPrototype = {
|
||||
navigator.clipboard.writeText(text || this.path) },
|
||||
|
||||
|
||||
// Renderers (DOM)...
|
||||
//
|
||||
// XXX add a render mode where we replace specific element's .dom...
|
||||
// ...this could be done via:
|
||||
// .renderUpdate(..) -- like .renderFinalize(..) but replaces nodes...
|
||||
// needs more thought...
|
||||
// ...another idea would be to make the render flow "internal", i.e. each
|
||||
// item renders and places itself item "packing" is done internally
|
||||
// rather than via outer render combinators...
|
||||
// - create/get root context
|
||||
// - container -> create/clone context + append/replace in parent context
|
||||
// - item -> create/clone + append/replace self in parent context
|
||||
// - ...
|
||||
// - update root context
|
||||
// this approach should make the individual renderers self-contained, i.e.
|
||||
// rendering a single item would update it in the main tree... (needs thought)
|
||||
//
|
||||
//
|
||||
// Prepare context for maintaining scroll offset...
|
||||
//
|
||||
// XXX need a counterpart to this to finalize the context on any render...
|
||||
// ...or a way to tell .render(..) to render / re-render only
|
||||
// specific items...
|
||||
renderContext: function(context){
|
||||
context = context || {}
|
||||
|
||||
// prepare for maintaining the scroll position...
|
||||
// XXX need to do this pre any .render*(..) call...
|
||||
// ...something like:
|
||||
// this.getRenderContext(context)
|
||||
// should do the trick...
|
||||
// another way to go might be a context object, but that seems to be
|
||||
// complicating things...
|
||||
var ref = context.scroll_reference =
|
||||
context.scroll_reference
|
||||
|| this.focused
|
||||
|| this.pagetop
|
||||
context.scroll_offset =
|
||||
context.scroll_offset
|
||||
|| ((ref && ref.dom && ref.dom.offsetTop) ?
|
||||
ref.dom.offsetTop - ref.dom.offsetParent.scrollTop
|
||||
: null)
|
||||
|
||||
//context.scroll_offset && console.log('renderContext:', context.scroll_offset)
|
||||
|
||||
return context
|
||||
},
|
||||
//
|
||||
// This also does:
|
||||
// - save the rendered state to .dom
|
||||
// - wrap a list of nodes (nested list) in a div
|
||||
// - setup event handling
|
||||
// - init state...
|
||||
//
|
||||
// Format:
|
||||
// if list of items passed:
|
||||
// <div>
|
||||
// <!-- items -->
|
||||
// ...
|
||||
// </div>
|
||||
// or same as .renderList(..)
|
||||
//
|
||||
// XXX set scroll offset... should it be here?
|
||||
// ...would be nice to universally wrap any render operation
|
||||
// in a context getter/setter that would track the root call and
|
||||
// trigger an postRender event...
|
||||
// how can we Identify the root call??
|
||||
// ...the traditional way would be a stack -- pop last elem
|
||||
// means we are done...
|
||||
// ways to go with this:
|
||||
// - renderer wrapper/generator
|
||||
// - a 1:1 item-based render mechanic with single entry point...
|
||||
// ...this may be a problem as some of the renderers
|
||||
// can call others... (???)
|
||||
renderFinalize: function(header, items, footer, context){
|
||||
var that = this
|
||||
var context = this.renderContext(context)
|
||||
|
||||
var d = this.renderList(header, items, footer, context)
|
||||
var options = context.options || this.options || {}
|
||||
|
||||
// wrap the list (nested list) of nodes in a div...
|
||||
if(d instanceof Array){
|
||||
var c = document.createElement('div')
|
||||
d.classList.add('focusable')
|
||||
d.forEach(function(e){
|
||||
c.appendChild(e) })
|
||||
d = c
|
||||
}
|
||||
d.setAttribute('tabindex', '0')
|
||||
|
||||
// Setup basic event handlers...
|
||||
// keyboard...
|
||||
// NOTE: we are not doing:
|
||||
// d.addEventListener('keydown', this.keyPress.bind(this))
|
||||
// because we are abstracting the user from DOM events and
|
||||
// directly passing them parsed keys...
|
||||
d.addEventListener('keydown', function(evt){
|
||||
that.keyPress(that.keyboard.event2key(evt)) })
|
||||
// focus...
|
||||
d.addEventListener('click',
|
||||
function(e){
|
||||
e.stopPropagation()
|
||||
d.focus() })
|
||||
/* XXX this messes up the scrollbar...
|
||||
d.addEventListener('focus',
|
||||
function(){
|
||||
that.focused
|
||||
&& that.focused.elem.focus() })
|
||||
//*/
|
||||
|
||||
|
||||
// XXX should this be done here or in .render(..)???
|
||||
this.dom = d
|
||||
|
||||
// set the scroll offset...
|
||||
// XXX does not work correctly for all cases yet...
|
||||
// XXX move this to a seporate method -- need to trigger this
|
||||
// on render that can affect scroll position, e.g. partial
|
||||
// render...
|
||||
// XXX need to trigger the setup for this from .render(..) itself...
|
||||
if(context.scroll_offset){
|
||||
var ref = this.focused || this.pagetop
|
||||
var scrolled = ref.dom.offsetParent
|
||||
//scrolled.scrollTop =
|
||||
// ref.elem.offsetTop - scrolled.scrollTop - context.scroll_offset
|
||||
scrolled
|
||||
&& (scrolled.scrollTop =
|
||||
ref.elem.offsetTop - scrolled.scrollTop - context.scroll_offset)
|
||||
}
|
||||
|
||||
// keep focus where it is...
|
||||
var focused = this.focused
|
||||
focused
|
||||
&& focused.elem
|
||||
// XXX this will trigger the focus event...
|
||||
// ...can we do this without triggering new events???
|
||||
.focus()
|
||||
|
||||
return this.dom
|
||||
},
|
||||
//
|
||||
// Foramt:
|
||||
// <div class="browse-widget" tabindex="0">
|
||||
// <!-- header -->
|
||||
// ...
|
||||
//
|
||||
// <!-- list -->
|
||||
// <div class="list v-block">
|
||||
// <!-- items -->
|
||||
// ...
|
||||
// </div>
|
||||
//
|
||||
// <!-- footer -->
|
||||
// ...
|
||||
// </div>
|
||||
//
|
||||
renderList: function(header, items, footer, context){
|
||||
var that = this
|
||||
var context = this.renderContext(context)
|
||||
var options = context.options || this.options || {}
|
||||
|
||||
// dialog (container)...
|
||||
var dialog = document.createElement('div')
|
||||
dialog.classList.add('browse-widget')
|
||||
dialog.setAttribute('tabindex', '0')
|
||||
// HACK?: prevent dialog from grabbing focus from item...
|
||||
dialog.addEventListener('mousedown',
|
||||
function(evt){ evt.stopPropagation() })
|
||||
|
||||
// header...
|
||||
header
|
||||
&& !options.hideListHeader
|
||||
&& dialog.appendChild(this.renderListHeader(header, context))
|
||||
|
||||
// list...
|
||||
var list = document.createElement('div')
|
||||
list.classList.add('list', 'v-block', 'items')
|
||||
// prevent scrollbar from grabbing focus...
|
||||
list.addEventListener('mousedown',
|
||||
function(evt){ evt.stopPropagation() })
|
||||
items
|
||||
.forEach(function(item){
|
||||
list.appendChild(item instanceof Array ?
|
||||
that.renderGroup(item)
|
||||
: item) })
|
||||
dialog.appendChild(list)
|
||||
|
||||
// footer...
|
||||
footer
|
||||
&& !options.hideListFooter
|
||||
&& dialog.appendChild(this.renderListFooter(footer, context))
|
||||
|
||||
return dialog
|
||||
},
|
||||
//
|
||||
// Foramt:
|
||||
// <div class="path v-block">
|
||||
// <!-- list -->
|
||||
// <div class="list v-block header">
|
||||
// <!-- items -->
|
||||
// ...
|
||||
// </div>
|
||||
// </div>
|
||||
//
|
||||
// XXX populate this...
|
||||
// XXX make this an item???
|
||||
renderListHeader: function(items, context){
|
||||
var elem = this.renderList(null, items, null, context).firstChild
|
||||
// XXX should we replace 'list' or add 'header'
|
||||
elem.classList.replace('items', 'header')
|
||||
return elem },
|
||||
renderListFooter: function(items, context){
|
||||
var elem = this.renderList(null, items, null, context).firstChild
|
||||
// XXX should we replace 'list' or add 'footer'
|
||||
elem.classList.replace('items', 'footer')
|
||||
return elem },
|
||||
//
|
||||
// Format:
|
||||
// <div class="list">
|
||||
// <!-- header (optional) -->
|
||||
// ...
|
||||
//
|
||||
// <!-- children (optional) -->
|
||||
// ...
|
||||
// </div>
|
||||
//
|
||||
renderNested: function(header, children, item, context){
|
||||
var that = this
|
||||
var context = this.renderContext(context)
|
||||
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.itemLocalEvents || [])
|
||||
.forEach(function(evt){
|
||||
e.addEventListener(evt, stopPropagation) })
|
||||
|
||||
// header...
|
||||
header
|
||||
&& e.appendChild(header)
|
||||
|
||||
// items...
|
||||
children instanceof Node ?
|
||||
e.appendChild(children)
|
||||
: children instanceof Array ?
|
||||
children
|
||||
.forEach(function(item){
|
||||
e.appendChild(item) })
|
||||
: null
|
||||
|
||||
item.dom = e
|
||||
|
||||
return e
|
||||
},
|
||||
// NOTE: this is the similar to .renderItem(..)
|
||||
renderNestedHeader: function(item, i, context){
|
||||
var that = this
|
||||
return this.renderItem(item, i, context)
|
||||
// update dom...
|
||||
.run(function(){
|
||||
this.classList.add('sub-list-header', 'traversable')
|
||||
item.collapsed
|
||||
&& this.classList.add('collapsed')
|
||||
}) },
|
||||
//
|
||||
// Format:
|
||||
// <div class="group">
|
||||
// <!-- items -->
|
||||
// ...
|
||||
// </div>
|
||||
//
|
||||
renderGroup: function(items, context){
|
||||
var context = this.renderContext(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:
|
||||
// <div value="value_json" class="item .." tabindex="0" ..>
|
||||
// <!-- value -->
|
||||
// <div class="text">value_a</div>
|
||||
// <div class="text">value_b</div>
|
||||
// ...
|
||||
//
|
||||
// <!-- buttons (optional) -->
|
||||
// <div class="button">button_a_html</div>
|
||||
// <div class="button">button_b_html</div>
|
||||
// ...
|
||||
// </div>
|
||||
//
|
||||
// NOTE: DOM events trigger Browser events but not the other way
|
||||
// around. It is not recommended to use DOM events directly.
|
||||
//
|
||||
// XXX need to figure out an intuitive behavior of focus + disable/enable...
|
||||
// ...do we skip disabled elements?
|
||||
// ...can a disabled item be focused?
|
||||
// ...how do we collapse/expand a disabled root?
|
||||
// ...what do we focus when toggleing disabled?
|
||||
// XXX handle .options.focusDisabledItems correctly...
|
||||
// - tabindex -- DONE
|
||||
// - ???
|
||||
// XXX show button global/local keys...
|
||||
renderItem: function(item, i, context){
|
||||
var that = this
|
||||
var context = this.renderContext(context)
|
||||
var options = (context || {}).options || this.options || {}
|
||||
if(options.hidden && !options.renderHidden){
|
||||
return null
|
||||
}
|
||||
var section = item.section || options.section
|
||||
|
||||
// helpers...
|
||||
// XXX we need to more carefully test the value to avoid name clashes...
|
||||
var resolveValue = function(value, context, exec_context){
|
||||
var htmlhandler = typeof(value) == typeof('str') ?
|
||||
that.parseStringHandler(value, exec_context)
|
||||
: null
|
||||
return value instanceof Function ?
|
||||
value.call(that, item)
|
||||
: htmlhandler
|
||||
&& htmlhandler.action in context
|
||||
&& context[htmlhandler.action] instanceof Function ?
|
||||
context[htmlhandler.action]
|
||||
.call(that, item, ...htmlhandler.arguments)
|
||||
: value }
|
||||
var setDOMValue = function(target, value){
|
||||
value instanceof HTMLElement ?
|
||||
target.appendChild(value)
|
||||
: (typeof(jQuery) != 'undefined' && value instanceof jQuery) ?
|
||||
value.appendTo(target)
|
||||
: (target.innerHTML = value)
|
||||
return target }
|
||||
var doTextKeys = function(text, doKey){
|
||||
return text.replace(/\$\w/g,
|
||||
function(k){
|
||||
// forget the '$'...
|
||||
k = k[1]
|
||||
return (doKey && doKey(k)) ?
|
||||
`<u class="key-hint">${k}</u>`
|
||||
: k }) }
|
||||
|
||||
// special-case: item.html...
|
||||
if(item.html){
|
||||
// NOTE: this is a bit of a cheat, but it saves us from either
|
||||
// parsing or restricting the format...
|
||||
var tmp = document.createElement('div')
|
||||
tmp.innerHTML = item.html
|
||||
var elem = item.dom = tmp.firstElementChild
|
||||
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 = item.text
|
||||
|
||||
// classes...
|
||||
elem.classList.add(...['item']
|
||||
// user classes...
|
||||
.concat((item['class'] || item.cls || [])
|
||||
// parse space-separated class strings...
|
||||
.run(function(){
|
||||
return this instanceof Array ?
|
||||
this
|
||||
: this.split(/\s+/g) }))
|
||||
// special classes...
|
||||
.concat(
|
||||
(options.shorthandItemClasses || [])
|
||||
.filter(function(cls){
|
||||
return !!item[cls] })))
|
||||
|
||||
// attrs...
|
||||
;(item.disabled && !options.focusDisabledItems)
|
||||
|| elem.setAttribute('tabindex', '0')
|
||||
Object.entries(item.attrs || {})
|
||||
// shorthand attrs...
|
||||
.concat((options.shorthandItemAttrs || [])
|
||||
.map(function(key){
|
||||
return [key, item[key]] }))
|
||||
.forEach(function([key, value]){
|
||||
value !== undefined
|
||||
&& elem.setAttribute(key, value) })
|
||||
;(item.value == null
|
||||
|| item.value instanceof Object)
|
||||
|| elem.setAttribute('value', item.text)
|
||||
;(item.value == null
|
||||
|| item.value instanceof Object
|
||||
|| item.alt != item.text)
|
||||
&& elem.setAttribute('alt', item.alt)
|
||||
|
||||
// values...
|
||||
text != null
|
||||
&& (item.value instanceof Array ?
|
||||
item.value
|
||||
: [item.value])
|
||||
// handle $keys and other stuff...
|
||||
// NOTE: the actual key setup is done in .__preRender__(..)
|
||||
// see that for more info...
|
||||
.map(function(v){
|
||||
// handle key-shortcuts $K...
|
||||
v = typeof(v) == typeof('str') ?
|
||||
doTextKeys(v,
|
||||
function(k){
|
||||
return (item.keys || [])
|
||||
.includes(that.keyboard.normalizeKey(k)) })
|
||||
: v
|
||||
|
||||
var value = document.createElement('span')
|
||||
value.classList.add('text')
|
||||
|
||||
// set the value...
|
||||
setDOMValue(value,
|
||||
resolveValue(v, that))
|
||||
|
||||
elem.appendChild(value)
|
||||
})
|
||||
|
||||
// system events...
|
||||
elem.addEventListener('click',
|
||||
function(evt){
|
||||
evt.stopPropagation()
|
||||
// NOTE: if an item is disabled we retain its expand/collapse
|
||||
// functionality...
|
||||
// XXX revise...
|
||||
item.disabled ?
|
||||
that.toggleCollapse(item)
|
||||
: that.open(item, text, elem) })
|
||||
elem.addEventListener('focus',
|
||||
function(){
|
||||
// NOTE: we do not retrigger focus on an item if it's
|
||||
// already focused...
|
||||
that.focused !== item
|
||||
// only trigger focus on gettable items...
|
||||
// ...i.e. items in the main section excluding headers
|
||||
// and footers...
|
||||
&& that.focus(item) })
|
||||
elem.addEventListener('contextmenu',
|
||||
function(evt){
|
||||
evt.preventDefault()
|
||||
that.menu(item) })
|
||||
// user events...
|
||||
Object.entries(item.events || {})
|
||||
// shorthand DOM events...
|
||||
.concat((options.shorthandItemEvents || [])
|
||||
.map(function(evt){
|
||||
return [evt, item[evt]] }))
|
||||
// setup the handlers...
|
||||
.forEach(function([evt, handler]){
|
||||
handler
|
||||
&& elem.addEventListener(evt, handler.bind(that)) })
|
||||
|
||||
// buttons...
|
||||
var button_keys = {}
|
||||
// XXX migrate button inheritance...
|
||||
var buttons = (item.buttons
|
||||
|| (section == 'header'
|
||||
&& (options.headerButtons || []))
|
||||
|| (section == 'footer'
|
||||
&& (options.footerButtons || []))
|
||||
|| options.itemButtons
|
||||
|| [])
|
||||
// resolve buttons from library...
|
||||
.map(function(button){
|
||||
return button instanceof Array ?
|
||||
button
|
||||
// XXX reference the actual make(..) and not Items...
|
||||
: Items.buttons[button] instanceof Function ?
|
||||
[Items.buttons[button].call(that, item)].flat()
|
||||
: Items.buttons[button] || button })
|
||||
// NOTE: keep the order unsurprising -- first defined, first from left...
|
||||
.reverse()
|
||||
var stopPropagation = function(evt){ evt.stopPropagation() }
|
||||
buttons
|
||||
.forEach(function([html, handler, ...rest]){
|
||||
var force = (rest[0] === true
|
||||
|| rest[0] === false
|
||||
|| rest[0] instanceof Function) ?
|
||||
rest.shift()
|
||||
: undefined
|
||||
var metadata = rest.shift() || {}
|
||||
|
||||
// metadata...
|
||||
var cls = metadata.cls || []
|
||||
cls = cls instanceof Function ?
|
||||
cls.call(that, item)
|
||||
: cls
|
||||
cls = cls instanceof Array ?
|
||||
cls
|
||||
: cls.split(/\s+/g)
|
||||
var alt = metadata.alt
|
||||
alt = alt instanceof Function ?
|
||||
alt.call(that, item)
|
||||
: alt
|
||||
var keys = metadata.keys
|
||||
|
||||
var button = document.createElement('div')
|
||||
button.classList.add('button', ...cls)
|
||||
alt
|
||||
&& button.setAttribute('alt', alt)
|
||||
|
||||
// button content...
|
||||
var text_keys = []
|
||||
var v = resolveValue(html, Items.buttons, {item})
|
||||
setDOMValue(button,
|
||||
typeof(v) == typeof('str') ?
|
||||
doTextKeys(v,
|
||||
function(k){
|
||||
k = that.keyboard.normalizeKey(k)
|
||||
return options.disableButtonSortcuts ?
|
||||
false
|
||||
: !text_keys.includes(k)
|
||||
&& text_keys.push(k) })
|
||||
: v)
|
||||
keys = text_keys.length > 0 ?
|
||||
(keys || []).concat(text_keys)
|
||||
: keys
|
||||
|
||||
// non-disabled button...
|
||||
if(force instanceof Function ?
|
||||
force.call(that, item)
|
||||
: (force || !item.disabled) ){
|
||||
button.setAttribute('tabindex', '0')
|
||||
// events to keep in buttons...
|
||||
;(options.buttonLocalEvents || options.itemLocalEvents || [])
|
||||
.forEach(function(evt){
|
||||
button.addEventListener(evt, stopPropagation) })
|
||||
// button keys...
|
||||
keys && !options.disableButtonSortcuts
|
||||
&& (keys instanceof Array ? keys : [keys])
|
||||
.forEach(function(key){
|
||||
// XXX should we break or warn???
|
||||
if(key in button_keys){
|
||||
throw new Error(`renderItem(..): button key already used: ${key}`) }
|
||||
button_keys[keyboard.joinKey(keyboard.normalizeKey(key))] = button })
|
||||
// keep focus on the item containing the button -- i.e. if
|
||||
// we tab out of the item focus the item we get to...
|
||||
button.addEventListener('focus', function(){
|
||||
item.focused
|
||||
// only focus items in the main section,
|
||||
// outside of headers and footers...
|
||||
|| that.focus(item)
|
||||
&& button.focus() })
|
||||
// main button action (click/enter)...
|
||||
// XXX should there be a secondary action (i.e. shift-enter)???
|
||||
if(handler){
|
||||
var func = handler instanceof Function ?
|
||||
handler
|
||||
// string handler -> that.<handler>(item)
|
||||
: function(evt, ...args){
|
||||
var a = that.parseStringHandler(
|
||||
handler,
|
||||
// button handler arg namespace...
|
||||
{
|
||||
event: evt,
|
||||
item: item,
|
||||
// NOTE: if we are not focusing
|
||||
// on button click this may
|
||||
// be different from item...
|
||||
focused: that.focused,
|
||||
button: html,
|
||||
})
|
||||
that[a.action](...a.arguments) }
|
||||
|
||||
// handle clicks and keyboard...
|
||||
button.addEventListener('click', func.bind(that))
|
||||
// NOTE: we only trigger buttons on Enter and do
|
||||
// not care about other keys...
|
||||
button.addEventListener('keydown',
|
||||
function(evt){
|
||||
var k = keyboard.event2key(evt)
|
||||
if(k.includes('Enter')){
|
||||
event.stopPropagation()
|
||||
func.call(that, evt, item) } }) }
|
||||
}
|
||||
|
||||
elem.appendChild(button)
|
||||
})
|
||||
|
||||
// button shortcut keys...
|
||||
Object.keys(button_keys).length > 0
|
||||
&& elem.addEventListener('keydown',
|
||||
function(evt){
|
||||
var k = keyboard.joinKey(keyboard.event2key(evt))
|
||||
if(k in button_keys){
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
button_keys[k].focus()
|
||||
// XXX should this be optional???
|
||||
button_keys[k].click() } })
|
||||
|
||||
item.dom = elem
|
||||
// XXX for some reason this messes up navigation...
|
||||
// to reproduce:
|
||||
// - select element with children
|
||||
// - press right
|
||||
// -> blur current elem
|
||||
// -> next elem not selected...
|
||||
//item.elem = elem
|
||||
|
||||
return elem
|
||||
},
|
||||
|
||||
|
||||
// Events extensions...
|
||||
//
|
||||
// NOTE: this will also kill any user-set keys for disabled/hidden items...
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user