1834 lines
		
	
	
		
			50 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1834 lines
		
	
	
		
			50 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| //=====================================================================
 | |
| //
 | |
| //
 | |
| // TODO:
 | |
| // 	- ui for cropping...
 | |
| // 	- drag-n-drop for touch devices...
 | |
| // 	- handle url-hash
 | |
| //
 | |
| //
 | |
| //=====================================================================
 | |
| 
 | |
| 
 | |
| //---------------------------------------------------------------------
 | |
| // Generic stuff...
 | |
| 
 | |
| // This compansates for any resize rounding errors in patchFlexRows(..).
 | |
| var PATCH_MARGIN = 2
 | |
| // XXX do this partially (ignoring prevent_row_expansion)...
 | |
| var patchFlexRows = 
 | |
| function(elems, 
 | |
| 		prevent_row_expansion=false, 
 | |
| 		last_row_resize=1.5, 
 | |
| 		patch_margin=PATCH_MARGIN){
 | |
| 	if(elems.length == 0){
 | |
| 		return }
 | |
| 	// NOTE: -1 here is to compensate for rounding errors...
 | |
| 	var W = elems[0].parentElement.clientWidth - patch_margin
 | |
| 	var w = 0
 | |
| 	var h
 | |
| 	var row = []
 | |
| 	var top = elems[0].offsetTop
 | |
| 	// 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){
 | |
| 			if(r == 0 || h == 0){
 | |
| 				e.style.height = ''
 | |
| 			} else {
 | |
| 				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 = ''
 | |
| 		h = h 
 | |
| 			?? elem.offsetHeight
 | |
| 		top = top 
 | |
| 			?? elem.offsetTop
 | |
| 		// collect row...
 | |
| 		if(elem.offsetTop == top){
 | |
| 			w += elem.offsetWidth
 | |
| 			row.push(elem)
 | |
| 		// row done + prep for next...
 | |
| 		} else {
 | |
| 			var expanded_row = 
 | |
| 				prevent_row_expansion ?
 | |
| 					handleRow()
 | |
| 					: handleRow('expand')
 | |
| 			// 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 = [] }}}
 | |
| 	// handle last row...
 | |
| 	last_row_resize
 | |
| 		&& handleRow(last_row_resize) }
 | |
| 
 | |
| 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) }
 | |
| 
 | |
| // - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| var _ruler
 | |
| var px2rem = function(px){
 | |
| 	if(!_ruler){
 | |
| 		_ruler = document.createElement('div')
 | |
| 		document.body.append(_ruler) }
 | |
| 	_ruler.style.width = '1rem'
 | |
| 	var c = _ruler.offsetWidth
 | |
| 	return px / c }
 | |
| var rem2px = function(rem){
 | |
| 	if(!_ruler){
 | |
| 		_ruler = document.createElement('div')
 | |
| 		document.body.append(_ruler) }
 | |
| 	_ruler.style.width = rem + 'em'
 | |
| 	var px = _ruler.offsetWidth
 | |
| 	return px }
 | |
| 
 | |
| 
 | |
| // - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| var getTouch = 
 | |
| function(evt, id){
 | |
| 	if(id != null && id !== false && evt.targetTouches){
 | |
| 		for(var k in evt.targetTouches){
 | |
| 			if(evt.targetTouches[k]?.identifier == id){
 | |
| 				return evt.targetTouches[k] } } } }
 | |
| 
 | |
| 
 | |
| // - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| // XXX account for relative-positioned elements...
 | |
| // XXX constrain this to:
 | |
| // 		viewport -- DONE but needs to be smoother...
 | |
| // 		parent
 | |
| // 		element
 | |
| // 		...might be a good idea to write a fast/generic containment 
 | |
| // 		tester...
 | |
| // XXX might be a good idea to allow bounds to be:
 | |
| // 		string		- css selector
 | |
| // 		element		- 
 | |
| // XXX might be a good idea to either:
 | |
| // 		- scroll into view the dragged element
 | |
| // 		- bound it by screen
 | |
| // XXX need to check if element already set as movable...
 | |
| // XXX does it make sence to abstract out the bounds checking code???
 | |
| // XXX docs...
 | |
| HTMLElement.prototype.moveable = 
 | |
| function(options={}){
 | |
| 	var elem = this
 | |
| 	if(elem.dataset.movable){
 | |
| 		throw new Error('element already movable.') }
 | |
| 	elem.dataset.movable = true
 | |
| 	// options...
 | |
| 	var {
 | |
| 		// CSS class added to element while it is dragged...
 | |
| 		cls = 'movable',
 | |
| 		// Drag handle element -- watches the drag events...
 | |
| 		handle = elem,
 | |
| 		// Blement bounding box used to check bounds...
 | |
| 		box = elem,
 | |
| 		// Bounds object...
 | |
| 		// can be:
 | |
| 		// 	false
 | |
| 		// 	{
 | |
| 		// 		top: <top>,
 | |
| 		// 		left: <left>,
 | |
| 		// 		bottom: <bottom>,
 | |
| 		// 		<right>: <right>,
 | |
| 		// 	}
 | |
| 		// XXX add support for:
 | |
| 		// 		css selector
 | |
| 		// 		element
 | |
| 		bounds,
 | |
| 		// can be:
 | |
| 		// 	'x'
 | |
| 		// 	'y'
 | |
| 		// 	undefined
 | |
| 		lock,
 | |
| 		//	start(<elem>, <data>)
 | |
| 		//		-> undefined
 | |
| 		//		-> <data>
 | |
| 		start,
 | |
| 		// can be:
 | |
| 		// 	'strict'
 | |
| 		// 	'scroll' / true
 | |
| 		// 	false
 | |
| 		keepInView = true,
 | |
| 		//	move(<elem>, <data>)
 | |
| 		move = function(elem, data){
 | |
| 			data.x != null
 | |
| 				&& (elem.style.left = data.x + 'px')
 | |
| 			data.y != null
 | |
| 				&& (elem.style.top = data.y + 'px') },
 | |
| 		//	end(<elem>, <data>)
 | |
| 		end,
 | |
| 		ignoreBounds = false,
 | |
| 	} = options
 | |
| 	handle = 
 | |
| 		typeof(handle) == 'string' ?
 | |
| 			elem.querySelector(handle)
 | |
| 		: handle
 | |
| 
 | |
| 	var data
 | |
| 
 | |
| 	var handleMoveStart = function(evt){
 | |
| 		evt.preventDefault()
 | |
| 		evt.stopPropagation()
 | |
| 		if(!data){
 | |
| 			cls
 | |
| 				&& elem.classList.add(cls)
 | |
| 			var x = evt.clientX 
 | |
| 				?? evt.targetTouches[0].clientX
 | |
| 			var y = evt.clientY
 | |
| 				?? evt.targetTouches[0].clientY
 | |
| 			data = {
 | |
| 				bounds: bounds,
 | |
| 				offset: {
 | |
| 					// XXX get offset relative to position parent...
 | |
| 					// 		...or relative to screen...
 | |
| 					x: x - elem.offsetLeft, 
 | |
| 					y: y - elem.offsetTop,
 | |
| 				},
 | |
| 				touch: evt.targetTouches ?
 | |
| 					evt.targetTouches[0].identifier
 | |
| 					: undefined,
 | |
| 			} 
 | |
| 			if(typeof(start) == 'function'){
 | |
| 				var res = start(elem, data)
 | |
| 				data = res != null ?
 | |
| 					res
 | |
| 					: data } } }
 | |
| 	var handleMove = function(evt){
 | |
| 		if(data){
 | |
| 			var src = data.touch != null ?
 | |
| 				getTouch(evt, data.touch)
 | |
| 				: evt
 | |
| 			if(!src){
 | |
| 				return }
 | |
| 			evt.preventDefault()
 | |
| 			evt.stopPropagation()
 | |
| 			// viewport bounding box in elem coordinates...
 | |
| 			var x = src.clientX 
 | |
| 			var y = src.clientY
 | |
| 			data.x =
 | |
| 				lock == 'x' ?
 | |
| 					null
 | |
| 				: (!ignoreBounds 
 | |
| 						&& data.bounds != null) ?
 | |
| 					Math.min(
 | |
| 						data.bounds.right,
 | |
| 						Math.max(
 | |
| 							data.bounds.left, 
 | |
| 							x - data.offset.x))
 | |
| 				: x - data.offset.x
 | |
| 			data.y =
 | |
| 				lock == 'y' ?
 | |
| 					null
 | |
| 				: (!ignoreBounds 
 | |
| 						&& data.bounds != null) ?
 | |
| 					Math.min(
 | |
| 						data.bounds.bottom,
 | |
| 						Math.max(
 | |
| 							data.bounds.top, 
 | |
| 							y - data.offset.y))
 | |
| 				: y - data.offset.y
 | |
| 			// restrict to viewport...
 | |
| 			if(!ignoreBounds 
 | |
| 					&& keepInView == 'strict'){
 | |
| 				var t, l
 | |
| 				var bb = elem.getBoundingClientRect()
 | |
| 				var screen = {
 | |
| 					top: t = box.offsetTop - bb.top,
 | |
| 					left: l = box.offsetLeft - bb.left,
 | |
| 					bottom: window.innerHeight + t - box.offsetHeight,
 | |
| 					right: window.innerWidth + l - box.offsetWidth,
 | |
| 				}
 | |
| 				data.x = Math.min(
 | |
| 					screen.right,
 | |
| 					Math.max(
 | |
| 						screen.left,
 | |
| 						data.x))
 | |
| 				data.y = Math.min(
 | |
| 					screen.bottom,
 | |
| 					Math.max(
 | |
| 						screen.top,
 | |
| 						data.y)) }
 | |
| 			// deferred render..
 | |
| 			// NOTE: we only allow a single requestAnimationFrame(..)
 | |
| 			// 		to run per frame...
 | |
| 			if(!data.animate){
 | |
| 				data.animate = requestAnimationFrame(function(){
 | |
| 					if(!data){
 | |
| 						return }
 | |
| 					move
 | |
| 						&& move(elem, data)
 | |
| 					// keep in view...
 | |
| 					// NOTE: this works best with CSS's scroll-margin...
 | |
| 					!ignoreBounds
 | |
| 						&& (keepInView == 'scroll' 
 | |
| 							|| keepInView === true)
 | |
| 						&& elem.scrollIntoView({ block: 'nearest' }) 
 | |
| 					delete data.animate }) } } }
 | |
| 	var handleMoveEnd = function(evt){
 | |
| 		if(data){
 | |
| 			if(evt.targetTouches
 | |
| 					&& (evt.targetTouches.length == 0 
 | |
| 						|| getTouch(evt, data.touch))){
 | |
| 				return }
 | |
| 			evt.preventDefault()
 | |
| 			evt.stopPropagation()
 | |
| 			cls
 | |
| 				&& elem.classList.remove(cls)
 | |
| 			end
 | |
| 				&& end(elem, data) 
 | |
| 			data = false } }
 | |
| 
 | |
| 	// XXX can we reuse these???
 | |
| 	// 		...i.e. keep the data in the element???
 | |
| 	handle.addEventListener('mousedown', handleMoveStart)
 | |
| 	handle.addEventListener('touchstart', handleMoveStart)
 | |
| 	document.addEventListener('mousemove', handleMove)
 | |
| 	document.addEventListener('touchmove', handleMove)
 | |
| 	document.addEventListener('touchend', handleMoveEnd)
 | |
| 	document.addEventListener('mouseup', handleMoveEnd) }
 | |
| 
 | |
| 
 | |
| 
 | |
| //---------------------------------------------------------------------
 | |
| 
 | |
| // XXX add shift+arrow to select...
 | |
| // XXX add home/end, pageup/pagedown...
 | |
| // XXX need a real ui stack -- close things top to bottom (Enter/Escape/...)...
 | |
| var keyboard = {
 | |
| 	ArrowLeft: function(){
 | |
| 		gallery.prev() },
 | |
| 	ArrowRight: function(){
 | |
| 		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.dom.classList.contains('lightboxed')
 | |
| 				|| gallery.dom.classList.contains('detailed'))
 | |
| 			&& evt.preventDefault()
 | |
| 		gallery.lightbox.shown
 | |
| 			|| gallery.details.shown
 | |
| 			|| gallery.up() },
 | |
| 	ArrowDown: function(evt){
 | |
| 		gallery.__at_bottom_row 
 | |
| 			|| evt.preventDefault()
 | |
| 		;(gallery.dom.classList.contains('lightboxed')
 | |
| 				|| gallery.dom.classList.contains('detailed'))
 | |
| 			&& evt.preventDefault()
 | |
| 		gallery.lightbox.shown
 | |
| 			|| gallery.details.shown
 | |
| 			|| gallery.down() },
 | |
| 	Enter: function(){
 | |
| 		gallery.details.shown ?
 | |
| 			gallery.details.hide()
 | |
| 			: gallery.lightbox.toggle() },
 | |
| 	Escape: function(evt){
 | |
| 		gallery.details.shown ?
 | |
| 			gallery.details.hide() 
 | |
| 		: gallery.lightbox.shown ?
 | |
| 			gallery.lightbox.hide() 
 | |
| 		// XXX should we remember which image was current and select 
 | |
| 		// 		it again when needed???
 | |
| 		: gallery.unmark_current ?
 | |
| 			(gallery.current = null) 
 | |
| 		: null },
 | |
| 	// del (image) -> mark current for deletion
 | |
| 	// del (marked) -> toggle marked for deletion all marked
 | |
| 	// shift-del (none marked for deletion) -> delete current
 | |
| 	// shift-del (1+ marked for deletion) -> delete marked for deletion
 | |
| 	Delete: function(evt){
 | |
| 		// remove...
 | |
| 		if(evt.shiftKey){
 | |
| 			var to_remove = gallery.toBeRemoved
 | |
| 			// remove marked...
 | |
| 			if(to_remove.length > 0){
 | |
| 				gallery.removeQueued()
 | |
| 			// remove current...
 | |
| 			} else if(gallery.current){
 | |
| 				to_remove = gallery.current
 | |
| 				if(gallery.marked.includes(to_remove)){
 | |
| 					gallery.remove(...gallery.marked)
 | |
| 				} else {
 | |
| 					// move focus...
 | |
| 					gallery.next()
 | |
| 					gallery.current === to_remove
 | |
| 						&& gallery.prev()
 | |
| 					gallery.remove(to_remove) } } 
 | |
| 		// mark for removal...
 | |
| 		} else if(gallery.current){
 | |
| 			var cur = gallery.current
 | |
| 			// toggle marked...
 | |
| 			if(gallery.marked.includes(cur)){
 | |
| 				gallery.toggleQueueRemoval('marked') 
 | |
| 			// toggle current...
 | |
| 			} else {
 | |
| 				gallery.toggleQueueRemoval() } } },
 | |
| 	// selection...
 | |
| 	' ': function(evt){
 | |
| 		gallery.current
 | |
| 			&& evt.preventDefault()
 | |
| 		gallery.toggleMark() },
 | |
| 	// XXX use key codes...
 | |
| 	'a': function(evt){
 | |
| 		evt.preventDefault()
 | |
| 		if(evt.ctrlKey){
 | |
| 			gallery.markAll() } },
 | |
| 	'd': function(evt){
 | |
| 		evt.preventDefault()
 | |
| 		if(evt.ctrlKey){
 | |
| 			gallery.unmarkAll() } },
 | |
| 	'i': function(evt){
 | |
| 		evt.preventDefault()
 | |
| 		if(evt.ctrlKey){
 | |
| 			gallery.markInverse() } },
 | |
| }
 | |
| 
 | |
| 
 | |
| 
 | |
| //---------------------------------------------------------------------
 | |
| 
 | |
| var Gallery = {
 | |
| 
 | |
| 	// Options...
 | |
| 	//
 | |
| 	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 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
 | |
| 	// 	<number>	- 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,
 | |
| 
 | |
| 	// Mode to select the above/below image...
 | |
| 	//
 | |
| 	// 					 	   	   
 | |
| 	// 		     - -----+-------------+
 | |
| 	// 					|	   .	  |
 | |
| 	// 					|   current	  |
 | |
| 	// 					|	   .	  |
 | |
| 	// 		- --+-------+---.---+--.--+
 | |
| 	// 			|		.	    |  .  |			
 | |
| 	// 			|	    B	    |  A  |			
 | |
| 	// 			|		.	    |  .  |			
 | |
| 	// 		- --+---------------+-----+
 | |
| 	// 			 		^   ^      ^
 | |
| 	// 			 		c   i      c   			
 | |
| 	//
 | |
| 	// Here, A has the closest center (c) to current but B has the closest 
 | |
| 	// center of intersection (i), thus the two approaches will yield 
 | |
| 	// different results, moving down from current:
 | |
| 	// 		current ----(center)----> A
 | |
| 	// 		current -(intersection)-> B
 | |
| 	//
 | |
| 	// can be:
 | |
| 	// 	'intersection'	- closest center of intersecting part to center 
 | |
| 	// 						of current image.
 | |
| 	// 	'center'		- closest center of image to current image center
 | |
| 	// XXX remove this when/if the selected options feels natural...
 | |
| 	vertical_navigate_mode: 'intersection',
 | |
| 
 | |
| 	// a timeout to let the resize settle before we handle dragover...
 | |
| 	// XXX this is too long -- we can pickup and drag an image within this 
 | |
| 	// 		timeout...
 | |
| 	// 		need to make it more specific (handle only vertical drag???)
 | |
| 	resize_settle_timeout: 16,
 | |
| 
 | |
| 	code: `
 | |
| 		<div class="gallery">
 | |
| 			<!-- gallery: content -->
 | |
| 			<div class="images">
 | |
| 			</div>
 | |
| 			<!-- lightbox -->
 | |
| 			<div class="lightbox">
 | |
| 				<img>
 | |
| 				<div class="button close"></div>
 | |
| 			</div>
 | |
| 		</div>`,
 | |
| 
 | |
| 	dom: undefined,
 | |
| 
 | |
| 	// XXX these are the same....
 | |
| 	// XXX add support for multiple toolbars...
 | |
| 	__toolbars: undefined,
 | |
| 	get toolbars(){
 | |
| 		if(this.dom){
 | |
| 			var that = this
 | |
| 			return this.__toolbars 
 | |
| 				?? (this.__toolbars = 
 | |
| 					[...this.dom.querySelectorAll('.toolbar')]
 | |
| 						.map(function(toolbar){
 | |
| 							return {__proto__: Toolbar}
 | |
| 								.setup(
 | |
| 									toolbar, 
 | |
| 									that) })) }
 | |
| 		delete this.__toolbars
 | |
| 		return 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 },
 | |
| 	__details: undefined,
 | |
| 	get details(){
 | |
| 		if(this.dom){
 | |
| 			return this.__details 
 | |
| 				?? (this.__details = { __proto__: Details }
 | |
| 					.setup(
 | |
| 						this.dom.querySelector('.details'),
 | |
| 						this)) }
 | |
| 		delete this.__details 
 | |
| 		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',
 | |
| 		}) 
 | |
| 		// 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){ 
 | |
| 				return img.src }) },
 | |
| 
 | |
| 	get marked(){
 | |
| 		return [...this.dom.querySelectorAll('.images img.marked')] },
 | |
| 
 | |
| 	get length(){
 | |
| 		return this.images.length },
 | |
| 	get index(){
 | |
| 		return this.images.indexOf(this.current) },
 | |
| 
 | |
| 	getRow: function(img, direction='current', images){
 | |
| 		if(['above', 'current', 'below'].includes(img)){
 | |
| 			images = direction
 | |
| 			direction = img
 | |
| 			img = this.current }
 | |
| 		if(direction instanceof Array){
 | |
| 			images = direction
 | |
| 			direction = null }
 | |
| 		direction ??= 'current'
 | |
| 		images ??= this.images
 | |
| 		// 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(images.at(-1))
 | |
| 				: undefined
 | |
| 		} else if(direction == 'below'){
 | |
| 			// special case: nothing marked...
 | |
| 			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(images[0])
 | |
| 				: undefined }
 | |
| 		// get current row...
 | |
| 		var cur = img 
 | |
| 			?? this.current
 | |
| 		if(cur == null){
 | |
| 			var scroll = getScrollParent(this.dom).scrollTop
 | |
| 			var images = 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', images){
 | |
| 		// .getImage(direction[, images])
 | |
| 		if(['left', 'above', 'current', 'below', 'right'].includes(img)){
 | |
| 			images = direction
 | |
| 			direction = img
 | |
| 			img = null }
 | |
| 		// .getImage(img, images)
 | |
| 		if(direction instanceof Array){
 | |
| 			images = direction
 | |
| 			direction = null }
 | |
| 		direction ??= 'current'
 | |
| 		images ??= this.images
 | |
| 		// current...
 | |
| 		if(direction == 'current'){
 | |
| 			return img 
 | |
| 				?? this.current 
 | |
| 				?? this.getRow(img, images)[0]
 | |
| 		// above/below...
 | |
| 		// get image with closest center to target image center...
 | |
| 		} else if(direction == 'above' || direction == 'below'){
 | |
| 			var row = this.getRow(direction, images)
 | |
| 			if(row == null){
 | |
| 				return undefined }
 | |
| 			var cur = this.current 
 | |
| 				?? row[0]
 | |
| 			// image center point...
 | |
| 			var c = cur.offsetLeft + cur.offsetWidth/2
 | |
| 			var target
 | |
| 			var min
 | |
| 			for(var img of row){
 | |
| 				// length of intersection...
 | |
| 				if(this.vertical_navigate_mode == 'intersection'){
 | |
| 					var l = Math.max(
 | |
| 						img.offsetLeft, 
 | |
| 						cur.offsetLeft)
 | |
| 					var r = Math.min(
 | |
| 						img.offsetLeft + img.offsetWidth,
 | |
| 						cur.offsetLeft + cur.offsetWidth)
 | |
| 					var w = r - l
 | |
| 					var n = l + w/2
 | |
| 				// closest center...
 | |
| 				} else {
 | |
| 					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, images)
 | |
| 			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 },
 | |
| 	//
 | |
| 	// 	.getImages()
 | |
| 	// 		-> []
 | |
| 	//
 | |
| 	// 	.getImages('*')
 | |
| 	// 	.getImages('all')
 | |
| 	// 		-> <all-images>
 | |
| 	//
 | |
| 	// 	.getImages('marked')
 | |
| 	// 		-> <marked-images>
 | |
| 	//
 | |
| 	// 	.getImages(<img>, ..)
 | |
| 	// 	.getImages([<img>, ..])
 | |
| 	// 		-> <images>
 | |
| 	//
 | |
| 	// 	.getImages('sorted', ..)
 | |
| 	// 	.getImages(.., 'sorted')
 | |
| 	// 		-> <sorted-images>
 | |
| 	//
 | |
| 	// <img> ::=
 | |
| 	// 		ImageElement
 | |
| 	// 		| <index>
 | |
| 	// 		| 'current'
 | |
| 	//
 | |
| 	getImages: function(...images){
 | |
| 		var that = this
 | |
| 		var sorted = 
 | |
| 			images.at(-1) == 'sorted' ?
 | |
| 				!!images.pop()
 | |
| 			: images[0] == 'sorted' ?
 | |
| 				!!images.shift()
 | |
| 			: false
 | |
| 		images = images.flat()
 | |
| 		if(images.includes('all') 
 | |
| 				|| images.includes('*')){
 | |
| 			return this.images }
 | |
| 		images = [...new Set(images
 | |
| 			.map(function(img){
 | |
| 				return ((typeof(img) == 'string' 
 | |
| 								&& img.trim() == '') ?
 | |
| 							[]
 | |
| 						: !isNaN(img*1) ?
 | |
| 							that.images.at(img*1)
 | |
| 						: img == 'marked' ?
 | |
| 							that.marked
 | |
| 						: img == 'current' ?
 | |
| 							that.current
 | |
| 						: typeof(img) == 'string' ?
 | |
| 							[]
 | |
| 						: img)
 | |
| 					?? [] })
 | |
|    			.flat())] 
 | |
| 		if(sorted){
 | |
| 			var order = new Map(
 | |
| 				Object.entries(this.images)
 | |
| 					.map(function(e){ 
 | |
| 						return e.reverse() }))
 | |
| 			images
 | |
| 				.sort(function(a, b){
 | |
| 					return order.get(a) - order.get(b) }) }
 | |
| 		return images },
 | |
| 
 | |
| 	// XXX cache image list???
 | |
| 	prev: function(images){
 | |
| 		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]
 | |
| 		this.update()
 | |
| 		return this },
 | |
| 	next: function(images){
 | |
| 		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]
 | |
| 		this.update()
 | |
| 		return this },
 | |
| 
 | |
| 	// navigate images visually...
 | |
| 	// XXX BUG: these seem not to work with passed list of images...
 | |
| 	left: function(images){
 | |
| 		var cur = this.current
 | |
| 		var row = this.getRow(cur, images)
 | |
| 		var i = row.indexOf(cur) - 1
 | |
| 		this.current = row[i < 0 ?
 | |
| 			row.length-1
 | |
| 			: i]
 | |
| 		return this },
 | |
| 	right: function(images){
 | |
| 		var cur = this.current
 | |
| 		var row = this.getRow(cur, images)
 | |
| 		var i = row.indexOf(cur) + 1
 | |
| 		this.current = row[i >= row.length ?
 | |
| 			0
 | |
| 			: i]
 | |
| 		return this },
 | |
| 	up: function(images){
 | |
| 		var img = this.getImage('above', images)
 | |
| 		img 
 | |
| 			&& (this.current = img)
 | |
| 		return this },
 | |
| 	down: function(images){
 | |
| 		var img = this.getImage('below', images)
 | |
| 		img 
 | |
| 			&& (this.current = img)
 | |
| 		return this },
 | |
| 
 | |
| 	// selection...
 | |
| 	//
 | |
| 	// 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.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('marked', 'mark')
 | |
| 				mark.addEventListener('click', function(evt){
 | |
| 					evt.stopPropagation()
 | |
| 					that.unmark(mark) })
 | |
| 				img.after(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 },
 | |
| 	mark: function(img){
 | |
| 		img = img ?? this.current
 | |
| 		img?.classList.add('marked')
 | |
| 		return this.updateMarkers() },
 | |
| 	unmark: function(img){
 | |
| 		img = img ?? this.current
 | |
| 		img?.classList.remove('marked')
 | |
| 		return this.updateMarkers() },
 | |
| 	toggleMark: function(img){
 | |
| 		img = img ?? this.current
 | |
| 		img?.classList.toggle('marked')
 | |
| 		this.updateMarkers()
 | |
| 		return this },
 | |
| 	markAll: function(){
 | |
| 		for(var img of this.images){
 | |
| 			img.classList.add('marked') }
 | |
| 		return this.updateMarkers() },
 | |
| 	unmarkAll: function(){
 | |
| 		for(var img of this.images){
 | |
| 			img.classList.remove('marked') }
 | |
| 		return this.updateMarkers() },
 | |
| 	markInverse: function(){
 | |
| 		for(var img of this.images){
 | |
| 			img.classList.toggle('marked') }
 | |
| 		return this.updateMarkers() },
 | |
| 
 | |
| 	// XXX should this be an interface to something like .splash???
 | |
| 	get loading(){
 | |
| 		return !this.dom.classList.contains('ready') },
 | |
| 	showLoading: function(){
 | |
| 		this.dom.classList.remove('ready')
 | |
| 		return this },
 | |
| 	hideLoading: function(){
 | |
| 		this.dom.classList.add('ready')
 | |
| 		return this },
 | |
| 	toggleLoading: function(){
 | |
| 		this.dom.classList.toggle('ready')
 | |
| 		return this },
 | |
| 
 | |
| 	// show current image in lightbox...
 | |
| 	show: function(){
 | |
| 		this.lightbox.show()
 | |
| 		return this },
 | |
| 
 | |
| 	__update_grid_size_timeout: undefined,
 | |
| 	__update_grid_size_running: false,
 | |
| 	__update_grid_size: function(){
 | |
| 		// if still running then delay untill done...
 | |
| 		// NOTE: this will keep only one call no matter how many times 
 | |
| 		// 		the method was actually called...
 | |
| 		if(this.__update_grid_size_running){
 | |
| 			var that = this
 | |
| 			this.__update_grid_size_timeout
 | |
| 				&& clearTimeout(this.__update_grid_size_timeout)
 | |
| 			this.__update_grid_size_timeout = setTimeout(function(){
 | |
| 				that.__update_grid_size() 
 | |
| 				delete that.__update_grid_size_timeout }, 10)
 | |
| 			return this }
 | |
| 		// do the update...
 | |
| 		this.__update_grid_size_running = true
 | |
| 		try{
 | |
| 			patchFlexRows(this.images, 
 | |
| 				!this.allow_row_expansion, 
 | |
| 				this.last_row_resize ?? 1.2)
 | |
| 		}catch(err){ }
 | |
| 		delete this.__update_grid_size_running
 | |
| 		return this },
 | |
| 	update: function(){
 | |
| 		this.__update_grid_size()
 | |
| 		// XXX should this update markers???
 | |
| 		//this.updateMarkers()
 | |
| 		this.lightbox.shown
 | |
| 			&& this.lightbox.update()
 | |
| 		this.details.shown
 | |
| 			&& this.details.update()
 | |
| 		return this },
 | |
| 
 | |
| 	//	.load(<image>)
 | |
| 	//	.load(<images>)
 | |
| 	//	.load(<image>, <index>)
 | |
| 	//	.load(<images>, <index>)
 | |
| 	//
 | |
| 	//	<images> ::=
 | |
| 	//		<image>
 | |
| 	//		| [ <image>, .. ]
 | |
| 	//	<image> ::=
 | |
| 	//		<url>
 | |
| 	//		| [ <url>, <caption>, .. ]
 | |
| 	//		| { url: <url>, caption: <caption>, .. }
 | |
| 	//
 | |
| 	// XXX BUG: for some reason this breaks the gallery:
 | |
| 	// 			gallery.load(gallery.json('marked'))
 | |
| 	// XXX do we handle previews here???
 | |
| 	load: function(images, index=undefined){
 | |
| 		var that = this
 | |
| 		images = images instanceof Array ?
 | |
| 			images
 | |
| 			: [images]
 | |
| 		// create images...
 | |
| 		var elems = []
 | |
| 		for(var data of images){
 | |
| 			if(typeof(data) == 'string'){
 | |
| 				var [url, data] = [data, {}]
 | |
| 			} else if(data instanceof Array){
 | |
| 				var [url, caption] = data
 | |
| 				data = {}
 | |
| 				caption 
 | |
| 					?? (data.caption = caption)
 | |
| 			} else {
 | |
| 				var {url, ...data} = data }
 | |
| 			var elem = document.createElement('img')
 | |
| 			elem.onload = function(){
 | |
| 				that.update() }
 | |
| 			elem.src = url
 | |
| 			elem.setAttribute('draggable', 'true')
 | |
| 			for(var [key, value] of Object.entries(data)){
 | |
| 				value
 | |
| 					// XXX is this a good way to destinguish classes and attrs???
 | |
| 					&& (typeof(value) == 'boolean' ? 
 | |
| 						elem.classList.add(key)
 | |
| 						: elem.setAttribute(key, value)) }
 | |
| 			elems.push(elem) }
 | |
| 		// add to gallery...
 | |
| 		if(index == null){
 | |
| 			this.clear() }
 | |
| 		if(index == null 
 | |
| 				&& this.length == 0){
 | |
| 			this.dom.querySelector('.images')
 | |
| 				.append(...elems) 
 | |
| 		} else {
 | |
| 			var sibling = this.images.at(index)
 | |
| 			index < 0 ?
 | |
| 				sibling.after(...elems)
 | |
| 				: sibling.before(...elems) }
 | |
| 		return this
 | |
| 			.updateMarkers()
 | |
| 			.update() },
 | |
| 	__image_attributes__: [
 | |
| 		'caption',
 | |
| 		'filename',
 | |
| 	],
 | |
| 	__image_classes__: [
 | |
| 		// XXX should this be here or set as a root attribute???
 | |
| 		'current',
 | |
| 		'marked',
 | |
| 	],
 | |
| 	// XXX add option to include images as data urls...
 | |
| 	// XXX do we handle previews here???
 | |
| 	json: function(images=undefined){
 | |
| 		var that = this
 | |
| 		return this.getImages(images ?? this.images)
 | |
| 			.map(function(img, i){
 | |
| 				var res = { url: img.src }
 | |
| 				for(var key of that.__image_attributes__ ?? []){
 | |
| 					var value = img.getAttribute(key)
 | |
| 					value 
 | |
| 						&& (res[key] = value) }
 | |
| 				for(var key of that.__image_classes__ ?? []){
 | |
| 					img.classList.contains(key)
 | |
| 						&& (res[key] = true) }
 | |
| 				return res }) },
 | |
| 	// XXX add support for .load(..)...
 | |
| 	editorJson: function(){
 | |
| 		var data = {
 | |
| 			varsion: 1,
 | |
| 			gallery: this.json(),
 | |
| 		} 
 | |
| 		// XXX get serilization handlers...
 | |
| 		var handlers = []
 | |
| 		for(var handler of handlers){
 | |
| 			handler.call(this, data) }
 | |
| 		return data },
 | |
| 
 | |
| 	// XXX
 | |
| 	zip: function(){
 | |
| 		var json = this.json()
 | |
| 		// XXX
 | |
| 	},
 | |
| 
 | |
| 	// removal queue...
 | |
| 	get toBeRemoved(){
 | |
| 		return [...gallery.dom.querySelectorAll('.images img.to-remove')] },
 | |
| 	set toBeRemoved(lst){
 | |
| 		this.queueRemoval(lst) },
 | |
| 	// XXX should this be writable...
 | |
| 	get toBeKeept(){
 | |
| 		return [...gallery.dom.querySelectorAll('.images img:not(.to-remove)')] },
 | |
| 	set toBeKeept(lst){
 | |
| 		this.unqueueRemoval(lst) },
 | |
| 	queueRemoval: function(...images){
 | |
| 		images = images.length == 0 ?
 | |
| 			['current']
 | |
| 			: images
 | |
| 		for(var img of this.getImages(...images)){
 | |
| 			img.classList.add('to-remove') } 
 | |
| 		return this },
 | |
| 	unqueueRemoval: function(...images){
 | |
| 		images = images.length == 0 ?
 | |
| 			['current']
 | |
| 			: images
 | |
| 		for(var img of this.getImages(...images)){
 | |
| 			img.classList.remove('to-remove') } 
 | |
| 		return this },
 | |
| 	toggleQueueRemoval: function(...images){
 | |
| 		images = images.length == 0 ?
 | |
| 			['current']
 | |
| 			: images
 | |
| 		for(var img of this.getImages(...images)){
 | |
| 			img.classList.toggle('to-remove') } 
 | |
| 		return this },
 | |
| 	removeQueued: function(){
 | |
| 		return this.remove(...this.toBeRemoved) },
 | |
| 
 | |
| 	remove: function(...images){
 | |
| 		if(images.includes('all') 
 | |
| 				|| images.includes('*')){
 | |
| 			return this.clear() }
 | |
| 		// NOTE: we need to remove images from the end so as not to be 
 | |
| 		// 		affected by shifed indexes...
 | |
| 		images = this.getImages('sorted', ...images)
 | |
| 			.reverse()
 | |
| 		for(var img of images){
 | |
| 			typeof(img) == 'number' ?
 | |
| 				this.images.at(img)?.remove() 
 | |
| 			: img instanceof Element ?
 | |
| 				(this.images.includes(img) 
 | |
| 					&& img.remove())
 | |
| 			: null }
 | |
| 		return this
 | |
| 			.updateMarkers()
 | |
| 			.update() },
 | |
| 	clear: function(){
 | |
| 		this.dom.querySelector('.images').innerHTML = ''
 | |
| 		return this },
 | |
| 
 | |
| 	// crop...
 | |
| 	__crop_stack: undefined,
 | |
| 	crop: function(lst){
 | |
| 		var stack = this.__crop_stack ??= []
 | |
| 		var state = this.json()
 | |
| 		stack.push(state)
 | |
| 		if(lst){
 | |
| 			state = this.json(this.getImages(lst)) }
 | |
| 		this.load(state)
 | |
| 		return this },
 | |
| 	uncrop: function(lst){
 | |
| 		var state = this.__crop_stack?.pop()
 | |
| 		state 
 | |
| 			&& state.length == 0
 | |
| 			&& (delete this.__crop_stack)
 | |
| 		state 
 | |
| 			&& this.load(state)
 | |
| 		return this },
 | |
| 
 | |
| 	// named crops...
 | |
| 	// XXX add these to serilization...
 | |
| 	// XXX add default loaded crop...
 | |
| 	// XXX might be fun to explore using crops as history...
 | |
| 	__saved_crops: undefined,
 | |
| 	get savedCropNames(){
 | |
| 		return Object.keys(this.__saved_crops ?? {}) },
 | |
| 	saveCrop: function(name){
 | |
| 		var saved = this.__saved_crops ??= {}
 | |
| 		saved[name] = this.json()
 | |
| 		return this },
 | |
| 	loadCrop: function(name){
 | |
| 		var state = (this.__saved_crops ?? {})[name]
 | |
| 		state 
 | |
| 			&& this.load(state)
 | |
| 		return this },
 | |
| 	removeCrop: function(name){
 | |
| 		delete (this.__saved_crops ?? {})[name]
 | |
| 		return this },
 | |
| 
 | |
| 	setup: function(dom){
 | |
| 		var that = this
 | |
| 		this.dom = dom
 | |
| 
 | |
| 		this.toolbars
 | |
| 
 | |
| 		// image clicks...
 | |
| 		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.toggleMark(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.unmark_current){
 | |
| 					that.current = null } })
 | |
| 		this.dom
 | |
| 			.addEventListener('click', function(evt){
 | |
| 				that.unmark_current
 | |
| 					&& (that.current = null) })
 | |
| 
 | |
| 		// drag/drop: sort...
 | |
| 		var dragged
 | |
| 		var dragged_over
 | |
| 		this.dom
 | |
| 			.addEventListener('dragstart', function(evt){
 | |
| 				var i = that.images.indexOf(evt.target)
 | |
| 				if(i >= 0){
 | |
| 					dragged = evt.target 
 | |
| 					dragged.classList.add('dragged') 
 | |
| 					evt.dataTransfer.setData('text/uri-list', evt.target.src)
 | |
| 					evt.dataTransfer.setData('text/plain', evt.target.src)
 | |
| 					// XXX this is ugly in FF...
 | |
| 					//evt.dataTransfer.setDragImage(dragged, evt.offsetX, evt.offsetY)
 | |
| 					// XXX add
 | |
| 				} })
 | |
| 		var skip_dragover = false
 | |
| 		this.dom
 | |
| 			.addEventListener('dragenter', function(evt){
 | |
| 				evt.preventDefault()
 | |
| 				evt.stopPropagation()
 | |
| 				// NOTE: this prevents jumping back and forth if moving 
 | |
| 				// 		image causes a resize that causes the image to 
 | |
| 				// 		move again...
 | |
| 				if(skip_dragover){
 | |
| 					return }
 | |
| 				var target = evt.target
 | |
| 				var i = that.images.indexOf(target)
 | |
| 				if(dragged 
 | |
| 						&& i >= 0){
 | |
| 					evt.offsetX < target.offsetWidth / 2 ?
 | |
| 						target.after(dragged)
 | |
| 						: target.before(dragged) 
 | |
| 					// skip a dragover event if triggered by (right 
 | |
| 					// after) resize...
 | |
| 					skip_dragover = true
 | |
| 					setTimeout(function(){
 | |
| 						skip_dragover = false }, that.resize_settle_timeout)
 | |
| 					that
 | |
| 						.__update_grid_size()
 | |
| 						.updateMarkers() 
 | |
| 					dragged.scrollIntoView({
 | |
| 						behavior: 'smooth',
 | |
| 						block: 'nearest',
 | |
| 					}) } })
 | |
| 		// check if we just went out of the edge image...
 | |
| 		// NOTE: this handles a special case:
 | |
| 		// 			|[A][  B  ][C]   |
 | |
| 		// 		when a narrow image (A, C) is at the edge and the 
 | |
| 		// 		adjacent image is wide (b). 
 | |
| 		// 		dragging the narrow image over the wide places it at 
 | |
| 		// 		the other side of the wide image but the cursor is now 
 | |
| 		// 		over the wide image so to drag back we either need to 
 | |
| 		// 		drag out of it and drag over again (not intuitive) or 
 | |
| 		// 		simply drag over the oppoiste edge of the wide image 
 | |
| 		// 		(dragleave handler)
 | |
| 		// NOTE: we are not implementing the whole drag process here 
 | |
| 		// 		because dragging up/down here is far more complicated 
 | |
| 		// 		than when doing it in dragover...
 | |
| 		// XXX might be a good idea when dragged is null (dragging in files)
 | |
| 		// 		to place a placeholder between images instead of styling
 | |
| 		// 		the image below...
 | |
| 		this.dom
 | |
| 			.addEventListener('dragleave', function(evt){
 | |
| 				// cleanup on drag out...
 | |
| 				// XXX this does not work sometimes...
 | |
| 				if(evt.target === that.dom){
 | |
| 					that.dom.classList.remove('dragged-over')
 | |
| 					dragged_over
 | |
| 						&& dragged_over.classList.remove('dragged-over') }
 | |
| 				// check edge...
 | |
| 				if(!dragged 
 | |
| 						|| skip_dragover){
 | |
| 					return }
 | |
| 				var target = evt.target
 | |
| 				var images = that.images
 | |
| 				var i = images.indexOf(target)
 | |
| 				if(dragged !== target 
 | |
| 						&& i >= 0){
 | |
| 					// left edge...
 | |
| 					if(evt.offsetX <= 0){
 | |
| 						var prev = images[i-1]
 | |
| 						// adjacent image is not on the same offsetTop (edge)
 | |
| 						if(prev == null 
 | |
| 								|| prev.offsetTop != target.offsetTop){
 | |
| 							target.efore(dragged) 
 | |
| 							that.updateMarkers() }
 | |
| 					// right edge...
 | |
| 					} else {
 | |
| 						var next = images[i+1]
 | |
| 						// adjacent image is not on the same offsetTop (edge)
 | |
| 						if(next == null 
 | |
| 								|| next.offsetTop != target.offsetTop){
 | |
| 							target.after(dragged)
 | |
| 							that.updateMarkers() } } } })
 | |
| 		this.dom
 | |
| 			.addEventListener('dragover', function(evt){
 | |
| 				evt.preventDefault()
 | |
| 				evt.stopPropagation()
 | |
| 				evt.dataTransfer.dropEffect = 'copy' 
 | |
| 				var target = evt.target
 | |
| 				var i = that.images.indexOf(target)
 | |
| 				if(!dragged 
 | |
| 						&& i >= 0){
 | |
| 					// XXX BUG: canceling drag leaves the classes in place...
 | |
| 					// 		...handling dragend does not help...
 | |
| 					// indicate replacing all...
 | |
| 					if(!evt.shiftKey){
 | |
| 						that.dom.classList.add('dragged-over')
 | |
| 						target.classList.remove('dragged-over') 
 | |
| 					// indicate insertion...
 | |
| 					} else {
 | |
| 						that.dom.classList.remove('dragged-over')
 | |
| 						dragged_over
 | |
| 							&& dragged_over.classList.remove('dragged-over') 
 | |
| 						dragged_over = target
 | |
| 						dragged_over.classList.add('dragged-over') } } }, false)
 | |
| 		// drag/drop: files...
 | |
| 		// NOTE: if shift is pressed then this will add files to the 
 | |
| 		// 		loaded list, otherwise it will replace the list...
 | |
| 		// XXX handle serilized data...
 | |
| 		this.dom
 | |
| 			.addEventListener('drop', function(evt){
 | |
| 				evt.preventDefault()
 | |
| 				evt.stopPropagation()
 | |
| 
 | |
| 				// non-local drag...
 | |
| 				if(!dragged){
 | |
| 					that.showLoading()
 | |
| 
 | |
| 					var expand = (evt.shiftKey && dragged_over) ?
 | |
| 						that.images.indexOf(dragged_over)
 | |
| 						: undefined
 | |
| 
 | |
| 					var files = 
 | |
| 						evt.dataTransfer.items ?
 | |
| 							[...evt.dataTransfer.items]
 | |
| 								.map(function(elem){
 | |
| 									return elem.kind == 'file' ?
 | |
| 										[elem.getAsFile()]
 | |
| 										: [] })
 | |
| 								.flat()
 | |
| 							: [...evt.dataTransfer.files]
 | |
| 
 | |
| 					Promise.all(files
 | |
| 							.map(function(file){
 | |
| 								// XXX handle serilized data...
 | |
| 								// XXX
 | |
| 
 | |
| 								// images...
 | |
| 								if(file.type.match('image.*')){
 | |
| 									// XXX TEST...
 | |
| 									if(file.path){
 | |
| 										return {
 | |
| 											url: file.path,
 | |
| 											filename: file.name,
 | |
| 										}
 | |
| 									} else {
 | |
| 										return new Promise(function(resolve, reject){
 | |
| 											var reader = new FileReader()
 | |
| 											reader.onload = function(f){
 | |
| 												resolve({
 | |
| 													url: f.target.result,
 | |
| 													filename: file.name,
 | |
| 													// XXX any other metadata to include???
 | |
| 												}) }
 | |
| 											reader.readAsDataURL(file) }) } } 
 | |
| 								// other files...
 | |
| 								return [] })
 | |
| 							.flat())
 | |
| 						.then(
 | |
| 							function(images){
 | |
| 								that.hideLoading()
 | |
| 								// no images...
 | |
| 								if(images.length == 0){
 | |
| 									return }
 | |
| 								return that.load(images, expand) }, 
 | |
| 							function(err){
 | |
| 								// XXX handle errors...
 | |
| 								that.hideLoading() }) }
 | |
| 				dragged
 | |
| 					&& dragged.classList.remove('dragged')
 | |
| 				dragged_over
 | |
| 					&& dragged_over.classList.remove('dragged-over')
 | |
| 				that.dom.classList.remove('dragged-over')
 | |
| 				// XXX if this is used in the promise, move to the point 
 | |
| 				// 		after we nned this...
 | |
| 				dragged = undefined 
 | |
| 				dragged_over = undefined }, false)
 | |
| 		// drag/drom: cleanup...
 | |
| 		// XXX BUG: this does not handle the situation when drag was cancelled 
 | |
| 		// 		but while the browser was not focused leaving the class 
 | |
| 		// 		on the element...
 | |
| 		//		to reproduce:
 | |
| 		//			- start drag of files
 | |
| 		//			- drag over the gallery
 | |
| 		//			- press esc to cancel drag
 | |
| 		//		...this will leave the dragged-over classes in place...
 | |
| 		var cleanupAfterDrag = function(evt){
 | |
| 			that.dom.classList.remove('dragged-over')
 | |
| 			dragged_over
 | |
| 				&& dragged_over.classList.remove('dragged-over') }
 | |
| 		//this.dom.addEventListener('dragend', cleanupAfterDrag) 
 | |
| 		// XXX HACK-ish...
 | |
| 		document.body.addEventListener('dragenter', cleanupAfterDrag) 
 | |
| 		// XXX HACK...
 | |
| 		this.dom.addEventListener('mouseover', cleanupAfterDrag) 
 | |
| 
 | |
| 		// XXX
 | |
| 		for(var img of this.images){
 | |
| 			img.setAttribute('draggable', 'true') }
 | |
| 
 | |
| 		// handle resizing...
 | |
| 		new ResizeObserver(
 | |
| 			function(elems){
 | |
| 				that.__update_grid_size() })
 | |
| 			.observe(this.dom)
 | |
| 		
 | |
| 		return this
 | |
| 			.updateMarkers()
 | |
|    			.update() 
 | |
| 			.hideLoading() },
 | |
| }
 | |
| 
 | |
| 
 | |
| //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| var ContextProto = {
 | |
| 	dom: undefined,
 | |
| 	parent: undefined,
 | |
| 
 | |
| 	constructors: {},
 | |
| 
 | |
| 	end: function(){
 | |
| 		return this.parent },
 | |
| 
 | |
| 	setup: function(){
 | |
| 		return this },
 | |
| }
 | |
| var Context = function(name, constructors, extend={}){
 | |
| 	var obj = this instanceof Context ?
 | |
| 		obj
 | |
| 		: Reflect.construct(
 | |
| 			Function, 
 | |
| 			['return this.setup ? this.setup(...arguments) : this'], 
 | |
| 			Context)
 | |
| 	return obj }
 | |
| 
 | |
| 
 | |
| 
 | |
| var Toolbar = {
 | |
| 	dom: undefined,
 | |
| 	gallery: undefined,
 | |
| 	cls: 'toolbar',
 | |
| 
 | |
| 	// time to hold the handle to toggle autohide...
 | |
| 	hold: 300,
 | |
| 
 | |
| 	// autohide timeout...
 | |
| 	toolbar_autohide: 2000,
 | |
| 
 | |
| 	// XXX make these generic...
 | |
| 	// XXX rename .toggle(..) to something like .toggleExpanded(..)
 | |
| 	toggle: function(action='toggle'){
 | |
| 		var toolbar = this.dom?.classList
 | |
| 		toolbar
 | |
| 			&& (action == 'toggle' ?
 | |
| 					toolbar.toggle('shown')
 | |
| 				: action == 'on' ?
 | |
| 					toolbar.add('shown')
 | |
| 				: toolbar.remove('shown'))
 | |
| 		return this },
 | |
| 	movable: function(action='toggle'){
 | |
| 		var toolbar = this.dom?.classList
 | |
| 		toolbar
 | |
| 			&& (action == 'toggle' ?
 | |
| 					toolbar.toggle('fixed')
 | |
| 				: action == 'off' ?
 | |
| 					toolbar.add('fixed')
 | |
| 				: toolbar.remove('fixed'))
 | |
| 		return this },
 | |
| 
 | |
| 	get shown(){
 | |
| 		return this.dom.style.display != 'none' },
 | |
| 	show: function(){
 | |
| 		// XXX
 | |
| 		this.dom.style.display = ''
 | |
| 		return this },
 | |
| 	hide: function(){
 | |
| 		this.dom.style.display = 'none'
 | |
| 		return this },
 | |
| 
 | |
| 	setup: function(dom, gallery){
 | |
| 		var that = this
 | |
| 		this.dom = dom
 | |
| 		this.gallery = gallery
 | |
| 
 | |
| 		// toolbar...
 | |
| 		var toolbar = this.dom
 | |
| 		var toolbar_moving = false
 | |
| 		// prevent clicks in toolbar from affecting the gallery...
 | |
| 		toolbar
 | |
| 			.addEventListener('click', function(evt){
 | |
| 				evt.stopPropagation() })
 | |
| 
 | |
| 		// toolbar: collapse: click, hold to make sticky...
 | |
| 		var hold_timeout
 | |
| 		var holding_toggle
 | |
| 		var handleInteractionStart = function(evt){
 | |
| 			holding_toggle = true
 | |
| 			hold_timeout = setTimeout(
 | |
| 				function(){
 | |
| 					hold_timeout = undefined
 | |
| 					toolbar.classList.toggle('sticky') }, 
 | |
| 				that.hold ?? 300) }
 | |
| 		var handleInteractionEnd = function(evt){
 | |
| 			if(holding_toggle){
 | |
| 				holding_toggle = false
 | |
| 				evt.preventDefault()
 | |
| 				if(hold_timeout){
 | |
| 					clearTimeout(hold_timeout)
 | |
| 					hold_timeout = undefined 
 | |
| 					that.toggle() } }}
 | |
| 		var collapse_button = toolbar.querySelector('.collapse')
 | |
| 		collapse_button.addEventListener('touchstart', handleInteractionStart)
 | |
| 		collapse_button.addEventListener('mousedown', handleInteractionStart)
 | |
| 		collapse_button.addEventListener('touchend', handleInteractionEnd) 
 | |
| 		collapse_button.addEventListener('mouseup', handleInteractionEnd) 
 | |
| 
 | |
| 		// toolbar: autohide...
 | |
| 		var hide_timeout
 | |
| 		var toolbarAutoHideTimerStart = function(evt){
 | |
| 			if(that.autohide == 0 
 | |
| 					|| toolbar.classList.contains('sticky')){
 | |
| 				return }
 | |
| 			hide_timeout = setTimeout(
 | |
| 				function(){
 | |
| 					hide_timeout = undefined
 | |
| 					that.toggle('hide') }, 
 | |
| 				that.autohide ?? 1000) }
 | |
| 		var toolbarAutoHideCancel = function(evt){
 | |
| 			hide_timeout
 | |
| 				&& clearTimeout(hide_timeout) 
 | |
| 			hide_timeout = undefined }
 | |
| 		toolbar.addEventListener('mouseout', toolbarAutoHideTimerStart)
 | |
| 		toolbar.addEventListener('touchend', toolbarAutoHideTimerStart)
 | |
| 		toolbar.addEventListener('mouseover', toolbarAutoHideCancel)
 | |
| 		toolbar.addEventListener('touchstart', toolbarAutoHideCancel)
 | |
| 
 | |
| 		// toolbar: move...
 | |
| 		// XXX to drag anywhere on the elem we need to prevent 
 | |
| 		// 		clicks while dragging...
 | |
| 		toolbar.moveable({
 | |
| 			cls: 'moving',
 | |
| 			handle: '.drag-handle',
 | |
| 			// set bounds...
 | |
| 			start: function(elem, data){
 | |
| 				if(elem.classList.contains('fixed')){
 | |
| 					return false }
 | |
| 				data.bounds = {
 | |
| 					top: 0,
 | |
| 					left: 0,
 | |
| 					right: that.gallery.dom.offsetWidth - elem.offsetWidth - 20,
 | |
| 					bottom: that.gallery.dom.offsetHeight - elem.offsetHeight - 20,
 | |
| 				} },
 | |
| 			//keepInView: 'scroll',
 | |
| 			keepInView: 'strict',
 | |
| 			move: function(elem, data){
 | |
| 				data.x != null
 | |
| 					&& elem.style.setProperty('--move-x', data.x + 'px') 
 | |
| 				data.y != null
 | |
| 					&& elem.style.setProperty('--move-y', data.y + 'px') }, })
 | |
| 
 | |
| 		// keep toolbar in view while scrolling...
 | |
| 		// NOTE: top is taken care of by position: sticky...
 | |
| 		window.addEventListener('scroll', function(evt){
 | |
| 			var bb = toolbar.getBoundingClientRect()
 | |
| 			if(window.innerHeight <= bb.bottom){
 | |
| 				var top = toolbar.offsetTop - bb.top
 | |
| 				var bottom = window.innerHeight + top - toolbar.offsetHeight
 | |
| 				toolbar.style.setProperty('--move-y', bottom + 'px') } })
 | |
| 
 | |
| 		return this },
 | |
| }
 | |
| 
 | |
| 
 | |
| //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| // This is a prototype for all modal views...
 | |
| 
 | |
| var Overlay = {
 | |
| 	dom: undefined,
 | |
| 	gallery: undefined,
 | |
| 	cls: 'overlay',
 | |
| 
 | |
| 	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
 | |
| 			&& this.cache_count != 0
 | |
| 			&& this.cache() },
 | |
| 
 | |
| 	__clicks_canceled: false,
 | |
| 	__clicks_canceled_timeout: 500,
 | |
| 
 | |
| 	get shown(){
 | |
| 		return this.gallery.dom.classList.contains(this.cls) },
 | |
| 	show: function(){
 | |
| 		var that = this
 | |
| 		this.__clicks_canceled = true
 | |
| 		setTimeout(
 | |
| 			function(){
 | |
| 				that.__clicks_canceled = false }, 
 | |
| 			this.__clicks_canceled_timeout ?? 200)
 | |
| 		this.update()
 | |
| 		this.gallery.dom.classList.add(this.cls)
 | |
| 		return this },
 | |
| 	hide: function(){
 | |
| 		this.gallery.exit_fullscreen_on_lightbox_close
 | |
| 			&& document.fullscreenElement
 | |
| 			&& document.exitFullscreen()
 | |
| 		this.gallery.dom.classList.remove(this.cls)
 | |
| 		return this },
 | |
| 	toggle: function(){
 | |
| 		return this.shown ?
 | |
| 			this.hide()
 | |
| 			: this.show() },
 | |
| 
 | |
| 	cache: null,
 | |
| 
 | |
| 	update: function(){
 | |
| 		this.url = 
 | |
| 			(this.gallery.current
 | |
| 				?? this.gallery.next().current
 | |
| 				?? {}).src
 | |
| 		var caption = 
 | |
| 			(this.gallery.current
 | |
| 					?? this.gallery.next().current
 | |
| 					?? {})
 | |
| 				.getAttribute('caption') 
 | |
| 					?? ''
 | |
| 		var index = (this.gallery.index+1) +'/'+ this.gallery.length
 | |
| 		// set caption...
 | |
| 		// XXX should these be separate elements???
 | |
| 		this.dom.setAttribute('caption',
 | |
| 			(this.caption_format ?? '${CAPTION}')
 | |
| 				.replace(/\${CAPTION}/, caption)
 | |
| 				.replace(/\${INDEX}/, index))
 | |
| 		// set selection...
 | |
| 		this.gallery.current.classList.contains('marked') ?
 | |
| 			this.dom.classList.add('marked')
 | |
| 			: this.dom.classList.remove('marked')
 | |
| 		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()
 | |
| 				if(that.__clicks_canceled){
 | |
| 					return }
 | |
| 				that.hide() })
 | |
| 		this.dom.querySelector('.fullscreen')
 | |
| 			?.addEventListener('click', function(evt){
 | |
| 				evt.stopPropagation()
 | |
| 				if(that.__clicks_canceled){
 | |
| 					return }
 | |
| 				document.fullscreenElement ?
 | |
| 					document.exitFullscreen()
 | |
| 					: that.dom.requestFullscreen() })
 | |
| 		this.dom.querySelector('.info')
 | |
| 			?.addEventListener('click', function(evt){
 | |
| 				evt.stopPropagation()
 | |
| 				if(that.__clicks_canceled){
 | |
| 					return }
 | |
| 				that.gallery.details.show() })
 | |
| 		this.dom.querySelector('.prev')
 | |
| 			?.addEventListener('click', function(evt){
 | |
| 				evt.stopPropagation()
 | |
| 				if(that.__clicks_canceled){
 | |
| 					return }
 | |
| 				that.gallery.prev() })
 | |
| 		this.dom.querySelector('.next')
 | |
| 			?.addEventListener('click', function(evt){
 | |
| 				evt.stopPropagation()
 | |
| 				if(that.__clicks_canceled){
 | |
| 					return }
 | |
| 				that.gallery.next() })
 | |
| 		// stop body from scrolling...
 | |
| 		var stop = function(evt){
 | |
| 			evt.stopPropagation()
 | |
| 			evt.preventDefault()
 | |
| 			return false }
 | |
| 		this.dom.addEventListener('touchmove', stop)
 | |
| 		this.dom.addEventListener('mousewheel', stop)
 | |
| 		this.dom.addEventListener('wheel', stop)
 | |
| 		this.dom.addEventListener('scroll', stop)
 | |
| 		// drag...
 | |
| 		this.dom
 | |
| 			.addEventListener('dragover', function(evt){
 | |
| 				that.gallery.dom.scrollIntoView({
 | |
| 					behavior: 'smooth',
 | |
| 					block: 'nearest',
 | |
| 				})
 | |
| 				that.hide() })
 | |
| 		return this },
 | |
| }
 | |
| 
 | |
| 
 | |
| //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | |
| 
 | |
| var Lightbox = {
 | |
| 	__proto__: Overlay,
 | |
| 
 | |
| 	cls: 'lightboxed',
 | |
| 
 | |
| 	caption_format: '${INDEX} ${CAPTION}',
 | |
| 
 | |
| 	navigation_deadzone: 0.3,
 | |
| 	caption_hysteresis: 10,
 | |
| 	cache_count: 1,
 | |
| 
 | |
| 	__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
 | |
| 
 | |
| 		Overlay.setup.call(this, ...arguments)
 | |
| 
 | |
| 		// click zones...
 | |
| 		var dblclick_canceled = false
 | |
| 		var deadzone = this.navigation_deadzone ?? 0.3
 | |
| 		this.dom
 | |
| 			.addEventListener('click', function(evt){
 | |
| 				evt.stopPropagation()
 | |
| 				if(that.__clicks_canceled){
 | |
| 					return }
 | |
| 				var d = that.dom.offsetWidth * deadzone
 | |
| 				// click left/right side of view...
 | |
| 				// NOTE: this is vewport-relative...
 | |
| 				dblclick_canceled = false
 | |
| 				evt.clientX < that.dom.offsetWidth / 2 - d/2
 | |
| 					&& that.gallery.prev()
 | |
| 					&& (dblclick_canceled = true)
 | |
| 				evt.clientX > that.dom.offsetWidth / 2 + d/2
 | |
| 					&& that.gallery.next() 
 | |
| 					&& (dblclick_canceled = true) })
 | |
| 		this.dom
 | |
| 			.addEventListener('dblclick', function(evt){
 | |
| 				evt.stopPropagation()
 | |
| 				dblclick_canceled
 | |
| 					|| that.__clicks_canceled
 | |
| 					|| that.hide() })
 | |
| 		// hover zones...
 | |
| 		var hysteresis = this.caption_hysteresis ?? 10
 | |
| 		this.dom
 | |
| 			.addEventListener('mousemove', function(evt){
 | |
| 				var d = that.dom.offsetWidth * deadzone
 | |
| 				// indicate action...
 | |
| 				if(evt.clientX < that.dom.offsetWidth / 2 - d/2){
 | |
| 					that.dom.classList.contains('clickable')
 | |
| 						|| that.dom.classList.add('clickable')
 | |
| 				} else if( evt.clientX > that.dom.offsetWidth / 2 + d/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 Details = {
 | |
| 	__proto__: Overlay,
 | |
| 
 | |
| 	cls: 'detailed',
 | |
| 
 | |
| 	// XXX
 | |
| }
 | |
| 
 | |
| 
 | |
| 
 | |
| //---------------------------------------------------------------------
 | |
| 
 | |
| 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 -- handle multiple galleries...
 | |
| 		window.gallery = setupGallery(gallery) } 
 | |
| 	// keyboard...
 | |
| 	document.addEventListener('keydown', function(evt){
 | |
| 		if(window.gallery.loading){
 | |
| 			return }
 | |
| 		var key = evt.key
 | |
| 		if(key in keyboard){
 | |
| 			keyboard[key](evt) } }) }
 | |
| 	
 | |
| 
 | |
| 
 | |
| //---------------------------------------------------------------------
 | |
| // vim:set ts=4 sw=4 :
 |