diff --git a/ui (gen4)/lib/widget/browse2.js b/ui (gen4)/lib/widget/browse2.js index d29c0785..f5d42786 100755 --- a/ui (gen4)/lib/widget/browse2.js +++ b/ui (gen4)/lib/widget/browse2.js @@ -780,9 +780,9 @@ module.BaseRenderer = { root: null, // component renderers... - elem: function(elem, index, path, options){ + elem: function(item, index, path, options){ throw new Error('.elem(..): Not implemented.') }, - inline: function(elem, lst, index, path, options){ + inline: function(item, lst, index, path, options){ throw new Error('.inline(..): Not implemented.') }, nest: function(header, lst, index, path, options){ throw new Error('.nest(..): Not implemented.') }, @@ -807,12 +807,12 @@ var TextRenderer = module.TextRenderer = { __proto__: BaseRenderer, - elem: function(elem, index, path, options){ + elem: function(item, index, path, options){ return path .slice(0, -1) .map(function(e){ return ' '}) - .join('') + elem.id }, - inline: function(elem, lst, index, path, options){ + .join('') + item.id }, + inline: function(item, lst, index, path, options){ return lst }, // XXX if header is null then render a headless nested block... nest: function(header, lst, index, path, options){ @@ -839,9 +839,9 @@ module.PathRenderer = { // renderers... // // render paths... - elem: function(elem, index, path, options){ + elem: function(item, index, path, options){ return path.join('/') }, - inline: function(elem, lst, index, path, options){ + inline: function(item, lst, index, path, options){ return lst }, // XXX if header is null then render a headless nested block... nest: function(header, lst, index, path, options){ @@ -3008,6 +3008,7 @@ var BaseBrowserPrototype = { // do not reconstruct the ones already present... // XXX should from/to/around/count be a feature of this or of .walk(..)??? // XXX might be a good idea to use this.root === this instead of context.root === this + //* render: function(options, renderer, context){ renderer = renderer || this context = renderer.renderContext(context) @@ -3176,6 +3177,7 @@ var BaseBrowserPrototype = { renderer.renderFinalize(null, items, null, context) // nested context -> return item list... : items } }, + /*/ // XXX EXPERIMENTAL.... // @@ -3186,7 +3188,7 @@ var BaseBrowserPrototype = { // - ability to render separate items/sub-trees or lists of items... // ...pass the list to .walk(..), i.e. .walk(list/query, ...) // XXX doc... - render2: function(options, renderer){ + render: function(options, renderer){ var that = this // XXX args parsing... @@ -3233,7 +3235,7 @@ var BaseBrowserPrototype = { // used as a means to calculate lengths of nested blocks rendered - // via .render2(..) + // via .render(..) var l return ((render.root === this && section instanceof Array) ? // render list of sections... @@ -3243,7 +3245,7 @@ var BaseBrowserPrototype = { // is rendered for all nested browsers... section .reduce(function(res, name){ - res[name] = that.render2( + res[name] = that.render( Object.assign({}, options, { @@ -3278,21 +3280,21 @@ var BaseBrowserPrototype = { p = base_path.concat(p) // children... - // do not go down child browsers -- use their .render2(..) + // do not go down child browsers -- use their .render(..) // NOTE: doing so will require us to manually handle some // of the options that would otherwise be handled // by .walk(..)... var inlined = (e instanceof BaseBrowser || e.children instanceof BaseBrowser) && !children(false) - // get children either via .walk(..) or .render2(..) + // get children either via .walk(..) or .render(..) // depending on item type... var getChildren = function(){ return inlined ? (l = (e.children instanceof BaseBrowser ? e.children : e) - .render2(options, render, i+1, p)) + .render(options, render, i+1, p)) : children(true) } // do the actual rendering... @@ -3333,6 +3335,7 @@ var BaseBrowserPrototype = { {[section]: this} : this, options) : this }) }, + //*/ // Events... @@ -4124,25 +4127,515 @@ var updateElemClass = function(action, cls, handler){ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Renderer... -// XXX +// XXX needs testing... +// - structure seems to be fine... +// - rename .render(..) -> .render(..) and do a full test... +// - problems: +// - inlined/nested dialogs do not get button config... +// ...options not passed down correctly? +// - re-rendering loses focus... +// XXX doc... var HTMLRenderer = module.HTMLRenderer = { __proto__: BaseRenderer, - elem: function(elem, index, path, options){ + // secondary renderers... + // + // base dialog structure... + dialog: function(sections, options){ + var that = this + var {header, items, footer} = sections + + // 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.dialogHeader(header, options)) + + // 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.dialogFooter(footer, options)) + + return dialog }, - inline: function(elem, lst, index, path, options){ + dialogHeader: function(items, options){ + var elem = this.dialog({items}, options).firstChild + elem.classList.replace('items', 'header') + return elem }, + dialogFooter: function(items, options){ + var elem = this.dialog({items}, options).firstChild + elem.classList.replace('items', 'footer') + return elem }, + // custom elements... + headerElem: function(item, index, path, options){ + return this.elem(...arguments) + // update dom... + .run(function(){ + this.classList.add('sub-list-header', 'traversable') + item.collapsed + && this.classList.add('collapsed') }) }, + + + // base renderers... + // + elem: function(item, index, path, options){ + var that = this + var browser = this.root + 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') ? + browser.parseStringHandler(value, exec_context) + : null + return value instanceof Function ? + value.call(browser, item) + : htmlhandler + && htmlhandler.action in context + && context[htmlhandler.action] instanceof Function ? + context[htmlhandler.action] + .call(browser, 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)) ? + `${k}` + : 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(browser.keyboard.normalizeKey(k)) }) + : v + + var value = document.createElement('span') + value.classList.add('text') + + // set the value... + setDOMValue(value, + resolveValue(v, browser)) + + 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 ? + browser.toggleCollapse(item) + : browser.open(item, text, elem) }) + elem.addEventListener('focus', + function(){ + // NOTE: we do not retrigger focus on an item if it's + // already focused... + browser.focused !== item + // only trigger focus on gettable items... + // ...i.e. items in the main section excluding headers + // and footers... + && browser.focus(item) }) + elem.addEventListener('contextmenu', + function(evt){ + evt.preventDefault() + browser.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(browser)) }) + + // 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(browser, 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(browser, item) + : cls + cls = cls instanceof Array ? + cls + : cls.split(/\s+/g) + var alt = metadata.alt + alt = alt instanceof Function ? + alt.call(browser, 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 = browser.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(browser, 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... + || browser.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 -> browser.(item) + : function(evt, ...args){ + var a = browser.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: browser.focused, + button: html, + }) + browser[a.action](...a.arguments) } + + // handle clicks and keyboard... + button.addEventListener('click', func.bind(browser)) + // 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(browser, 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 }, + inline: function(item, lst, index, path, options){ + var e = document.createElement('div') + e.classList.add('group') + lst + // XXX is this wrong??? + .flat(Infinity) + .forEach(function(item){ + e.appendChild(item) }) + return e }, // XXX add support for headless nested blocks... nest: function(header, lst, index, path, options){ + var that = this + + // 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... + // XXX make this optional... + e.appendChild(this.headerElem(header, index, path, options)) + + // items... + lst instanceof Node ? + e.appendChild(lst) + : lst instanceof Array ? + lst + .forEach(function(item){ + e.appendChild(item) }) + : null + + header.dom = e + + return e }, - //start: function(root, options){ - //}, - //finalize: function(sections, options){ - //}, -} + // life-cycle... + // + start: function(root, options){ + var render = object.parent(HTMLRenderer.start, this).call(this, root, options) + + var browser = render.root + + // prepare for maintaining the scroll position... + // XXX need to do this pre any .render*(..) call... + // ...something like: + // this.getRenderContext(render) + // should do the trick... + // another way to go might be a render object, but that seems to be + // complicating things... + var ref = render.scroll_reference = + render.scroll_reference + || browser.focused + || browser.pagetop + render.scroll_offset = + render.scroll_offset + || ((ref && ref.dom && ref.dom.offsetTop) ? + ref.dom.offsetTop - ref.dom.offsetParent.scrollTop + : null) + + //render.scroll_offset && console.log('renderContext:', render.scroll_offset) + + return render + }, + finalize: function(sections, options){ + var dialog = this.root + + var d = this.dialog(sections, 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){ + dialog.keyPress(dialog.keyboard.event2key(evt)) }) + // focus... + d.addEventListener('click', + function(e){ + e.stopPropagation() + d.focus() }) + /* XXX this messes up the scrollbar... + d.addEventListener('focus', + function(){ + dialog.focused + && dialog.focused.elem.focus() }) + //*/ + + + // XXX should this be done here or in .render(..)??? + 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 + //scrolled.scrollTop = + // ref.elem.offsetTop - scrolled.scrollTop - this.scroll_offset + scrolled + && (scrolled.scrollTop = + ref.elem.offsetTop - scrolled.scrollTop - this.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 dialog.dom + }, +} // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -4158,6 +4651,7 @@ var HTMLBrowserClassPrototype = { var HTMLBrowserPrototype = { __proto__: BaseBrowser.prototype, __item__: HTMLItem, + __renderer__: HTMLRenderer, options: {