//--------------------------------------------------------------------- // XXX need to account for scrollbar -- add hysteresis??? var patchFlexRows = function(elems){ var W = elems[0].parentElement.clientWidth - 2 var w = 0 var h var row = [] var top = elems[0].offsetTop // NOTE: this will by design skip the last row. for(var elem of elems){ elem.style.height = '' elem.style.width = '' h = h ?? elem.offsetHeight top = top ?? elem.offsetTop // collect row... if(elem.offsetTop == top){ w += elem.offsetWidth row.push(elem) // next row... } 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 = 1/r1 < r2 if(!expanded_row){ var r = r1 } else { var r = r2 row.push(elem) } // patch the row... for(var e of row){ e.style.height = Math.floor(h * r) + 'px' } // prep for next row... if(!expanded_row){ w = elem.offsetWidth h = elem.offsetHeight top = elem.offsetTop row = [elem] } else { w = 0 h = null top = null row = [] }}}} var getScrollParent = function(elem){ var parent = elem.parentElement while(parent !== document.body && parent.scrollHeight > parent.clientHeight){ parent = elem.parentElement } return parent } // XXX also need to check if scrolled under something... var isVisible = function(elem) { const rect = elem.getBoundingClientRect() return rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) } //--------------------------------------------------------------------- // XXX add home/end, pageup/pagedown... var keyboard = { ArrowLeft: function(){ gallery.lightbox.shown ? gallery.lightbox.prev() : gallery.prev() }, ArrowRight: function(){ gallery.lightbox.shown ? gallery.lightbox.next() : gallery.next() }, ArrowUp: function(evt){ evt.preventDefault() gallery.lightbox.shown || gallery.up() }, ArrowDown: function(evt){ evt.preventDefault() gallery.lightbox.shown || gallery.down() }, Enter: function(){ gallery.lightbox.toggle() }, Escape: function(){ gallery.lightbox.shown && gallery.lightbox.hide() }, } //--------------------------------------------------------------------- var Gallery = { dom: undefined, __lightbox: undefined, get lightbox(){ if(this.dom){ return this.__lightbox ?? (this.__lightbox = { __proto__: Lightbox } .setup( this.dom.querySelector('.lightbox'), this)) } delete this.__lightbox return undefined }, get current(){ return this.dom.querySelector('img.current') }, set current(img){ for(var i of this.dom.querySelectorAll('img.current')){ i.classList.remove('current') } img.classList.add('current') img.scrollIntoView({ behavior: 'smooth', block: 'nearest', }) }, getRow: function(img, direction='current'){ if(['above', 'current', 'below'].includes(img)){ direction = img img = null } // get above/below row... // XXX these are wastefull... if(direction == 'above'){ var row = this.getRow(img) var e = row[0].previousSibling while(e && e.tagName != 'IMG'){ e = e.previousSibling } return e ? this.getRow(e) : this.getRow([...this.dom.querySelectorAll('img')].at(-1)) } else if(direction == 'below'){ var row = this.getRow(img) var e = row.at(-1).nextSibling while(e && e.tagName != 'IMG'){ e = e.nextSibling } return e ? this.getRow(e) : this.getRow([...this.dom.querySelectorAll('img')][1]) } // get current row... var cur = img ?? this.current if(cur == null){ var scroll = getScrollParent(this.dom).scrollTop var images = [...this.dom.querySelectorAll('img')].slice(1) for(cur of images){ if(cur.offsetTop >= scroll){ break } } } var top = cur.offsetTop var row = [] var e = cur while(e && e.offsetTop == top){ row.push(e) e = e.nextSibling while(e && e.tagName != 'IMG'){ e = e.nextSibling } } e = cur while(e && e.offsetTop == top){ e === cur || row.unshift(e) e = e.previousSibling while(e && e.tagName != 'IMG'){ e = e.previousSibling } } return row }, getImage: function(img, direction='current'){ if(['left', 'above', 'current', 'below', 'right'].includes(img)){ direction = img img = null } // current... if(direction == 'current'){ return img ?? this.current ?? this.getRow(img) // above/below... } else if(direction == 'above' || direction == 'below'){ var row = this.getRow(direction) var cur = this.current ?? row[0] var c = cur.offsetLeft + cur.offsetWidth/2 var target var min for(var img of row){ var n = img.offsetLeft + img.offsetWidth/2 var d = Math.abs(n - c) min = min ?? d if(d <= min){ min = d target = img } } // left/right... } else { var row = this.getRow(img) var i = row.indexOf( img ?? this.current ?? row[0]) i += direction == 'left' ? -1 : +1 i = i < 0 ? row.length-1 : i >= row.length-1 ? 0 : i var target = row[i] } return target }, // XXX cache image list??? prev: function(){ var images = [...this.dom.querySelectorAll('img')].slice(1) var i = this.current == null ? images.length-1 : images.indexOf(this.current)-1 i = i < 0 ? images.length-1 : i this.current = images[i] return this }, next: function(){ var images = [...this.dom.querySelectorAll('img')].slice(1) var i = this.current == null ? 0 : images.indexOf(this.current)+1 i = i >= images.length ? 0 : i this.current = images[i] return this }, // navigate images visually... left: function(){ var cur = this.current var row = this.getRow(cur) var i = row.indexOf(cur) - 1 this.current = row[i < 0 ? row.length-1 : i] return this }, right: function(){ var cur = this.current var row = this.getRow(cur) var i = row.indexOf(cur) + 1 this.current = row[i >= row.length ? 0 : i] return this }, up: function(){ this.current = this.getImage('above') return this }, down: function(){ this.current = this.getImage('below') return this }, // XXX select: function(){ }, show: function(){ this.lightbox.show() return this }, // XXX load: function(urls){ }, setup: function(dom){ var that = this this.dom = dom this.dom.addEventListener('click', function(evt){ var target = evt.target if(target.tagName == 'IMG' // skip images in lightbox... && target.parentElement === that.dom){ that.current = target that.show() } }) return this }, } //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // XXX ignore click from blur... // XXX might be a good idea to close on click outside the image... // XXX esc from context menu closes view... var Lightbox = { dom: undefined, gallery: undefined, navigation_deadzone: 100, caption_hysteresis: 10, cache_count: 1, get url(){ return this.dom.querySelector('img').src }, set url(url){ this.dom.querySelector('img').src = url // remove preview dir... .replace(/\/[0-9]+px\//, '/') // cache... this.cache_count != 0 && this.cache() }, get shown(){ return this.dom.style.display == 'block' }, show: function(url){ this.url = url ?? (this.gallery.current ?? this.gallery.next().current ?? {}).src // set caption... this.dom.setAttribute('caption', (this.gallery.current ?? this.gallery.next().current ?? {}) .getAttribute('caption') ?? '') this.dom.style.display = 'block' return this }, hide: function(){ this.dom.style.display = '' return this }, toggle: function(){ return this.shown ? this.hide() : this.show() }, prev: function(){ this.gallery.prev().show() return this }, next: function(){ this.gallery.next().show() return this }, __cache: undefined, cache: function(){ var cache = [] var _cache = this.__cache = [] var cur = this.gallery.current var images = [...this.gallery.dom.querySelectorAll('img')].slice(1) var i = images.indexOf(cur) var c = this.cache_count ?? 2 for(var j=i+1; j<=i+c; j++){ cache.push(j >= images.length ? j % images.length : j) } for(var j=i-1; j>=i-c; j--){ cache.unshift(j < 0 ? images.length+j : j) } for(i of cache){ var img = document.createElement('img') img.src = images[i].src .replace(/\/[0-9]+px\//, '/') _cache.push(img) } return this }, setup: function(dom, gallery){ var that = this this.dom = dom this.gallery = gallery // controls... this.dom.querySelector('.close') .addEventListener('click', function(evt){ evt.stopPropagation() that.hide() }) // click... var deadzone = this.navigation_deadzone ?? 100 this.dom .addEventListener('click', function(evt){ // click left/right side of view... // NOTE: this is vewport-relative... evt.clientX < that.dom.offsetWidth / 2 - deadzone/2 && that.prev() evt.clientX > that.dom.offsetWidth / 2 + deadzone/2 && that.next() }) // mouseofver... var hysteresis = this.caption_hysteresis ?? 10 this.dom .addEventListener('mousemove', function(evt){ // indicate action... if(evt.clientX < that.dom.offsetWidth / 2 - deadzone/2){ that.dom.classList.contains('clickable') || that.dom.classList.add('clickable') } else if( evt.clientX > that.dom.offsetWidth / 2 + deadzone/2){ that.dom.classList.contains('clickable') || that.dom.classList.add('clickable') } else { that.dom.classList.contains('clickable') && that.dom.classList.remove('clickable') } // show/hide caption... // hysteresis: // +---+-- off // | | // v ^ // | | // on -+---+ evt.clientY > that.dom.offsetHeight / 2 + hysteresis && that.dom.classList.add('show-caption') evt.clientY < that.dom.offsetHeight / 2 - hysteresis && that.dom.classList.remove('show-caption') }) return this }, } //--------------------------------------------------------------------- var setupGallery = function(gallery){ return {__proto__: Gallery} .setup(gallery) } var setup = function(){ patchFlexRows([...document.querySelectorAll('.gallery>img')]) var galleries = document.body.querySelectorAll('.gallery') for(var gallery of galleries){ // XXX this is wrong... window.gallery = setupGallery(gallery) } // keyboard... document.addEventListener('keydown', function(evt){ var key = evt.key if(key in keyboard){ keyboard[key](evt) } }) window.addEventListener('resize', function(){ patchFlexRows([...document.querySelectorAll('.gallery>img')]) }) } //--------------------------------------------------------------------- // vim:set ts=4 sw=4 :