diff --git a/css/grid-n-view.css b/css/grid-n-view.css index 9f10002..b8589a0 100644 --- a/css/grid-n-view.css +++ b/css/grid-n-view.css @@ -129,8 +129,8 @@ body { right: 0; } -/* marker: selected */ -.gallery .images img+.mark.selected:after { +/* marker: marked */ +.gallery .images img+.mark.marked:after { content: ""; position: absolute; display: block; diff --git a/grid-n-view.html b/grid-n-view.html index bc740f8..c49708e 100644 --- a/grid-n-view.html +++ b/grid-n-view.html @@ -47,7 +47,7 @@ - drop files/images - drag to sort - Gallery: remove image - - mark images for deletion + delete marked + - UI: mark images for deletion + delete marked - Gallery: serialize / deserialize - Lightbox: navigation (keyboard / mouse) - Lightbox: fullscreen mode diff --git a/grid-n-view.js b/grid-n-view.js index 039035f..5325b92 100644 --- a/grid-n-view.js +++ b/grid-n-view.js @@ -17,9 +17,8 @@ //--------------------------------------------------------------------- -// XXX need to account for scrollbar -- add hysteresis??? var patchFlexRows = -function(elems, prevent_row_expansion=false){ +function(elems, prevent_row_expansion=false, last_row_resize=1.5){ if(elems.length == 0){ return } // NOTE: -1 here is to compensate for rounding errors... @@ -28,7 +27,44 @@ function(elems, prevent_row_expansion=false){ var h var row = [] var top = elems[0].offsetTop - // NOTE: this will by design skip the last row. + // modes: + // 'as-is' | false + // 'expand' + // 'close' + // closure: W, w, h, row, elem + var max = 0 + var handleRow = function(mode='justify'){ + if(!mode || mode == 'as-is'){ + return false } + + if(typeof(mode) == 'number'){ + var r = Math.min( + W / w, + max * mode) + } else { + var r = W / w } + + var expanded + if(mode == 'expand'){ + var r2 = W / (w + elem.offsetWidth) + // NOTE: we are checking which will require a lesser resize + // the current row or it with the next image... + if(1/r < r2){ + expanded = true + var r = r2 + row.push(elem) } } + + max = Math.max(max, r) + + // patch the row... + var nw = 0 + for(var e of row){ + e.style.height = (h * r) + 'px' + nw += e.offsetWidth } + return !!expanded } + // NOTE: this will by design skip the last row... + // ...because we handle the row only when we see an image at a + // different vertical offset... for(var elem of elems){ elem.style.height = '' elem.style.width = '' @@ -40,26 +76,12 @@ function(elems, prevent_row_expansion=false){ if(elem.offsetTop == top){ w += elem.offsetWidth row.push(elem) - // row donw + prep for next... + // row done + prep for next... } else { - // NOTE: we are checking which will require a lesser resize - // the current row or it with the next image... - var r1 = W / w - var r2 = W / (w + elem.offsetWidth) var expanded_row = prevent_row_expansion ? - false - : 1/r1 < r2 - if(!expanded_row){ - var r = r1 - } else { - var r = r2 - row.push(elem) } - // patch the row... - var nw = 0 - for(var e of row){ - e.style.height = (h * r) + 'px' - nw += e.offsetWidth } + handleRow() + : handleRow('expand') // prep for next row... if(!expanded_row){ w = elem.offsetWidth @@ -70,7 +92,10 @@ function(elems, prevent_row_expansion=false){ w = 0 h = null top = null - row = [] }}}} + row = [] }}} + // handle last row... + last_row_resize + && handleRow(last_row_resize) } var getScrollParent = function(elem){ @@ -125,14 +150,14 @@ var keyboard = { gallery.lightbox.hide() // XXX should we remember which image was current and select // it again when needed??? - : gallery.deselect_current ? + : gallery.unmark_current ? (gallery.current = null) : null }, // selection... ' ': function(evt){ gallery.current && evt.preventDefault() - gallery.toggleSelect() }, + gallery.toggleMark() }, // XXX use key codes... 'a': function(evt){ evt.preventDefault() @@ -141,11 +166,11 @@ var keyboard = { 'd': function(evt){ evt.preventDefault() if(evt.ctrlKey){ - gallery.deselectAll() } }, + gallery.unmarkAll() } }, 'i': function(evt){ evt.preventDefault() if(evt.ctrlKey){ - gallery.selectInverse() } }, + gallery.markInverse() } }, } @@ -156,16 +181,33 @@ var Gallery = { // Options... // - deselect_current: true, + unmark_current: true, // If true navigation will loop over top/bottom rows and first/last // images... // This is mainly usefull for small galleries that do not need paging. - // XXX might be a good idea to autodisable this when paging is required. + // XXX might be a good idea to auto-disable this when paging is required. loop_images: false, + // If true for each row two versions will be compared, as-is and with + // one image from the next row and the closest resiae will be chosen. + // + // This produses a more uniform image grid but will have greater + // effect on gallery height / slower. allow_row_expansion: true, + // define how to handle last row + // + // can be: + // 'as-is' - do not resize + // 'justify' - fit width + // - maximum ration relative to largest row (must be >=1) + // + // Recomended value close to 1 + last_row_resize: 1, + + // If true first click on image will select it the second click will + // open it... click_to_select: true, exit_fullscreen_on_lightbox_close: true, @@ -261,6 +303,9 @@ var Gallery = { .replace(/\/[0-9]+px\//, '/') }) }, //*/ + get marked(){ + return [...this.dom.querySelectorAll('.images img.marked')] }, + get length(){ return this.images.length }, get index(){ @@ -283,7 +328,7 @@ var Gallery = { this.getRow(this.images.at(-1)) : undefined } else if(direction == 'below'){ - // special case: nothing selected... + // special case: nothing marked... if(img == null){ return this.getRow() } var row = this.getRow(img) @@ -434,56 +479,55 @@ var Gallery = { return this }, // selection... - get selected(){ - return this.dom.querySelectorAll('.images img.selected') }, + // // NOTE: this is here because we can't use :before / :after directly // on the img tag... // XXX make this generic and use a .marks list... updateMarkers: function(){ var that = this // select... - for(var img of this.dom.querySelectorAll('.images img.selected')){ + for(var img of this.dom.querySelectorAll('.images img.marked')){ var mark = img.nextElementSibling while(mark && mark.tagName != 'IMG' && !mark.classList.contains('mark')){ mark = img.nextElementSibling } if(!mark || !mark.classList.contains('mark')){ mark = document.createElement('div') - mark.classList.add('selected', 'mark') + mark.classList.add('marked', 'mark') mark.addEventListener('click', function(evt){ evt.stopPropagation() - that.deselect(mark) }) + that.unmark(mark) }) img.after(mark) } } - // clear deselected... - for(var mark of this.dom.querySelectorAll('.images img:not(.selected)+.mark')){ + // clear unmarked... + for(var mark of this.dom.querySelectorAll('.images img:not(.marked)+.mark')){ mark.remove() } // update lightbox... this.lightbox.shown && this.lightbox.update() return this }, - select: function(img){ + mark: function(img){ img = img ?? this.current - img?.classList.add('selected') + img?.classList.add('marked') return this.updateMarkers() }, - deselect: function(img){ + unmark: function(img){ img = img ?? this.current - img?.classList.remove('selected') + img?.classList.remove('marked') return this.updateMarkers() }, - toggleSelect: function(img){ + toggleMark: function(img){ img = img ?? this.current - img?.classList.toggle('selected') + img?.classList.toggle('marked') this.updateMarkers() return this }, selectAll: function(){ for(var img of this.images){ - img.classList.add('selected') } + img.classList.add('marked') } return this.updateMarkers() }, - deselectAll: function(){ + unmarkAll: function(){ for(var img of this.images){ - img.classList.remove('selected') } + img.classList.remove('marked') } return this.updateMarkers() }, - selectInverse: function(){ + markInverse: function(){ for(var img of this.images){ - img.classList.toggle('selected') } + img.classList.toggle('marked') } return this.updateMarkers() }, show: function(){ @@ -491,7 +535,9 @@ var Gallery = { return this }, update: function(){ - patchFlexRows(this.images, !this.allow_row_expansion) + patchFlexRows(this.images, + !this.allow_row_expansion, + this.last_row_resize ?? 1.2) return this }, // .load() @@ -546,9 +592,17 @@ var Gallery = { 'caption', ], // XXX do we handle previews here??? - json: function(){ + json: function(images=undefined){ var that = this - return this.images + images = + (images == 'all' || images == '*') ? + this.images + : images == 'marked' ? + this.marked + : !images ? + this.images + : images + return images .map(function(img){ var res = { url: img.src } for(var key of that.__image_attributes__){ @@ -589,7 +643,7 @@ var Gallery = { if(target.tagName == 'IMG'){ // shift+click: toggle selections... if(evt.shiftKey){ - that.toggleSelect(target) + that.toggleMark(target) // first click selects, second shows... } else if(that.click_to_select){ target.classList.contains('current') ? @@ -599,11 +653,11 @@ var Gallery = { } else { that.current = target that.show() } - } else if(that.deselect_current){ + } else if(that.unmark_current){ that.current = null } }) this.dom .addEventListener('click', function(evt){ - that.deselect_current + that.unmark_current && (that.current = null) }) // drag... this.dom @@ -692,9 +746,9 @@ var Lightbox = { .replace(/\${CAPTION}/, caption) .replace(/\${INDEX}/, index)) // set selection... - this.gallery.current.classList.contains('selected') ? - this.dom.classList.add('selected') - : this.dom.classList.remove('selected') + this.gallery.current.classList.contains('marked') ? + this.dom.classList.add('marked') + : this.dom.classList.remove('marked') return this }, prev: function(){