440 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			440 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | //---------------------------------------------------------------------
 | ||
|  | 
 | ||
|  | // 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 :
 |