From 505053796686187706403cdac84eec13a026b890 Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Sun, 15 Jan 2017 00:49:19 +0300 Subject: [PATCH] refactoring + work on kb editors... Signed-off-by: Alex A. Naanou --- ui (gen4)/css/experimenting.css | 20 ++ ui (gen4)/features/history.js | 16 +- ui (gen4)/features/keyboard.js | 304 ++++++++++++++++------------- ui (gen4)/features/ui-slideshow.js | 6 +- ui (gen4)/features/ui-widgets.js | 219 +++++++++++++++------ ui (gen4)/lib/widget/browse.js | 1 + 6 files changed, 350 insertions(+), 216 deletions(-) diff --git a/ui (gen4)/css/experimenting.css b/ui (gen4)/css/experimenting.css index e775da6b..a626f153 100755 --- a/ui (gen4)/css/experimenting.css +++ b/ui (gen4)/css/experimenting.css @@ -241,6 +241,26 @@ body { display: none; } +/* new item... */ +.browse-widget .list>.action { + margin-top: 0.2em; + border-top: solid 1px rgba(255,255,255, 0.2); +} +.browse-widget .list>.action .text { + font-style: italic; +} +/* do not show top border if after another action or separator... */ +.browse-widget .list>.action+.action, +.browse-widget .list>.separator+.action { + border-top: none; +} + +/* do not show top border if after another action or separator... */ +.browse-widget .list>.warn { + background-color: yellow !important; + color: red !important; + font-weight: bolder !important; +} /* Dialog highlight experiment... */ diff --git a/ui (gen4)/features/history.js b/ui (gen4)/features/history.js index db08a16c..a2240696 100755 --- a/ui (gen4)/features/history.js +++ b/ui (gen4)/features/history.js @@ -594,21 +594,7 @@ var URLHistoryUIActions = actions.Actions({ o.redraw() }], // mark for removal... - ['×', - function(p, cur){ - cur.toggleClass('strike-out') - - if(cur.hasClass('strike-out')){ - to_remove.indexOf(p) < 0 - && to_remove.push(p) - - } else { - var i = to_remove.indexOf(p) - if(i >= 0){ - to_remove.splice(i, 1) - } - } - }], + widgets.makeRemoveItemButton(to_remove) ], }) .open(function(evt, path){ diff --git a/ui (gen4)/features/keyboard.js b/ui (gen4)/features/keyboard.js index 2cb40a93..036561d1 100755 --- a/ui (gen4)/features/keyboard.js +++ b/ui (gen4)/features/keyboard.js @@ -147,6 +147,8 @@ module.GLOBAL_KEYBOARD2 = { 'on this page.', pattern: '*', + F1: 'browseActions: "/Help/" -- Help menu...', + alt_X: 'close', alt_F4: 'close', meta_Q: 'close', @@ -176,7 +178,7 @@ module.GLOBAL_KEYBOARD2 = { // XXX should this be all here or in respective sections??? alt_A: 'browseActions', - //alt_S: 'browseActions: "/Sort/"', + //alt_S: 'browseActions: "/Sort/" -- Sort menu...', alt_shift_A: 'listActions', @@ -201,7 +203,7 @@ module.GLOBAL_KEYBOARD2 = { ctrl_Z: 'undo', shift_U: 'redo', ctrl_shift_Z: 'redo', - alt_H: 'browseActions: "/History/" -- Open history menu', + alt_H: 'browseActions: "/History/" -- History menu...', // tilt... @@ -293,8 +295,8 @@ module.GLOBAL_KEYBOARD2 = { // ribbon image stuff... - alt_I: 'browseActions: "/Image/" -- Show image menu', - alt_R: 'browseActions: "/Ribbon/" -- Open ribbon menu', + alt_I: 'browseActions: "/Image/" -- Image menu...', + alt_R: 'browseActions: "/Ribbon/" -- Ribbon menu...', // ranges... @@ -332,7 +334,7 @@ module.GLOBAL_KEYBOARD2 = { shift_F2: 'cropRibbonAndAbove', ctrl_F2: 'cropMarked', alt_F2: 'cropBookmarked', - C: 'browseActions: "/Crop/" -- Show crop menu', + C: 'browseActions: "/Crop/" -- Crop menu...', // metadata... @@ -347,14 +349,14 @@ module.GLOBAL_KEYBOARD2 = { ctrl_I: 'toggleMark!: "ribbon" -- Invert marks in ribbon', ',': 'prevMarked', '.': 'nextMarked', - alt_M: 'browseActions: "/Mark/" -- Show mark menu', + alt_M: 'browseActions: "/Mark/" -- Mark menu...', // bookmarking... B: 'toggleBookmark', '[': 'prevBookmarked', ']': 'nextBookmarked', - alt_B: 'browseActions: "/Bookmark/" -- Show bookmark menu', + alt_B: 'browseActions: "/Bookmark/" -- Bookmark menu...', @@ -425,6 +427,9 @@ var KeyboardActions = actions.Actions({ // for changes to take effect. // XXX EXPERIMENTAL 'keyboard-key-pressed-action': 'off', + + // XXX make this generic... + 'confirm-delete-timeout': 2000, }, get keybindings(){ @@ -717,11 +722,27 @@ var KeyboardActions = actions.Actions({ // Interface stuff ------------------------------------------------ + // options format: + // { + // cls: 'edit', + // show_non_actions: true, + // empty_section_text: false, + // + // mode_buttons: + // mode_actions: + // + // key_buttons: + // + // drop_buttons: + // + // } + // // XXX BUG sections with doc do not show up in title... // XXX sub-group by path (???) - // XXX place this in /Doc/.. (???) browseKeyboardBindings: ['Interface|Help/Keyboard bindings...', - widgets.makeUIDialog(function(path, edit, get_text){ + widgets.makeUIDialog(function(path, options){ + options = options || {} + var actions = this var keybindings = this.keybindings var kb = this.keyboard @@ -729,14 +750,13 @@ var KeyboardActions = actions.Actions({ var keys = kb.keys('*') // get doc... - get_text = get_text === undefined && !edit ? - function(action){ + var getKeyText = options.get_key_text + || function(action){ var doc = action.doc ? action.doc : action.action in this ? this.getDocTitle(action.action) : action.action return doc.length == 0 ? action.action : doc } - : get_text var dialog = browse.makeLister(null, function(path, make){ @@ -744,16 +764,8 @@ var KeyboardActions = actions.Actions({ .forEach(function(mode){ var dropped = keybindings[mode].drop || [] var bound_ignored = [] - var buttons = edit ? - [ - // XXX up - ['⏶', function(){}], - // XXX down - ['⏷', function(){}], - ].concat(dialog.options.itemButtons) - : undefined - // section heading... + // section heading (mode)... make(keybindings[mode].doc ? $('') // NOTE: at this time adding a br @@ -769,7 +781,7 @@ var KeyboardActions = actions.Actions({ not_filtered_out: true, // XXX should sections be searchable??? not_searchable: true, - buttons: buttons, + buttons: options.mode_buttons, }) .attr('mode', mode) .addClass('mode') @@ -780,9 +792,9 @@ var KeyboardActions = actions.Actions({ var o = keyboard.parseActionCall(action) - if(get_text){ + if(getKeyText){ var doc = '' - var text = get_text.call(actions, o) + var text = getKeyText.call(actions, o) } else { var doc = o.doc @@ -792,10 +804,14 @@ var KeyboardActions = actions.Actions({ (': '+ o.arguments.map(JSON.stringify).join(' ')) : '') } - var hidden = !edit - && !(o.action in actions) - && !(kb.handler(mode, keys[mode][action][0]) - instanceof Function) + + var hidden = !options.show_non_actions + // hide all non-actions... + && !(o.action in actions + // except: functions represented by their doc... + || keybindings[mode][action] == null + && kb.handler(mode, keys[mode][action][0]) + instanceof Function) // NOTE: wee need the button spec to be // searchable, thus we are not using @@ -820,6 +836,8 @@ var KeyboardActions = actions.Actions({ // hide stuff that is not an action... hidden: hidden, disabled: hidden, + + buttons: options.key_buttons, }) .attr({ 'mode': mode, @@ -842,8 +860,8 @@ var KeyboardActions = actions.Actions({ // no keys in view mode... // XXX is adding info stuff like this a correct // thing to do in code? - c == 0 && !edit - && make('No bindings...', + c == 0 && options.empty_section_text !== false + && make(options.empty_section_text || 'No bindings...', { disabled: true, hide_on_search: true, @@ -860,30 +878,18 @@ var KeyboardActions = actions.Actions({ dropped .filter(function(k){ return bound_ignored.indexOf(k) == -1 }) - .join(' / ')]) + .join(' / ')], + { + buttons: options.drop_buttons, + }) .addClass('drop-list') .attr('mode', mode) // controls... - if(edit){ + if(options.mode_actions){ var elem = make('new', { - buttons: [ - // XXX - ['key', - function(){ - //elem.before( XXX ) - actions.editKeyBinding(mode) - // XXX update when done??? - }], - // XXX - ['mode', - function(){ - //elem.after( XXX ) - // XXX need to pass order info... - actions.editKeyboardMode() - // XXX update when done??? - }], - ]}) + buttons: options.mode_actions, + }) .addClass('new') } }) @@ -900,40 +906,13 @@ var KeyboardActions = actions.Actions({ hide_on_search: true, }) .addClass('info') - }, { + }, + { cls: [ 'key-bindings', 'no-item-numbers', - (edit ? 'edit' : 'browse'), + options.cls, ].join(' '), - - itemButtons: edit ? - [ - // NOTE: ordering within one section is purely - // aesthetic and has no function... - // XXX do wee actually need ordering??? - // XXX up - //['⏶', function(){}], - // XXX down - //['⏷', function(){}], - - // XXX edit -- launch the editor... - // ...do we actually need this as a button???? - ['⋯', function(_, cur){ - // key... - if(cur.hasClass('key')){ - actions.editKeyBinding(cur.attr('mode'), cur.attr('code')) - - // mode... - } else if(cur.hasClass('mode')){ - actions.editKeyboardMode(cur.attr('mode')) - } - }], - //*/ - //['edit', function(){}], - //['🖉', function(){}], - ] - : [], }) return dialog @@ -942,24 +921,77 @@ var KeyboardActions = actions.Actions({ editKeyboardBindings: ['Interface/Keyboard bindings editor...', widgets.uiDialog(function(path){ var that = this - var dialog = this.browseKeyboardBindings(path, true) + var dialog = this.browseKeyboardBindings( + path, + { + cls: 'edit', + show_non_actions: true, + empty_section_text: false, + + // mode... + mode_buttons: [ + // XXX up + ['⏶', function(_, cur){ + }], + // XXX down + ['⏷', function(_, cur){ + }], + ['⋯', function(_, cur){ + that.editKeyboardMode(cur.attr('mode')) + .close(function(){ dialog.update() }) }], + ], + mode_actions: [ + ['key', function(_, cur){ + //elem.before( XXX ) + that.editKeyBinding(cur.attr('mode')) + .close(function(){ dialog.update() }) }], + ['mode', function(_, cur){ + //elem.after( XXX ) + // XXX need to pass order info... + that.editKeyboardMode() + .close(function(){ dialog.update() }) }], + ], + + // keys... + key_buttons: [ + ['⋯', function(_, cur){ + that.editKeyBinding(cur.attr('mode'), cur.attr('code')) + .close(function(){ dialog.update() }) }], + ], + + // dropped key list... + drop_buttons: [ + ['⋯', function(_, cur){ + that.editKeyboardModeDroppedKeys(cur.attr('mode')) + .close(function(){ dialog.update() }) }], + ], + }) // XXX should this be only a button thing (done in .browseKeyboardBindings(..)) // or also the main action??? .open(function(){ var cur = dialog.select('!') + var sub_dialog // key... if(cur.hasClass('key')){ - that.editKeyBinding(cur.attr('mode'), cur.attr('code')) + sub_dialog = that + .editKeyBinding(cur.attr('mode'), cur.attr('code')) // mode... // XXX BUG: for some reason modes are unclickable... } else if(cur.hasClass('mode')){ - that.editKeyboardMode(cur.attr('mode')) + sub_dialog = that + .editKeyboardMode(cur.attr('mode')) + // dropped... } else if(cur.hasClass('drop-list')){ - that.editKeyboardModeDroppedKeys(cur.attr('mode')) + sub_dialog = that + .editKeyboardModeDroppedKeys(cur.attr('mode')) } + + sub_dialog + && sub_dialog + .close(function(){ dialog.update() }) }) return dialog })], @@ -970,8 +1002,8 @@ var KeyboardActions = actions.Actions({ // --- // // new key - // XXX - editKeyBinding: ['- Interface/Key binding editor...', + // XXX need a way to abort edits... + editKeyBinding: ['- Interface/Key mapping...', widgets.makeUIDialog(function(mode, code){ var that = this // XXX @@ -990,81 +1022,87 @@ var KeyboardActions = actions.Actions({ keys[mode][code] : [] + var to_remove = [] keys .forEach(function(key){ // XXX make editable... make(key, { buttons: [ - ['×', function(){}], + widgets.makeRemoveItemButton(to_remove), ], }) }) - make('New key') - // XXX stub... - .css({ fontStyle: 'italic' }) + var new_button = make('New key') + .addClass('action') + .on('open', function(){ + widgets.editItem(dialog, new_button) + }) make('---') - make('', { buttons: [ - ['Delete mapping', function(){}], - ], }) + widgets.makeConfirmActionItem(make('Delete'), + function(){ + // XXX + dialog.close() + }, that.config['confirm-delete-timeout'] || 2000) + }, + { + cls: 'metadata-view', }) return dialog })], - // XXX - editKeyboardMode: ['- Interface/keyboard mode editor...', - widgets.makeUIDialog(function(mode){ - var that = this - var dialog = browse.makeLister(null, - function(path, make){ - make(['Mode:', mode || '']) - make(['Doc:', that.keybindings[mode].doc || '']) - make(['Pattern:', that.keybindings[mode].pattern || mode]) - - make('---') - - make('', { buttons: [ - ['Delete mode', function(){}], - ], }) - }) - - return dialog - })], - // XXX - editKeyboardModeDroppedKeys: ['- Interface/keyboard mode dropped key editor...', + // XXX make fields editable... + // XXX need a way to abort edits... + editKeyboardMode: ['- Interface/Mode...', widgets.makeUIDialog(function(mode){ var that = this var dialog = browse.makeLister(null, function(path, make){ + // XXX make these editable.... make(['Mode:', mode || '']) + make(['Doc:', (that.keybindings[mode] || {}).doc || '']) + make(['Pattern:', (that.keybindings[mode] || {}).pattern || mode]) make('---') - var drop = that.keybindings[mode].drop || [] - drop = drop == '*' ? [drop] : drop - - drop - .forEach(function(key){ - // XXX make editable... - make(key, { buttons: [ - ['×', function(){}], - ], }) - }) - - make('New key') - // XXX stub... - .css({ fontStyle: 'italic' }) - - make('---') - - make('', { buttons: [ - ['Clear dropped keys', function(){}], - ], }) + widgets.makeConfirmActionItem(make('Delete'), + function(){ + if(mode in that.keybindings){ + delete that.keybindings[mode] + } + dialog.close() + }, that.config['confirm-delete-timeout'] || 2000) + }, + { + cls: 'metadata-view', }) return dialog })], + // XXX need a way to abort edits... + // XXX need a way to set a special '*' key... + editKeyboardModeDroppedKeys: ['- Interface/Dropped keys...', + widgets.makeUIDialog(function(mode){ + var that = this + + // XXX need a way to set a special '*' key... + var dialog = widgets.makeListEditor(function(keys){ + // get... + if(keys === undefined){ + return that.keybindings[mode].drop || [] + + // set... + } else { + that.keybindings[mode].drop = keys + } + }, + { + unique: true, + }) + + return dialog + })], // XXX move to gen2 diff --git a/ui (gen4)/features/ui-slideshow.js b/ui (gen4)/features/ui-slideshow.js index c15cfe0b..0e9197ed 100755 --- a/ui (gen4)/features/ui-slideshow.js +++ b/ui (gen4)/features/ui-slideshow.js @@ -148,15 +148,15 @@ var SlideshowActions = actions.Actions({ o.parent.close() }) .addClass('selected') + }, + { + cls: 'metadata-view tail-action', }) .on('close', function(){ // reset the timer if it was not suspended outside... suspended_timer || that.resetSlideshowTimer() }) - o.dom - .addClass('metadata-view tail-action') - return o })], diff --git a/ui (gen4)/features/ui-widgets.js b/ui (gen4)/features/ui-widgets.js index ecfeab34..3bea6092 100755 --- a/ui (gen4)/features/ui-widgets.js +++ b/ui (gen4)/features/ui-widgets.js @@ -144,24 +144,92 @@ function(cls, cfg, parent){ // XXX make the selector more accurate... // ...at this point this will select the first elem with text which // can be a different elem... -var makeEditableItem = -module.makeEditableItem = -function(list, item, elem, callback, options){ +var editItem = +module.editItem = +function(list, elem, callback, options){ return elem - .makeEditable({ - activate: true, - }) + .makeEditable(options + || { + activate: true, + clear_on_edit: true, + blur_on_abort: false, + blur_on_commit: false, + }) .on('edit-done', callback || function(){}) .on('edit-aborted edit-done', function(_, text){ list.update() // XXX make the selector more accurate... // ...at this point this will select the first elem // with text which can be a different elem... - .then(function(){ list.select(item.text()) }) + .then(function(){ list.select(elem.text()) }) }) } +var makeRemoveItemButton = +module.makeRemoveItemButton = +function makeRemoveItemButton(list, html){ + return [html || '×', + function(p, e){ + e.toggleClass('strike-out') + + if(e.hasClass('strike-out')){ + list.indexOf(p) < 0 + && list.push(p) + + } else { + var i = list.indexOf(p) + if(i >= 0){ + list.splice(i, 1) + } + } + }] +} + + +var makeConfirmActionItem = +module.makeConfirmActionItem = +function makeConfirmActionItem(elem, callback, timeout, confirm_text){ + confirm_text = confirm_text ? + confirm_text + : 'Confirm '+ elem.text().toLowerCase() +'?' + var text + + return elem + .addClass('action') + .on('open', function(){ + var item = $(this) + var elem = item.find('.text') + + // ready to delete... + if(elem.text() != confirm_text){ + text = elem.text() + + elem.text(confirm_text) + + item.addClass('warn') + + // reset... + setTimeout(function(){ + elem.text(text) + + item.removeClass('warn') + }, timeout || 2000) + + // confirmed... + } else { + callback && callback() + } + }) +} + + +var makeNewEditableItem = +module.makeNewEditableItem = +function makeNewEditableItem(elem){ +} + + // // Options format: // { @@ -190,15 +258,15 @@ function(list, item, elem, callback, options){ // } // // XXX add sort buttons: up/down/top/bottom... -// XXX make this more generic... // XXX currently using this also requires the use of makeUIDialog(..), // can this be simpler??? -var makeConfigListEditor = -module.makeConfigListEditor = -function(actions, list_key, options){ +// XXX this is generic, move to browse... +var makeListEditor = +module.makeListEditor = +function(list, options){ options = options || {} - var new_button = options.new_button + var new_button = options.new_button || true new_button = new_button === true ? 'New...' : new_button var _makeEditable = function(elem){ @@ -210,15 +278,15 @@ function(actions, list_key, options){ blur_on_commit: false, }) .on('edit-aborted', function(){ - list.select(null) - list.update() + dialog.select(null) + dialog.update() }) .on('edit-done', function(evt, text){ var txt = $(this).text() // invalid format... if(options.check && !options.check(txt)){ - list.update() + dialog.update() return } @@ -226,83 +294,76 @@ function(actions, list_key, options){ if(options.length_limit && (lst.length >= options.length_limit)){ - options.callback && options.callback.call(list, txt) + options.callback && options.callback.call(dialog, txt) return } // prevent editing non-arrays... - if(!(actions.config[list_key] instanceof Array)){ + if(!editable || !lst){ return } - // save the new version... - actions.config[list_key] = actions.config[list_key].slice() // add new value and sort list... - actions.config[list_key] - .push(txt) + lst.push(txt) // unique... if(options.unique == null || options.unique === true){ - actions.config[list_key] = actions.config[list_key] - .unique() + lst = lst.unique() // unique normalized... - } else if( typeof(options.unique) == typeof(function(){})){ - actions.config[list_key] = actions.config[list_key] - .unique(options.unique) + } else if(typeof(options.unique) == typeof(function(){})){ + lst = lst.unique(options.unique) } // sort... if(options.sort){ - actions.config[list_key] = actions.config[list_key] + lst = lst .sort(typeof(options.sort) == typeof(function(){}) ? options.sort : undefined) } + _write(lst) + // update the list data... - list.options.data - = actions.config[list_key] - .concat(new_button ? [ new_button ] : []) + dialog.options.data = lst.concat(new_button ? [ new_button ] : []) // update list and select new value... - list.update() + dialog.update() .done(function(){ - list.select('"'+txt+'"') + dialog.select('"'+txt+'"') }) }) } + var _write = function(lst){ + // write back the list... + return list instanceof Function ? + // call the writer... + list(lst) + // in-place replace list elements... + // NOTE: this is necessary as not everything we do with lst + // is in-place... + : list.splice.apply(list, [0, list.length].concat(lst)) + } var to_remove = [] - var lst = list_key instanceof Function ? list_key() - : list_key instanceof Array ? list_key - : actions.config[list_key] - lst = lst instanceof Array ? lst : Object.keys(lst) + var lst = list instanceof Function ? + list() + : list + var editable = lst instanceof Array - var list = browse.makeList(null, + // view objects... + lst = !editable ? Object.keys(lst) : lst.slice() + + var dialog = browse.makeList(null, lst.concat(new_button ? [ new_button ] : []), { path: options.path, itemButtons: options.itemButtons || [ // mark for removal... - ['×', - function(p){ - var e = this.filter('"'+p+'"', false) - .toggleClass('strike-out') - - if(e.hasClass('strike-out')){ - to_remove.indexOf(p) < 0 - && to_remove.push(p) - - } else { - var i = to_remove.indexOf(p) - if(i >= 0){ - to_remove.splice(i, 1) - } - } - }], + makeRemoveItemButton(to_remove) // XXX add shift up/down/top/bottom and other buttons (optional)... ] }) @@ -315,43 +376,71 @@ function(actions, list_key, options){ // restore striked-out items... .on('update', function(){ to_remove.forEach(function(e){ - list.filter('"'+ e +'"') + dialog.filter('"'+ e +'"') .toggleClass('strike-out') }) }) .open(function(evt, path){ // we clicked the 'New' button -- select it... if(new_button && (path == new_button || path == '')){ - list.select(new_button) + dialog.select(new_button) } else { - options.callback && options.callback.call(list, path) + options.callback && options.callback.call(dialog, path) } }) .on('close', function(){ // prevent editing non-arrays... - if(!(actions.config[list_key] instanceof Array)){ + if(!editable){ return } // remove striked items... to_remove.forEach(function(e){ - var lst = actions.config[list_key].slice() lst.splice(lst.indexOf(e), 1) - actions.config[list_key] = lst + _write(lst) }) // sort... if(options.sort){ - actions.config[list_key] = actions.config[list_key] - .sort(options.sort !== true ? options.sort : undefined) + lst.sort(options.sort !== true ? options.sort : undefined) + + _write(lst) } }) - new_button && list.dom.addClass('tail-action') + // XXX + new_button && dialog.dom.addClass('tail-action') - return list + return dialog +} + + + +//--------------------------------------------------------------------- + +var makeConfigListEditor = +module.makeConfigListEditor = +function(actions, path, options){ + path = path.split('.') + var key = path.pop() + + return makeListEditor(function(lst){ + var target = actions.config + path.forEach(function(p){ + target = target[p] = target[p] || {} + }) + + // get... + if(lst === undefined){ + return target[key] + + // set... + } else { + target[key] = lst + } + }, options) } @@ -372,7 +461,7 @@ function(actions, list, list_key, value_key, options){ // NOTE: this is called when adding a new value and // list maximum length is reached... callback: function(value){ - if(typeof(value_key) == typeof(function(){})){ + if(value_key instanceof Function){ value_key(value) } else { actions.config[value_key] = value @@ -392,7 +481,7 @@ function(actions, list, list_key, value_key, options){ }) // select default... o.on('update', function(){ - if(typeof(value_key) == typeof(function(){})){ + if(value_key instanceof Function){ o.select(value_key()) } else { diff --git a/ui (gen4)/lib/widget/browse.js b/ui (gen4)/lib/widget/browse.js index 626e673c..cd26f4dc 100755 --- a/ui (gen4)/lib/widget/browse.js +++ b/ui (gen4)/lib/widget/browse.js @@ -1131,6 +1131,7 @@ var BrowserPrototype = { // custom buttons... buttons && buttons + .slice() // make the order consistent for the user -- first // in list, first in item (from left), and should // be added last...