gallery/grid-n-view.html
2023-07-17 13:03:51 +03:00

595 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<html>
<meta charset="utf-8">
<style>
/********************************************************* Config ****/
:root {
/* dimensions */
--gallery-scrollbar-width: 0.5em;
--lightbox-frame-size: 5vmin;
--lightbox-image-margin-top: 0.75;
--lightbox-button-size: 4em;
/* theme */
--gallery-text-color: black;
--gallery-secondary-color: silver;
--gallery-background-color: white;
--lightbox-background-color: white;
}
/****************************************************** Scrolling ****/
::-webkit-scrollbar {
width: var(--gallery-scrollbar-width);
}
::-webkit-scrollbar-track {
background-color: transparent;
border-radius: 100px;
}
::-webkit-scrollbar-thumb {
background-color: var(--gallery-secondary-color);
border-radius: 100px;
}
body {
overflow-y: scroll;
}
/******************************************************** Gallery ****/
/* XXX need to account for scrollbar popping in and out */
.gallery {
position: relative;
display: flex;
justify-content: flex-start;
align-content: flex-start;
flex-flow: row wrap;
margin-left: var(--gallery-scrollbar-width);
margin-right: 0;
}
.gallery img {
height: 300px;
width: auto;
image-rendering: crisp-edges;
box-sizing: border-box;
}
.gallery>img {
cursor: hand;
}
.gallery img.current {
border: solid 2px red;
}
/******************************************************* Lightbox ****/
.gallery .lightbox {
display: none;
position: fixed;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
text-align: center;
background: var(--lightbox-background-color);
}
.gallery .lightbox.show-caption:after {
content: attr(caption);
position: absolute;
bottom: 0.5em;
left: 0.5em;
}
.gallery .lightbox.clickable {
cursor: hand;
}
/* XXX add metadata display... */
.gallery .lightbox img {
object-fit: contain;
width: calc(
100vw
- var(--lightbox-frame-size) * 2);
height: calc(
100vh
- var(--lightbox-frame-size) * 2);
margin-top: calc(
var(--lightbox-frame-size)
* var(--lightbox-image-margin-top));
}
/* controls: next/prev... */
.lightbox .button {
cursor: hand;
font-size: var(--lightbox-button-size);
padding: 0 0.25em;
filter: saturate(0);
opacity: 0.1;
}
.lightbox .button:hover {
opacity: 1;
filter: saturate(1);
}
/* controls: close... */
.gallery .lightbox .button.close {
position: absolute;
top: 0px;
right: 0px;
}
.gallery .lightbox .button.close:after {
content: "×";
color: red;
}
/*********************************************************************/
</style>
<script>
//---------------------------------------------------------------------
// 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 left/right side of view...
var deadzone = this.navigation_deadzone ?? 100
this.dom
.addEventListener('click', function(evt){
// 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() })
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')]) })
}
//---------------------------------------------------------------------
</script>
<body onload="setup()">
<div class="gallery">
<!-- lightbox -->
<div class="lightbox">
<img>
<div class="button close"></div>
</div>
<!-- gallery: content -->
<img src="images/500px/1.JPG" caption="Caption text">
<img src="images/500px/2.JPG">
<img src="images/500px/3.JPG">
<img src="images/500px/DSC08102.jpg">
<img src="images/500px/4.JPG">
<img src="images/500px/5.JPG">
<img src="images/500px/DSC08102.jpg">
<img src="images/500px/6.JPG">
<img src="images/500px/DSC08102.jpg">
<img src="images/500px/2.JPG">
<img src="images/500px/5.JPG">
</div>
</body>
</html>
<!-- vim:set ts=4 sw=4 : -->