From 267d5f705b051be5d8237623dad2a273765bdc33 Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Tue, 29 Aug 2017 20:59:00 +0300 Subject: [PATCH] lots of minor chnages, fixes and cleanup + refactoring... Signed-off-by: Alex A. Naanou --- ui (gen4)/css/experimenting.css | 8 ++ ui (gen4)/features/base.js | 37 +++--- ui (gen4)/features/collections.js | 181 +++++++++++++++++++++--------- ui (gen4)/features/demo.js | 11 ++ ui (gen4)/features/keyboard.js | 2 +- ui (gen4)/features/ui-ribbons.js | 1 + ui (gen4)/features/ui-widgets.js | 40 ++++++- ui (gen4)/imagegrid/data.js | 148 +++++++++++++++++------- ui (gen4)/lib/widget/browse.js | 19 +++- ui (gen4)/ui.js | 12 +- 10 files changed, 336 insertions(+), 123 deletions(-) diff --git a/ui (gen4)/css/experimenting.css b/ui (gen4)/css/experimenting.css index b723907c..06734848 100755 --- a/ui (gen4)/css/experimenting.css +++ b/ui (gen4)/css/experimenting.css @@ -258,6 +258,14 @@ body { cursor: text; } +/* Collection list */ +.browse-widget.collection-list .list .item .text[cropped]:after { + content: "(cropped)"; + margin-left: 5px; + opacity: 0.5; + font-style: italic; +} + /* External Editor List */ .browse-widget.editor-list .list .item:first-child .text:after { diff --git a/ui (gen4)/features/base.js b/ui (gen4)/features/base.js index 2c1e4085..cf1486af 100755 --- a/ui (gen4)/features/base.js +++ b/ui (gen4)/features/base.js @@ -1112,6 +1112,9 @@ module.TagsEditActions = actions.Actions({ gids = gids.constructor !== Array ? [gids] : gids tags = tags.constructor !== Array ? [tags] : tags + var that = this + gids = gids.map(function(gid){ return that.data.getImage(gid) }) + // data... this.data.tag(tags, gids) @@ -1190,7 +1193,9 @@ module.TagsEditActions = actions.Actions({ source = source || 'both' mode = mode || 'merge' - images = this.images + + var images = this.images + if(typeof(source) != typeof('str')){ images = source source = 'images' @@ -1415,16 +1420,6 @@ module.CropActions = actions.Actions({ crop_stack: null, - // load the crop stack if present... - load: [function(data){ - return function(){ - if(data.crop_stack){ - this.crop_stack = data.crop_stack.map(function(j){ - return data.Data(j) - }) - } - } - }], clear: [function(){ delete this.crop_stack }], @@ -1458,17 +1453,29 @@ module.CropActions = actions.Actions({ } } }], + // load the crop stack if present... load: [function(state){ return function(){ var that = this + if(!('crop_stack' in state)){ + return + } + + // load... if(state.crop_stack){ - that.crop_stack = (state.crop_stack || []) - .map(function(d){ - return d instanceof data.Data ? d : data.Data(d) }) + this.crop_stack = state.crop_stack + .map(function(d){ + return d instanceof data.Data ? + d + : data.Data(d) }) // merge the tags... - that.crop_stack.forEach(function(d){ d.tags = that.data.tags }) + this.crop_stack.forEach(function(d){ d.tags = that.data.tags }) + + // remove... + } else { + delete this.crop_stack } } }], diff --git a/ui (gen4)/features/collections.js b/ui (gen4)/features/collections.js index 75b91ee7..e26f55f4 100755 --- a/ui (gen4)/features/collections.js +++ b/ui (gen4)/features/collections.js @@ -26,7 +26,7 @@ var widgets = require('features/ui-widgets') // logical, would simplify control, etc. // -var MAIN_COLLECTION_TITLE = 'All' +var MAIN_COLLECTION_TITLE = 'ALL' // XXX things we need to do to collections: // - auto-collections @@ -101,19 +101,30 @@ var CollectionActions = actions.Actions({ .unique() .reverse() + // keep MAIN_COLLECTION_TITLE out of the collection order... + var m = res.indexOf(MAIN_COLLECTION_TITLE) + m >= 0 + && res.splice(m, 1) + // remove stuff not present... if(res.length > keys.length){ res = res.filter(function(e){ return e in collections }) } - this.__collection_order.splice.apply(this.__collection_order, - [0, this.__collection_order.length].concat(res)) + this.__collection_order.splice(0, this.__collection_order.length, ...res) return this.__collection_order }, set collection_order(value){ this.__collection_order = value }, + get collections_length(){ + var c = (this.collections || {}) + return MAIN_COLLECTION_TITLE in c ? + Object.keys(c).length - 1 + : Object.keys(c).length + }, + // Format: // { // // NOTE: this is always the first handler... @@ -207,6 +218,7 @@ var CollectionActions = actions.Actions({ var handlers = this.collection_handlers // save current collection state... + // // main view -> save it... if(this.collection == null){ var main = this.collections[MAIN_COLLECTION_TITLE] = { @@ -222,13 +234,12 @@ var CollectionActions = actions.Actions({ } else { //this.saveCollection(this.collection, 'crop') main.data = this.data - main.crop_stack = this.crop_stack + main.crop_stack = this.crop_stack + && this.crop_stack.slice() } } else if(crop_mode == 'all'){ this.saveCollection(this.collection, 'crop') - //this.collections[this.collection].data = this.data - //this.collections[this.collection].crop_stack = this.crop_stack } // load collection... @@ -257,15 +268,21 @@ var CollectionActions = actions.Actions({ // data needs to be updated as collections // may contain different numbers/orders of // images... - data.updateImagePositions() + // XXX + //data.updateImagePositions() + data.sortTags() that.load({ data: data, - crop_stack: collection_data.crop_stack, + crop_stack: collection_data.crop_stack + && collection_data.crop_stack.slice(), - collections: that.collections, - collection_order: that.collection_order, + // NOTE: we do not need to pass collections + // and order here as they stay in from + // the last .load(..) in merge mode... + //collections: that.collections, + //collection_order: that.collection_order, }, true) // maintain the .collection state... @@ -375,15 +392,15 @@ var CollectionActions = actions.Actions({ : mode == 'crop' ? this.data.clone() : this.data.clone() - .removeUnloadedGIDs()) - .run(function(){ - this.collection = collection + .run(function(){ + this.collection = collection - // NOTE: we are doing this manually after .removeUnloadedGIDs(..) - // as the later will mess-up the structures - // inherited from the main .data, namely tags... - //this.tags = that.data.tags - }), + // optimization: + // avoid processing .tags as we'll + // overwrite them anyway later... + delete this.tags + }) + .removeUnloadedGIDs()), } if(mode == 'crop' && this.crop_stack && depth != 0){ @@ -410,10 +427,11 @@ var CollectionActions = actions.Actions({ var that = this gid = this.data.getImage(gid) //return Object.keys(this.collections || {}) - return this.collection_order + return (this.collection_order || []) .filter(function(c){ - return !gid - || that.collections[c].data.getImage(gid) }) + return c != MAIN_COLLECTION_TITLE + && (!gid + || that.collections[c].data.getImage(gid)) }) }], collect: ['- Collections/', @@ -484,6 +502,7 @@ var CollectionActions = actions.Actions({ this.saveCollection(collection) } }], + // XXX BUG: .uncollect(..) from crop messes up global tags... uncollect: ['Collections|Image/$Uncollect image', {browseMode: function(){ return !this.collection && 'disabled' }}, function(gids, collection){ @@ -505,31 +524,30 @@ var CollectionActions = actions.Actions({ : [that.data.getImage(gid)] }) .reduce(function(a, b){ return a.concat(b) }, []) - /*/ NOTE: we are not using .data.updateImagePositions(gids, 'hide') - // here because it will remove the gids from everything - // while we need them removed only from ribbons... - var hideGIDs = function(){ - var d = this - gids.forEach(function(gid){ - var i = d.order.indexOf(gid) - Object.keys(d.ribbons).forEach(function(r){ - delete d.ribbons[r][i] - }) - }) - } - //*/ - if(this.collection == collection){ + // need to keep this from updating .tags... + // XXX this seems a bit hacky... + var tags = this.data.tags + delete this.data.tags + this.data - //.run(hideGIDs) + .removeGIDs(gids) + .removeEmptyRibbons() + .run(function(){ + this.tags = tags + this.sortTags() + }) + } + + // NOTE: we do both this and the above iff data is cloned... + // NOTE: if tags are saved to the collection it means that + // those tags are local to the collection and we do not + // need to protect them... + if(this.data !== this.collections[collection].data){ + this.collections[collection].data .removeGIDs(gids) .removeEmptyRibbons() } - - this.collections[collection].data - //.run(hideGIDs) - .removeGIDs(gids) - .removeEmptyRibbons() }], @@ -541,6 +559,7 @@ var CollectionActions = actions.Actions({ // their data on load as needed. load: [function(json){ var that = this + var collections = {} var c = json.collections || {} var order = json.collection_order || Object.keys(c) @@ -550,6 +569,7 @@ var CollectionActions = actions.Actions({ } Object.keys(c).forEach(function(title){ + // load data... var d = c[title].data instanceof data.Data ? c[title].data : data.Data @@ -564,6 +584,18 @@ var CollectionActions = actions.Actions({ data: d, } + // NOTE: this can be done lazily when loading each collection + // but doing so will make the system more complex and + // confuse (or complicate) some code that expects + // .collections[*].crop_stack[*] to be instances of Data. + if(c[title].crop_stack){ + state.crop_stack = c[title].crop_stack + .map(function(c){ + return c instanceof data.Data ? + c + : data.Data(c) }) + } + // copy the rest of collection data as-is... Object.keys(c[title]) .forEach(function(key){ @@ -761,6 +793,10 @@ module.Collection = core.ImageGridFeatures.Feature({ // XXX show collections in image metadata... var UICollectionActions = actions.Actions({ browseCollections: ['Collections|Crop/$Collec$tions...', + core.doc`Collection list... + + NOTE: collections are added live and not on dialog close... + `, widgets.makeUIDialog(function(action){ var that = this var to_remove = [] @@ -774,34 +810,66 @@ var UICollectionActions = actions.Actions({ .addClass('highlighted') }) + var openHandler = function(_, title){ + var gid = that.current + action ? + action.call(that, title) + : that.loadCollection(title) + that.focusImage(gid) + dialog.close() + } + var setCroppedState = function(title){ + // indicate collection crop... + var cs = + title == (that.collection || MAIN_COLLECTION_TITLE) ? + that.crop_stack + : (that.collections || {})[title] ? + that.collections[title].crop_stack + : null + cs + && this.find('.text').last() + .attr('cropped', cs.length) + } + //var collections = Object.keys(that.collections || {}) var collections = that.collection_order = that.collection_order || [] + // main collection... + !action && collections.indexOf(MAIN_COLLECTION_TITLE) < 0 + && make([ + MAIN_COLLECTION_TITLE, + ], + { events: { + update: function(_, title){ + // make this look almost like a list element... + // XXX hack??? + $(this).find('.text:first-child') + .before($('') + .css('color', 'transparent') + .addClass('sort-handle') + .html('☰')) + setCroppedState + .call($(this), title) + }, + open: openHandler, + }}) + + // collection list... make.EditableList(collections, { unique: true, sortable: 'y', to_remove: to_remove, - itemopen: function(title){ - var gid = that.current - action ? - action.call(that, title) - : that.loadCollection(title) - that.focusImage(gid) - dialog.close() - }, + + itemopen: openHandler, + normalize: function(title){ return title.trim() }, check: function(title){ return title.length > 0 }, - // remove the 'x' button from main collection... - // XXX is this the correct way to go??? - each: function(title){ - title == MAIN_COLLECTION_TITLE - && this.find('.button-container').remove() }, + each: setCroppedState, - // XXX should this be "on close"??? itemadded: function(title){ action ? that.newCollection(title) @@ -810,6 +878,7 @@ var UICollectionActions = actions.Actions({ disabled: action ? [MAIN_COLLECTION_TITLE] : false, }) }, { + cls: 'collection-list', // focus current collection... selected: that.collection || MAIN_COLLECTION_TITLE, }) @@ -869,7 +938,7 @@ var UICollectionActions = actions.Actions({ dialog.update() }, }) - : make.Empty() + : make.Empty('No collections...') }) .close(function(){ all.forEach(function(title){ diff --git a/ui (gen4)/features/demo.js b/ui (gen4)/features/demo.js index bca54942..5e2a8c85 100755 --- a/ui (gen4)/features/demo.js +++ b/ui (gen4)/features/demo.js @@ -56,6 +56,10 @@ var demo_images = module.demo_images = { a: { orientation: 90, + tags: ['test'], + }, + d: { + tags: ['test', 'bookmark'] }, f: { orientation: 270, @@ -68,6 +72,13 @@ module.demo_images = { }, } +// sync tags with images... +//demo_data = data.Data(demo_data) +// .tagsToImages(demo_images, 'merge') +// .tagsFromImages(demo_images, 'merge') +// .dumpJSON() + + /*********************************************************************/ diff --git a/ui (gen4)/features/keyboard.js b/ui (gen4)/features/keyboard.js index d7c2fae2..ce33c3e4 100755 --- a/ui (gen4)/features/keyboard.js +++ b/ui (gen4)/features/keyboard.js @@ -138,7 +138,7 @@ module.GLOBAL_KEYBOARD = { 'Collection': { pattern: '.collection-mode', - Esc: 'loadCollection: "All" -- Load all images', + Esc: 'loadCollection: "ALL" -- Load all images', }, 'Range': { diff --git a/ui (gen4)/features/ui-ribbons.js b/ui (gen4)/features/ui-ribbons.js index b2683fe3..b9ab9132 100755 --- a/ui (gen4)/features/ui-ribbons.js +++ b/ui (gen4)/features/ui-ribbons.js @@ -567,6 +567,7 @@ core.ImageGridFeatures.Feature({ 'reverseImages', 'reverseRibbons', 'cropGroup', + 'syncTags', ], function(target){ return this.reload() }], ], diff --git a/ui (gen4)/features/ui-widgets.js b/ui (gen4)/features/ui-widgets.js index 4b1c2654..a1350839 100755 --- a/ui (gen4)/features/ui-widgets.js +++ b/ui (gen4)/features/ui-widgets.js @@ -1846,8 +1846,14 @@ module.Buttons = core.ImageGridFeatures.Feature({ }], // update crop button status... - ['load clear reload', + [[ + 'load', + 'clear', + 'reload', + ], function(){ + var l = (this.crop_stack || []).length + $('.main-buttons.buttons .crop.button sub') // XXX should this be here or in CSS??? .css({ @@ -1855,11 +1861,38 @@ module.Buttons = core.ImageGridFeatures.Feature({ 'width': '0px', 'overflow': 'visible', }) - .text(this.crop_stack ? this.crop_stack.length : '') + .text(l == 0 ? '' + : l > 99 ? '99+' + : l) }], // update collection button status... - ['load clear reload collectionLoaded collectionUnloaded', + [[ + 'load', + 'clear', + 'reload', + 'saveCollection', + 'collectionLoaded', + 'collectionUnloaded', + ], function(){ + $('.main-buttons.buttons .collections.button') + .css({ + 'color': this.collection ? 'yellow' : '', + //'text-decoration': this.collection ? 'underline': '', + }) + + var l = this.collections_length + $('.main-buttons.buttons .collections.button sub') + .css({ + 'display': 'inline-block', + 'width': '0px', + 'overflow': 'visible', + //'color': this.collection ? 'yellow' : '', + }) + .text(l > 99 ? '99+' + : l == 0 ? '' + : l) + /* $('.main-buttons.buttons .collections.button sub') // XXX should this be here or in CSS??? .css({ @@ -1869,6 +1902,7 @@ module.Buttons = core.ImageGridFeatures.Feature({ 'color': 'yellow', }) .html(this.collection ? '●' : '') + //*/ }], // update zoom button status... ['viewScale', diff --git a/ui (gen4)/imagegrid/data.js b/ui (gen4)/imagegrid/data.js index 0c097604..28e8747e 100755 --- a/ui (gen4)/imagegrid/data.js +++ b/ui (gen4)/imagegrid/data.js @@ -222,9 +222,44 @@ var DataPrototype = { // Make a sparse list of image gids... // + // Make sparse list out of gids... + // .makeSparseImages(gids) + // -> list + // + // Make sparse list out of gids and drop gids not in .order... + // .makeSparseImages(gids, true) + // .makeSparseImages(gids, null, null, true) + // -> list + // NOTE: this sets drop_non_order_gids... + // + // Plase gids into their .order positions into target... + // .makeSparseImages(gids, target) + // -> list + // NOTE: items in target on given gid .order positions will + // get overwritten... + // + // Plase gids into their .order positions into target and reposition + // overwritten target items... + // .makeSparseImages(gids, target, true) + // -> list + // NOTE: this sets keep_target_items... + // + // Plase gids into their .order positions into target and reposition + // overwritten target items and drop gids not in .order... + // .makeSparseImages(gids, target, true, true) + // -> list + // NOTE: this sets keep_target_items and drop_non_order_gids... + // + // // This uses .order as the base for ordering the list. // - // If target is given then it will get updated with the input gids. + // By default items in gids that are not present in .order are + // appended to the output/target tail after .order.length, which ever + // is greater (this puts these items out of reach of further calls + // of .makeSparseImages(..)). + // Setting drop_non_order_gids to true will drop these items from + // output. + // // // NOTE: this can be used to re-sort sections of a target ribbon, // but care must be taken not to overwrite existing data... @@ -234,44 +269,65 @@ var DataPrototype = { // (see next for more info). // Another way to deal with this is to .makeSparseImages(target) // before using it as a target. - // NOTE: if keep_target_items is set items that are overwritten in - // the target will get pushed to gids. - // This flag has no effect if target is an empty list (default). - makeSparseImages: function(gids, target, keep_target_items){ + // NOTE: keep_target_items has no effect if target is not given... + makeSparseImages: function(gids, target, keep_target_items, drop_non_order_gids){ + if(arguments.length == 2 && target === true){ + drop_non_order_gids = true + target = null + } + // avoid mutating gids... + gids = gids === target || keep_target_items ? + gids.slice() + : gids target = target == null ? [] : target - keep_target_items = keep_target_items == null ? false : keep_target_items + order = this.order - // avoid directly updating self... - if(gids === target){ - gids = gids.slice() - } + var rest = [] - gids.forEach(function(e, i){ - // if the element is in its place alredy do nothing... - if(e == order[i] && e == target[i]){ - return + for(var i=0; i < gids.length; i++){ + var e = gids[i] + + // skip undefined... + if(e === undefined + // if the element is in its place alredy do nothing... + || (e == order[i] && e == target[i])){ + continue } - - // NOTE: try and avoid the expensive .indexOf(..) as much as - // possible... - i = e != order[i] ? order.indexOf(e) : i - if(i >= 0){ - var o = target[i] + // try and avoid the expensive .indexOf(..) as much as possible... + var j = e != order[i] ? order.indexOf(e) : i + + if(j >= 0){ // save overwritten target items if keep_target_items // is set... - if(keep_target_items - && o != null - // if the items is already in gids, forget it... - // NOTE: this is to avoid juggling loops... - && gids.indexOf(o) < 0){ - gids.push(o) - } + var o = target[j] + keep_target_items + && o != null + // if the item is already in gids, forget it... + // NOTE: this is to avoid juggling loops... + && gids.indexOf(o) < 0 + // look at o again later... + // NOTE: we should not loop endlessly here as target + // will eventually get exhausted... + && gids.push(o) - target[i] = e + target[j] = e + + // handle elements in gids that are not in .order + } else if(!drop_non_order_gids){ + rest.push(e) } - }) + } + + // avoid duplicating target items... + rest = rest + .filter(function(e){ return target.indexOf(e) < 0 }) + + if(rest.length > 0){ + target.length = Math.max(order.length, target.length) + target.splice(target.length, 0, ...rest) + } return target }, @@ -1271,22 +1327,31 @@ var DataPrototype = { // .updateImagePositions() // -> data // - // Reposition item(s) + // Full sort and remove items not in .order + // .updateImagePositions('remove') + // -> data + // + // Reposition specific item(s)... // .updateImagePositions(gid|index) + // .updateImagePositions([gid|index, .. ]) // -> data // - // Reposition item(s) and the item(s) they replace + // Reposition item(s) and the item(s) they replace... // .updateImagePositions(gid|index, 'keep') + // .updateImagePositions([gid|index, ..], 'keep') // -> data // - // Hide item(s) from lists + // Hide item(s) from lists... // .updateImagePositions(gid|index, 'hide') + // .updateImagePositions([gid|index, ..], 'hide') // -> data // - // Remove item(s) from lists + // Remove item(s) from lists... // .updateImagePositions(gid|index, 'remove') + // .updateImagePositions([gid|index, ..], 'remove') // -> data // + // // NOTE: hide will not change the order of other items while remove // will do a full sort... // NOTE: in any case other that the first this will not try to @@ -1295,6 +1360,10 @@ var DataPrototype = { // XXX needs more thought.... // do we need to move images by this??? updateImagePositions: function(from, mode, direction){ + if(['keep', 'hide', 'remove'].indexOf(from) >= 0){ + mode = from + from = null + } from = from != null && from.constructor !== Array ? [from] : from var r = this.getRibbon('current') @@ -1304,7 +1373,9 @@ var DataPrototype = { // resort... if(from == null){ - set[key] = this.makeSparseImages(cur) + set[key] = mode == 'remove' ? + this.makeSparseImages(cur, true) + : this.makeSparseImages(cur) // remove/hide elements... } else if(mode == 'remove' || mode == 'hide'){ @@ -1313,7 +1384,7 @@ var DataPrototype = { }) // if we are removing we'll also need to resort... if(mode == 'remove'){ - set[key] = this.makeSparseImages(cur) + set[key] = this.makeSparseImages(cur, true) } // place and keep existing... @@ -1328,7 +1399,7 @@ var DataPrototype = { // maintain focus... if(from && from.indexOf(this.current) >= 0){ - this.focusImage('r') + this.focusImage(r) } return this @@ -2613,7 +2684,7 @@ var DataPrototype = { // NOTE: this may result in empty ribbons... removeUnloadedGIDs: function(){ this.order = this.getImages('loaded') - this.updateImagePositions() + this.updateImagePositions('remove') return this }, @@ -2647,7 +2718,8 @@ var DataPrototype = { } this.order = order - this.updateImagePositions() + + this.updateImagePositions('remove') return this }, diff --git a/ui (gen4)/lib/widget/browse.js b/ui (gen4)/lib/widget/browse.js index b918ee2d..4a1b59bb 100755 --- a/ui (gen4)/lib/widget/browse.js +++ b/ui (gen4)/lib/widget/browse.js @@ -559,11 +559,11 @@ function(data, options){ // // length_limit: , // -// // Called when an item is opend... +// // Item open event handler... // // // // NOTE: this is simpler that binding to the global open event // // and filtering through the results... -// itemopen: function(value){ ... }, +// itemopen: function(evt, value){ ... }, // // // Check input value... // check: function(value){ ... }, @@ -910,7 +910,7 @@ function(list, options){ }) options.itemopen - && res.on('open', function(){ options.itemopen(dialog.selected) }) + && res.on('open', function(evt){ options.itemopen(evt, dialog.selected) }) res = res.toArray() @@ -2026,6 +2026,13 @@ var BrowserPrototype = { // // // event handlers... // events: { + // // item-specific update events... + // // + // // item added to dom by .update(..)... + // // NOTE: this is not propagated up, thus it will not trigger + // // the list update. + // update: , + // // : , // ... // }, @@ -2477,6 +2484,7 @@ var BrowserPrototype = { }) //--------------------------------- user event handlers --- + res.on('update', function(evt){ evt.stopPropagation() }) Object.keys(opts.events || {}) .forEach(function(evt){ res.on(evt, opts.events[evt]) }) @@ -2498,8 +2506,11 @@ var BrowserPrototype = { res.appendTo(l) } } - //--------------------------------------------------------- + + //------------------------------- item lifecycle events --- + res.trigger('update', txt) + //--------------------------------------------------------- return res } diff --git a/ui (gen4)/ui.js b/ui (gen4)/ui.js index b456db6a..332d4788 100755 --- a/ui (gen4)/ui.js +++ b/ui (gen4)/ui.js @@ -238,12 +238,12 @@ $(function(){ // load some testing data if nothing else loaded... if(!this.url_history || Object.keys(this.url_history).length == 0){ // NOTE: we can (and do) load this in parts... - this.loadDemoIndex() - - // this is needed when loading legacy sources that do not have tags - // synced... - // do not do for actual data... - //.syncTags() + this + .loadDemoIndex() + // this is needed when loading legacy sources that do not have tags + // synced... + // do not do for actual data... + .syncTags('both') } }) .start()