654 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			654 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| //=====================================================================
 | |
| //
 | |
| //
 | |
| //
 | |
| // TODO:
 | |
| // 	- selection
 | |
| // 	- drag-n-drop
 | |
| // 	- sort/move
 | |
| // 	- crop selection
 | |
| // 	- full screen
 | |
| // 	- make the gallery into a web component
 | |
| //
 | |
| //
 | |
| //=====================================================================
 | |
| 
 | |
| 
 | |
| 
 | |
| //---------------------------------------------------------------------
 | |
| 
 | |
| // XXX need to account for scrollbar -- add hysteresis???
 | |
| var patchFlexRows = 
 | |
| function(elems, prevent_row_expansion=false){
 | |
| 	// NOTE: -1 here is to compensate for rounding errors...
 | |
| 	var W = elems[0].parentElement.clientWidth - 1
 | |
| 	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)
 | |
| 		// row donw + 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 }
 | |
| 			// 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 shift+arrow to select...
 | |
| // 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() },
 | |
| 	// NOTE: up/down will not prevent the default scrolling behavior 
 | |
| 	// 		when at top/bottom of the gallery.
 | |
| 	ArrowUp: function(evt){
 | |
| 		gallery.__at_top_row 
 | |
| 			|| evt.preventDefault()
 | |
| 		gallery.lightbox.shown
 | |
| 			|| gallery.up() },
 | |
| 	ArrowDown: function(evt){
 | |
| 		gallery.__at_bottom_row 
 | |
| 			|| evt.preventDefault()
 | |
| 		gallery.lightbox.shown
 | |
| 			|| gallery.down() },
 | |
| 	Enter: function(){
 | |
| 		gallery.lightbox.toggle() },
 | |
| 	Escape: function(){
 | |
| 		gallery.lightbox.shown ?
 | |
| 			gallery.lightbox.hide() 
 | |
| 		// XXX should we remember which image was current and select 
 | |
| 		// 		it again when needed???
 | |
| 		: gallery.deselect_current ?
 | |
| 			(gallery.current = null) 
 | |
| 		: null },
 | |
| 	// selection...
 | |
| 	' ': function(evt){
 | |
| 		gallery.current
 | |
| 			&& evt.preventDefault()
 | |
| 		gallery.toggleSelect() },
 | |
| 	// XXX use key codes...
 | |
| 	'a': function(evt){
 | |
| 		evt.preventDefault()
 | |
| 		if(evt.ctrlKey){
 | |
| 			gallery.selectAll() } },
 | |
| 	'd': function(evt){
 | |
| 		evt.preventDefault()
 | |
| 		if(evt.ctrlKey){
 | |
| 			gallery.deselectAll() } },
 | |
| 	'i': function(evt){
 | |
| 		evt.preventDefault()
 | |
| 		if(evt.ctrlKey){
 | |
| 			gallery.selectInverse() } },
 | |
| }
 | |
| 
 | |
| 
 | |
| 
 | |
| //---------------------------------------------------------------------
 | |
| 
 | |
| var Gallery = {
 | |
| 
 | |
| 	// Options...
 | |
| 	//
 | |
| 	deselect_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.
 | |
| 	loop_images: false,
 | |
| 
 | |
| 	allow_row_expansion: true,
 | |
| 
 | |
| 	click_to_select: true,
 | |
| 
 | |
| 
 | |
| 	code: `
 | |
| 		<div class="gallery">
 | |
| 			<!-- gallery: content -->
 | |
| 			<div class="images">
 | |
| 			</div>
 | |
| 			<!-- lightbox -->
 | |
| 			<div class="lightbox">
 | |
| 				<img>
 | |
| 				<div class="button close"></div>
 | |
| 			</div>
 | |
| 		</div>`,
 | |
| 
 | |
| 	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 },
 | |
| 
 | |
| 	__at_top_row: undefined,
 | |
| 	__at_bottom_row: undefined,
 | |
| 	get current(){
 | |
| 		return this.dom.querySelector('.images img.current') },
 | |
| 	set current(img){
 | |
| 		// unset...
 | |
| 		if(img == null){
 | |
| 			this.current?.classList.remove('current')
 | |
| 			return }
 | |
| 		// set...
 | |
| 		for(var i of this.dom.querySelectorAll('.images img.current')){
 | |
| 			i.classList.remove('current') }
 | |
| 		img.classList.add('current') 
 | |
| 		// XXX add offsets from borders...
 | |
| 		img.scrollIntoView({
 | |
| 			behavior: 'smooth',
 | |
| 			block: 'nearest',
 | |
| 			offset: 10,
 | |
| 		}) 
 | |
| 		// helpers...
 | |
| 		this.__at_top_row = !this.getRow('above')
 | |
| 		this.__at_bottom_row = !this.getRow('below') },
 | |
| 
 | |
| 	// XXX should this be writable???
 | |
| 	get images(){
 | |
| 		return [...this.dom.querySelectorAll('.images img')] },
 | |
| 
 | |
| 	get urls(){
 | |
| 		return this.images
 | |
| 			.map(function(img){ 
 | |
| 				// XXX not sure if we should remove the preview dir...
 | |
| 				return img.src }) },
 | |
| 				/*/
 | |
| 				return img.src 
 | |
| 					// remove preview dir...
 | |
| 					.replace(/\/[0-9]+px\//, '/') }) },
 | |
| 				//*/
 | |
| 
 | |
| 	getRow: function(img, direction='current'){
 | |
| 		if(['above', 'current', 'below'].includes(img)){
 | |
| 			direction = img
 | |
| 			img = this.current }
 | |
| 		// get above/below row...
 | |
| 		// XXX these are wastefull...
 | |
| 		if(direction == 'above'){
 | |
| 			var row = this.getRow(img)
 | |
| 			var e = row[0].previousElementSibling
 | |
| 			while(e && e.tagName != 'IMG'){
 | |
| 				e = e.previousElementSibling }
 | |
| 			return e ?
 | |
| 					this.getRow(e)
 | |
| 				: this.loop_images ?
 | |
| 					this.getRow(this.images.at(-1))
 | |
| 				: undefined
 | |
| 		} else if(direction == 'below'){
 | |
| 			// special case: nothing selected...
 | |
| 			if(img == null){
 | |
| 				return this.getRow() }
 | |
| 			var row = this.getRow(img)
 | |
| 			var e = row.at(-1).nextElementSibling
 | |
| 			while(e && e.tagName != 'IMG'){
 | |
| 				e = e.nextElementSibling }
 | |
| 			return e ?
 | |
| 					this.getRow(e) 
 | |
| 				: this.loop_images ?
 | |
| 					this.getRow(this.images[0])
 | |
| 				: undefined }
 | |
| 		// get current row...
 | |
| 		var cur = img 
 | |
| 			?? this.current
 | |
| 		if(cur == null){
 | |
| 			var scroll = getScrollParent(this.dom).scrollTop
 | |
| 			var images = this.images 
 | |
| 			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.nextElementSibling
 | |
| 			while(e && e.tagName != 'IMG'){
 | |
| 				e = e.nextElementSibling } }
 | |
| 		e = cur
 | |
| 		while(e && e.offsetTop == top){
 | |
| 			e === cur
 | |
| 				|| row.unshift(e)
 | |
| 			e = e.previousElementSibling
 | |
| 			while(e && e.tagName != 'IMG'){
 | |
| 				e = e.previousElementSibling } }
 | |
| 		return row },
 | |
| 	// XXX add .loop_images support???
 | |
| 	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)[0]
 | |
| 		// above/below...
 | |
| 		} else if(direction == 'above' || direction == 'below'){
 | |
| 			var row = this.getRow(direction)
 | |
| 			if(row == null){
 | |
| 				return undefined }
 | |
| 			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.images
 | |
| 		var i = this.current == null ? 
 | |
| 			images.length-1
 | |
| 			: images.indexOf(this.current)-1
 | |
| 		i = i >= 0 ?
 | |
| 				i
 | |
| 			: this.loop_images ?
 | |
| 				images.length-1
 | |
| 			: 0
 | |
| 		this.current = images[i]
 | |
| 		return this },
 | |
| 	next: function(){
 | |
| 		var images = this.images 
 | |
| 		var i = this.current == null ? 
 | |
| 			0
 | |
| 			: images.indexOf(this.current)+1
 | |
| 		i = i < images.length ?
 | |
| 				i
 | |
| 			: this.loop_images ?
 | |
| 				0
 | |
| 			: images.length-1
 | |
| 		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(){
 | |
| 		var img = this.getImage('above')
 | |
| 		img 
 | |
| 			&& (this.current = img)
 | |
| 		return this },
 | |
| 	down: function(){
 | |
| 		var img = this.getImage('below')
 | |
| 		img 
 | |
| 			&& (this.current = img)
 | |
| 		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')){
 | |
| 			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.addEventListener('click', function(evt){
 | |
| 					evt.stopPropagation()
 | |
| 					that.deselect(mark) })
 | |
| 				img.after(mark) } }
 | |
| 		// clear deselected...
 | |
| 		for(var mark of this.dom.querySelectorAll('.images img:not(.selected)+.mark')){
 | |
| 			mark.remove() }
 | |
| 		// update lightbox...
 | |
| 		this.lightbox.shown
 | |
| 			&& this.lightbox.update()
 | |
| 		return this },
 | |
| 	select: function(img){
 | |
| 		img = img ?? this.current
 | |
| 		img?.classList.add('selected')
 | |
| 		return this.updateMarkers() },
 | |
| 	deselect: function(img){
 | |
| 		img = img ?? this.current
 | |
| 		img?.classList.remove('selected')
 | |
| 		return this.updateMarkers() },
 | |
| 	toggleSelect: function(img){
 | |
| 		img = img ?? this.current
 | |
| 		img?.classList.toggle('selected')
 | |
| 		this.updateMarkers()
 | |
| 		return this },
 | |
| 	selectAll: function(){
 | |
| 		for(var img of this.images){
 | |
| 			img.classList.add('selected') }
 | |
| 		return this.updateMarkers() },
 | |
| 	deselectAll: function(){
 | |
| 		for(var img of this.images){
 | |
| 			img.classList.remove('selected') }
 | |
| 		return this.updateMarkers() },
 | |
| 	selectInverse: function(){
 | |
| 		for(var img of this.images){
 | |
| 			img.classList.toggle('selected') }
 | |
| 		return this.updateMarkers() },
 | |
| 
 | |
| 	show: function(){
 | |
| 		this.lightbox.show()
 | |
| 		return this },
 | |
| 
 | |
| 	update: function(){
 | |
| 		patchFlexRows(this.images, !this.allow_row_expansion)
 | |
| 		return this },
 | |
| 
 | |
| 	load: function(urls){
 | |
| 		this.clear()
 | |
| 		var images = this.dom.querySelector('.images')
 | |
| 		for(var url of urls){
 | |
| 			var img = document.createElement('img')
 | |
| 			img.src = url
 | |
| 			images.appendChild(img) }
 | |
| 		return this },
 | |
| 	clear: function(){
 | |
| 		this.dom.querySelector('.images').innerHTML = ''
 | |
| 		return this },
 | |
| 
 | |
| 	setup: function(dom){
 | |
| 		var that = this
 | |
| 		this.dom = dom
 | |
| 
 | |
| 		this.dom.querySelector('.images')
 | |
| 			.addEventListener('click', function(evt){
 | |
| 				evt.stopPropagation()
 | |
| 				var target = evt.target
 | |
| 				if(target.tagName == 'IMG'){
 | |
| 					// shift+click: toggle selections...
 | |
| 					if(evt.shiftKey){
 | |
| 						that.toggleSelect(target)
 | |
| 					// first click selects, second shows...
 | |
| 					} else if(that.click_to_select){
 | |
| 						target.classList.contains('current') ?
 | |
| 							that.show()
 | |
| 							: (that.current = target)
 | |
| 					// first click selects and shows...
 | |
| 					} else {
 | |
| 						that.current = target
 | |
| 						that.show() } 
 | |
| 				} else if(that.deselect_current){
 | |
| 					that.current = null } })
 | |
| 		this.dom
 | |
| 			.addEventListener('click', function(evt){
 | |
| 				that.deselect_current
 | |
| 					&& (that.current = null) })
 | |
| 
 | |
| 		// handle resizing...
 | |
| 		new ResizeObserver(
 | |
| 			function(elems){
 | |
| 				that.update() })
 | |
| 			.observe(this.dom)
 | |
| 		
 | |
| 		return this
 | |
|    			.update() },
 | |
| }
 | |
| 
 | |
| 
 | |
| //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| // 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.gallery.dom.classList.contains('lightboxed') },
 | |
| 	show: function(url){
 | |
| 		this.url = url 
 | |
| 			?? (this.gallery.current
 | |
| 				?? this.gallery.next().current
 | |
| 				?? {}).src
 | |
| 		this.update()
 | |
| 		this.gallery.dom.classList.add('lightboxed')
 | |
| 		return this },
 | |
| 	hide: function(){
 | |
| 		this.gallery.dom.classList.remove('lightboxed')
 | |
| 		return this },
 | |
| 	toggle: function(){
 | |
| 		return this.shown ?
 | |
| 			this.hide()
 | |
| 			: this.show() },
 | |
| 
 | |
| 	update: function(){
 | |
| 		// set caption...
 | |
| 		this.dom.setAttribute('caption',
 | |
| 			(this.gallery.current
 | |
| 					?? this.gallery.next().current
 | |
| 					?? {})
 | |
| 				.getAttribute('caption') 
 | |
| 					?? '')
 | |
| 		// set selection...
 | |
| 		this.gallery.current.classList.contains('selected') ?
 | |
| 			this.dom.classList.add('selected')
 | |
| 			: this.dom.classList.remove('selected')
 | |
| 		return this },
 | |
| 
 | |
| 	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){
 | |
| 				evt.stopPropagation()
 | |
| 				// 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() })
 | |
| 		// mousemove...
 | |
| 		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(){
 | |
| 	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) } }) }
 | |
| 	
 | |
| 
 | |
| 
 | |
| //---------------------------------------------------------------------
 | |
| // vim:set ts=4 sw=4 :
 |