added a full version of the interface (almost feature complete)
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
parent
f4b6bf070b
commit
1f5fd5cd33
594
grid-n-view.html
Normal file
594
grid-n-view.html
Normal file
@ -0,0 +1,594 @@
|
||||
<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 : -->
|
||||
Loading…
x
Reference in New Issue
Block a user