refactored .make(..) and Items (Make(..))

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2019-07-16 18:52:18 +03:00
parent 4fac28a8a9
commit b7121808cb

View File

@ -81,348 +81,349 @@ var collectItems = function(make, items){
//--------------------------------------------------------------------- //---------------------------------------------------------------------
// Item constructors... // Item constructors...
//
// XXX general design:
// - each of these can take either a value or a function (constructor)
// - the function has access to Items.* and context
// - the constructor can be called from two contexts:
// - external
// called from the module or as a function...
// calls the passed constructor (passing context)
// builds the container
// - nested
// called from constructor function...
// calls constructor (if applicable)
// builds item(s)
// XXX need a way to pass container constructors (a-la ui-widgets dialog containers)
// - passing through the context (this) makes this more flexible...
// - passing via args fixes the signature which is a good thing...
//
//
// XXX var Items =
// XXX can't use Object.assign(..) here as it will not copy props... object.mixinFlat(function(){}, {
var Items = module.items = function(){} dialog: null,
called: false,
// placeholders... // Props...
Items.dialog = null //
Items.items = null // NOTE: writing to .items will reset .called to false...
__items: undefined,
get items(){
return this.__items },
set items(value){
this.called = false
this.__items = value },
buttons: {
//
// Draw checked checkboz is <attr> is true...
// Checkbox('attr')
//
// Draw checked checkboz is <attr> is false...
// Checkbox('!attr')
//
// XXX rename -- distinguish from actual button...
Checkbox: function(item, attr){
return (attr[0] == '!'
&& !item[attr.slice(1)])
|| item[attr] ?
'&#9744;'
: '&#9745;' },
// XXX can we make these not use the same icon...
ToggleDisabled: [
'Checkbox: "disabled"',
'toggleDisabled: item',
true,
{
alt: 'Disable/enable item',
cls: 'toggle-disabled',
}],
ToggleHidden: [
'Checkbox: "hidden"',
'toggleHidden: item',
{
alt: 'Show/hide item',
cls: 'toggle-hidden',
}],
ToggleSelected: [
'Checkbox: "selected"',
'toggleSelect: item',
{
alt: 'Select/deselect item',
cls: 'toggle-select',
}],
// NOTE: this button is disabled for all items but the ones with .children...
ToggleCollapse: [
function(item){
return !item.children ?
// placeholder...
'&nbsp;'
: item.collapsed ?
'+'
: '-' },
'toggleCollapse: item',
// disable button for all items that do not have children...
function(item){
return 'children' in item },
{
alt: 'Collapse/expand item',
cls: function(item){
return 'children' in item ?
'toggle-collapse'
: ['toggle-collapse', 'blank'] },
}],
// XXX delete button -- requires .markDelete(..) action...
Delete: [
'&times;',
'markDelete: item',
{
alt: 'Mark item for deletion',
cls: 'toggle-delete',
//keys: ['Delete', 'd'],
}],
},
// Last item created... // Getters...
// XXX not sure about this...
// XXX should this be a prop??? // Last item created...
Items.last = function(){ // XXX not sure about this...
return (this.items || [])[this.items.length - 1] } // XXX should this be a prop???
last: function(){
return (this.items || [])[this.items.length - 1] },
// Group a set of items... // Constructors/modifiers...
//
// .group(make(..), ..) // Group a set of items...
// .group([make(..), ..]) //
// -> make // .group(make(..), ..)
// // .group([make(..), ..])
// // -> make
// Example: //
// make.group( //
// make('made item'), // Example:
// 'literal item', // make.group(
// ...) // make('made item'),
// // 'literal item',
// // ...)
// NOTE: see notes to collectItems(..) for more info... //
// //
// XXX do we need to pass options to groups??? // NOTE: see notes to collectItems(..) for more info...
Items.group = function(...items){ //
var that = this // XXX do we need to pass options to groups???
items = items.length == 1 && items[0] instanceof Array ? group: function(...items){
items[0] var that = this
: items items = items.length == 1 && items[0] instanceof Array ?
// replace the items with the group... items[0]
this.items.splice(this.items.length, 0, collectItems(this, items)) : items
return this // replace the items with the group...
} this.items.splice(this.items.length, 0, collectItems(this, items))
return this },
// Place list in a sub-list of item...
//
// Examples:
// make.nest('literal header', [
// 'literal item',
// make('item'),
// ...
// ])
//
// make.nest(make('header'), [
// 'literal item',
// make('item'),
// ...
// ])
//
nest: function(item, list, options){
options = options || {}
//options = Object.assign(Object.create(this.options || {}), options || {})
options = Object.assign({},
{ children: list instanceof Array ?
collectItems(this, list)
: list },
options)
return item === this ?
((this.last().children = options.children), this)
: this(item, options) },
// Place list in a sub-list of item... // Wrappers...
//
// Examples: // this is here for uniformity...
// make.nest('literal header', [ Item: function(value, options){
// 'literal item', return this(...arguments) },
// make('item'),
// ... Empty: function(value){},
// ])
// Separator: function(){
// make.nest(make('header'), [ return this('---') },
// 'literal item', Spinner: function(){
// make('item'), return this('...') },
// ...
// ]) Heading: function(value, options){
// var cls = 'heading'
Items.nest = function(item, list, options){ options = options || {}
options = options || {} options.cls = options.cls instanceof Array ?
//options = Object.assign(Object.create(this.options || {}), options || {}) options.cls.concat([cls])
options = Object.assign({}, : typeof(options.cls) == typeof('str') ?
{ children: list instanceof Array ? options.cls +' '+ cls
collectItems(this, list) : [cls]
: list }, options.buttons = options.buttons
options) || this.dialog.options.headingButtons
return item === this ? return this(value, options) },
((this.last().children = options.children), this) Action: function(value, options){},
: this(item, options) ConfirmAction: function(value){},
} Editable: function(value){},
// lists...
List: function(values){},
EditableList: function(values){},
EditablePinnedList: function(values){},
// Special list components...
//Items.ListPath = function(){},
//Items.ListTitle = function(){},
// XXX EXPERIMENTAL...
//
// options:
// {
// showOKButton: <bool>,
//
// }
//
Confirm: function(message, accept, reject, options){
return this(message,
Object.assign({
// XXX should the user be able to merge buttons from options???
buttons: [
...(reject instanceof Function ?
[['$Cancel', reject]]
: []),
...(accept instanceof Function
&& (options || {}).showOKButton ?
[['$OK', accept]]
: []), ],
},
accept ?
{open: accept}
: {},
options || {})) },
// Generators...
//
// A generator is a function that creates 1 or more elements and sets up
// the appropriate interactions...
//
// NOTE: these can work both as item generators called from inside
// .make(..), i.e. as methods of the make constructor, or as
// generators assigned to .__header__ / .__items__ / .__footer__
// attributes...
// NOTE: when re-using these options.id needs to be set so as not to
// overwrite existing instances data and handlers...
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Make item generator...
// Buttons... //
var buttons = Items.buttons = {} makeDisplayItem: function(text, options){
var args = [...arguments]
return function(make, options){
make(...args) } },
// // Make confirm item generator...
// Draw checked checkboz is <attr> is true... //
// Checkbox('attr') // XXX move this to Item.Confirm(..) and reuse that...
// makeDisplayConfirm: function(message, accept, reject){
// Draw checked checkboz is <attr> is false... return this.makeDisplayItem(message, {
// Checkbox('!attr')
//
// XXX rename -- distinguish from actual button...
buttons.Checkbox = function(item, attr){
return (attr[0] == '!'
&& !item[attr.slice(1)])
|| item[attr] ?
'&#9744;'
: '&#9745;' }
// XXX can we make these not use the same icon...
buttons.ToggleDisabled = [
'Checkbox: "disabled"',
'toggleDisabled: item',
true,
{
alt: 'Disable/enable item',
cls: 'toggle-disabled',
}]
buttons.ToggleHidden = [
'Checkbox: "hidden"',
'toggleHidden: item',
{
alt: 'Show/hide item',
cls: 'toggle-hidden',
}]
buttons.ToggleSelected = [
'Checkbox: "selected"',
'toggleSelect: item',
{
alt: 'Select/deselect item',
cls: 'toggle-select',
}]
// NOTE: this button is disabled for all items but the ones with .children...
buttons.ToggleCollapse = [
function(item){
return !item.children ?
// placeholder...
'&nbsp;'
: item.collapsed ?
'+'
: '-' },
'toggleCollapse: item',
// disable button for all items that do not have children...
function(item){
return 'children' in item },
{
alt: 'Collapse/expand item',
cls: function(item){
return 'children' in item ?
'toggle-collapse'
: ['toggle-collapse', 'blank'] },
}]
// XXX delete button -- requires .markDelete(..) action...
buttons.Delete = [
'&times;',
'markDelete: item',
{
alt: 'Mark item for deletion',
cls: 'toggle-delete',
//keys: ['Delete', 'd'],
}]
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Wrappers...
// this is here for uniformity...
Items.Item = function(value, options){ return this(...arguments) }
Items.Empty = function(value){}
Items.Separator = function(){ return this('---') }
Items.Spinner = function(){ return this('...') }
Items.Heading = function(value, options){
var cls = 'heading'
options = options || {}
options.cls = options.cls instanceof Array ?
options.cls.concat([cls])
: typeof(options.cls) == typeof('str') ?
options.cls +' '+ cls
: [cls]
options.buttons = options.buttons
|| this.dialog.options.headingButtons
return this(value, options) }
Items.Action = function(value, options){}
Items.ConfirmAction = function(value){}
Items.Editable = function(value){}
// lists...
Items.List = function(values){}
Items.EditableList = function(values){}
Items.EditablePinnedList = function(values){}
// Special list components...
//Items.ListPath = function(){}
//Items.ListTitle = function(){}
// XXX EXPERIMENTAL...
//
// options:
// {
// showOKButton: <bool>,
//
// }
//
Items.Confirm = function(message, accept, reject, options){
return this(message,
Object.assign({
// XXX should the user be able to merge buttons from options???
buttons: [ buttons: [
...(reject instanceof Function ? ...[reject instanceof Function ?
[['$Cancel', reject]] ['Cancel', reject]
: []), : []],
...(accept instanceof Function ...[accept instanceof Function ?
&& (options || {}).showOKButton ? ['OK', accept]
[['$OK', accept]] : []], ], }) },
: []), ],
},
accept ?
{open: accept}
: {},
options || {})) }
// Focused item path...
//
// XXX add search/filter field...
// XXX add path navigation...
DisplayFocusedPath: function(make, options){
options = make instanceof Function ?
options
: make
options = options || {}
make = make instanceof Function ?
make
: this
var dialog = this.dialog || this
var tag = options.id || 'item_path_display'
// indicator...
var e = make('CURRENT_PATH',
Object.assign(
{
id: tag,
cls: 'path',
},
options))
.last()
// event handlers...
dialog
.off('*', tag)
.on('focus',
function(){
e.value = this.pathArray
this.renderItem(e) },
tag)
return make },
// Item info...
//
// Show item .info or .alt text.
//
// This will show info for items that are:
// - focused
// - hovered (not yet implemented)
//
// XXX use focused elements and not just item...
// XXX add on mouse over...
DisplayItemInfo: function(make, options){
options = make instanceof Function ?
options
: make
options = options || {}
make = make instanceof Function ?
make
: this
var dialog = this.dialog || this
var tag = options.id || 'item_info_display'
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // indicator...
// Generators... var e = make('INFO',
// Object.assign(
// A generator is a function that creates 1 or more elements and sets up {
// the appropriate interactions... id: tag,
// cls: 'info',
// NOTE: these can work both as item generators called from inside },
// .make(..), i.e. as methods of the make constructor, or as options))
// generators assigned to .__header__ / .__items__ / .__footer__ .last()
// attributes... // event handlers...
// NOTE: when re-using these options.id needs to be set so as not to dialog
// overwrite existing instances data and handlers... .off('*', tag)
.on('focus',
// Make item generator... function(){
// var focused = this.focused
Items.makeDisplayItem = function(text, options){ e.value = focused.doc
var args = [...arguments] || focused.alt
return function(make, options){ || '&nbsp;'
make(...args) } } this.renderItem(e) },
// Make confirm item generator...
//
// XXX move this to Item.Confirm(..) and reuse that...
Items.makeDisplayConfirm = function(message, accept, reject){
return this.makeDisplayItem(message, {
buttons: [
...[reject instanceof Function ?
['Cancel', reject]
: []],
...[accept instanceof Function ?
['OK', accept]
: []], ], }) }
// Focused item path...
//
// XXX add search/filter field...
// XXX add path navigation...
Items.DisplayFocusedPath = function(make, options){
options = make instanceof Function ?
options
: make
options = options || {}
make = make instanceof Function ?
make
: this
var dialog = this.dialog || this
var tag = options.id || 'item_path_display'
// indicator...
var e = make('CURRENT_PATH',
Object.assign(
{
id: tag,
cls: 'path',
},
options))
.last()
// event handlers...
dialog
.off('*', tag)
.on('focus',
function(){
e.value = this.pathArray
this.renderItem(e) },
tag) tag)
return make} return make },
// Item info...
//
// Show item .info or .alt text.
//
// This will show info for items that are:
// - focused
// - hovered (not yet implemented)
//
// XXX use focused elements and not just item...
// XXX add on mouse over...
Items.DisplayItemInfo = function(make, options){
options = make instanceof Function ?
options
: make
options = options || {}
make = make instanceof Function ?
make
: this
var dialog = this.dialog || this
var tag = options.id || 'item_info_display'
// indicator... // Constructors...
var e = make('INFO', //
Object.assign( __new__: function(_, dialog, constructor){
{ var that = function(){
id: tag, that.called = true
cls: 'info', constructor.call(that, ...arguments)
}, return that }
options)) return that },
.last() __init__: function(dialog){
// event handlers... this.items = []
dialog this.dialog = dialog },
.off('*', tag) })
.on('focus',
function(){
var focused = this.focused var Make =
e.value = focused.doc module.Make =
|| focused.alt object.makeConstructor('Make', Items)
|| '&nbsp;'
this.renderItem(e) },
tag)
return make }
@ -1644,7 +1645,7 @@ var BaseBrowserPrototype = {
// XXX revise options handling for .__items__(..) // XXX revise options handling for .__items__(..)
// XXX might be a good idea to enable the user to merge the state // XXX might be a good idea to enable the user to merge the state
// manually... // manually...
// one way to do: // one way to go:
// - get the previous item via an index, // - get the previous item via an index,
// - update it // - update it
// - pass it to make(..) // - pass it to make(..)
@ -1703,106 +1704,100 @@ var BaseBrowserPrototype = {
// ...would be more logical to store the object (i.e. browser/list) // ...would be more logical to store the object (i.e. browser/list)
// directly as the element... // directly as the element...
var section var section
var make_called = false
var ids = new Set() var ids = new Set()
var list = []
var keys = options.uniqueKeys ? var keys = options.uniqueKeys ?
new Set() new Set()
: null : null
var make = function(value, opts){ var make = new Make(this,
make_called = true function(value, opts){
var dialog = this.dialog
// special-case: inlined browser... // special-case: inlined browser...
// //
// NOTE: we ignore opts here... // NOTE: we ignore opts here...
// XXX not sure if this is the right way to go... // XXX not sure if this is the right way to go...
// ...for removal just remove the if statement and its // ...for removal just remove the if statement and its
// first branch... // first branch...
if(value instanceof BaseBrowser){ if(value instanceof BaseBrowser){
var item = value var item = value
item.parent = this item.parent = dialog
item.section = section item.section = section
// normal item... // normal item...
} else { } else {
var args = [...arguments] var args = [...arguments]
opts = opts || {} opts = opts || {}
// handle: make(.., func, ..) // handle: make(.., func, ..)
opts = opts instanceof Function ? opts = opts instanceof Function ?
{open: opts} {open: opts}
: opts : opts
// handle trailing options... // handle trailing options...
opts = args.length > 2 ? opts = args.length > 2 ?
Object.assign({}, Object.assign({},
args.pop(), args.pop(),
opts) opts)
: opts : opts
opts = Object.assign( opts = Object.assign(
{}, {},
opts, opts,
{value: value}) {value: value})
// item id... // item id...
var key = this.__key__(opts) var key = dialog.__key__(opts)
// duplicate keys (if .options.uniqueKeys is set)... // duplicate keys (if .options.uniqueKeys is set)...
if(keys){ if(keys){
if(keys.has(key)){ if(keys.has(key)){
throw new Error(`make(..): duplicate key "${key}": ` throw new Error(`make(..): duplicate key "${key}": `
+`can't create multiple items with the same key ` +`can't create multiple items with the same key `
+`when .options.uniqueKeys is set.`) +`when .options.uniqueKeys is set.`)
}
keys.add(key)
} }
keys.add(key) // duplicate ids...
if(opts.id && ids.has(opts.id)){
throw new Error(`make(..): duplicate id "${opts.id}": `
+`can't create multiple items with the same id.`) }
// build the item...
// NOTE: we intentionally isolate the item object from
// the input opts here, yes, having a ref to a mutable
// object may be convenient in some cases but in this
// case it would promote going around the main API...
var item = new dialog.__item__(
// default item template...
(options.itemTemplate || {})['*'] || {},
// item template...
(options.itemTemplate || {})[opts.value] || {},
opts,
{
parent: dialog,
section,
})
// XXX do we need both this and the above ref???
item.children instanceof BaseBrowser
&& (item.children.parent = dialog)
} }
// duplicate ids...
if(opts.id && ids.has(opts.id)){
throw new Error(`make(..): duplicate id "${opts.id}": `
+`can't create multiple items with the same id.`) }
// build the item... // user extended make...
// NOTE: we intentionally isolate the item object from // XXX differentiate this for header and list...
// the input opts here, yes, having a ref to a mutable dialog.__make__
// object may be convenient in some cases but in this && dialog.__make__(section, item)
// case it would promote going around the main API...
var item = new this.__item__(
// default item template...
(options.itemTemplate || {})['*'] || {},
// item template...
(options.itemTemplate || {})[opts.value] || {},
opts,
{
parent: this,
section,
})
// XXX do we need both this and the above ref??? // store the item...
item.children instanceof BaseBrowser this.items.push(item)
&& (item.children.parent = this) ids.add(key)
} })
// user extended make...
// XXX differentiate this for header and list...
this.__make__
&& this.__make__(section, item)
// store the item...
list.push(item)
ids.add(key)
return make
}.bind(this)
make.__proto__ = Items
make.dialog = this
// build the sections... // build the sections...
var reset_index = false var reset_index = false
sections sections
.forEach(function([name, handler]){ .forEach(function([name, handler]){
// setup closure for make(..)... // setup state/closure for make(..)...
section = name
make_called = false
ids = new Set() ids = new Set()
list = make.items = that[name] = [] section = name
make.items = that[name] = []
// prepare for index reset... // prepare for index reset...
reset_index = reset_index || name == 'items' reset_index = reset_index || name == 'items'
@ -1818,7 +1813,7 @@ var BaseBrowserPrototype = {
: null) : null)
// if make was not called use the .__items__(..) return value... // if make was not called use the .__items__(..) return value...
that[name] = make_called ? that[name] = make.called ?
that[name] that[name]
: res }) : res })