Compare commits

...

10 Commits

Author SHA1 Message Date
e0de8a9a13 several options and configs added + cleanup...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2023-07-23 06:46:14 +03:00
673912b219 more tweaks...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2023-07-22 18:41:49 +03:00
3c7ed59b26 lots of tweaks...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2023-07-22 15:33:55 +03:00
f869066082 added click to select option...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2023-07-22 00:11:20 +03:00
b8a0146d63 bugfix...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2023-07-21 21:24:40 +03:00
b156ba8e7b added lightbox selection markers + tweaks and fixes...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2023-07-21 14:40:30 +03:00
6a00c1e4df added todo...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2023-07-21 13:29:39 +03:00
93b8f63057 minor tweaks
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2023-07-21 03:16:20 +03:00
209202945a cleanup and tweaks...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2023-07-20 22:06:51 +03:00
03a79d3f22 added selection...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2023-07-20 21:42:17 +03:00
3 changed files with 338 additions and 84 deletions

View File

@ -9,6 +9,15 @@
:root {
/* dimensions */
--gallery-current-border-size: 0.7em;
--gallery-padding: 3em;
--gallery-padding-horizontal: var(--gallery-padding);
--gallery-padding-vertical: var(--gallery-current-border-size);
--gallery-padding-top: var(--gallery-padding-vertical);
--gallery-padding-bottom: var(--gallery-padding-vertical);
--gallery-padding-left: var(--gallery-padding-horizontal);
--gallery-padding-right: var(--gallery-padding-horizontal);
--gallery-image-scroll-margin: 1em;
--gallery-scrollbar-width: 0.5em;
--lightbox-frame-size: 5vmin;
@ -22,6 +31,7 @@
--lightbox-text-color: black;
--lightbox-background-color: white;
/*--lightbox-background-color: rgba(0,0,0,0.8);*/
}
.gallery-dark {
@ -62,30 +72,72 @@ body {
/* XXX need to account for scrollbar popping in and out */
.gallery {
padding-top: var(--gallery-padding-top);
padding-bottom: var(--gallery-padding-bottom);
padding-left: calc(
var(--gallery-scrollbar-width)
+ var(--gallery-padding-left) );
padding-right: var(--gallery-padding-right);
color: var(--gallery-text-color);
background: var(--gallery-background-color);
}
.gallery .images {
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;
color: var(--gallery-text-color);
background: var(--gallery-background-color);
}
.gallery img {
.gallery .images img {
height: 300px;
width: auto;
scroll-margin: var(--gallery-image-scroll-margin);
image-rendering: crisp-edges;
box-sizing: border-box;
cursor: pointer;
}
.gallery>img {
cursor: hand;
/* selection marker... */
.gallery .images img.current {
z-index: 1;
box-shadow:
0px 0px 0px var(--gallery-current-border-size) rgba(255,255,255,1),
0.4em 0.4em 3em 0em rgba(0,0,0,0.8);
}
.gallery img.current {
border: solid 2px red;
/*************************************************** Image markers ***/
.gallery .images img+.mark {
z-index: 1;
position: relative;
}
.gallery.lightboxed .images img+.mark {
z-index: 2;
position: fixed;
bottom: 0;
right: 0;
}
/* marker: selected */
.gallery .images img+.mark.selected:after {
content: "";
position: absolute;
display: block;
width: 1em;
height: 1em;
right: 0.5em;
bottom: 0.5em;
border-radius: 50%;
background: blue;
box-shadow: 0em 0em 0em 0.05em rgba(255,255,255,1);
cursor: pointer;
}
/******************************************************* Lightbox ****/
@ -97,20 +149,24 @@ body {
height: 100%;
top: 0px;
left: 0px;
z-index: 1;
text-align: center;
color: var(--lightbox-text-color);
background: var(--lightbox-background-color);
}
.gallery .lightbox.show-caption:after {
.gallery.lightboxed .lightbox {
display: block;
}
.gallery .lightbox.show-caption:before {
content: attr(caption);
position: absolute;
bottom: 0.5em;
left: 0.5em;
}
.gallery .lightbox.clickable {
cursor: hand;
cursor: pointer;
}
/* XXX add metadata display... */
.gallery .lightbox img {
@ -126,14 +182,14 @@ body {
* var(--lightbox-image-margin-top));
}
/* controls: next/prev... */
.lightbox .button {
cursor: hand;
.gallery .lightbox .button {
cursor: pointer;
font-size: var(--lightbox-button-size);
padding: 0 0.25em;
filter: saturate(0);
opacity: 0.1;
}
.lightbox .button:hover {
.gallery .lightbox .button:hover {
opacity: 1;
filter: saturate(1);
}

View File

@ -18,26 +18,55 @@
</head>
<body onload="setup()">
<h3>ToDo</h3>
<pre>
- <s>Gallery: Adaptable image justification in grid</s>
- Can we make this passive??? (i.e. CSS only)
- <s>Make more accurate -- align right side to pixel...</s>
- <s>Gallery: Spacial navigation (up/down/left/right)</s>
- <b>option: .loop_images (in porgress)</b>
- Up/Down: might be a good idea to err slightly to the left
- Gallery: PageUp/PageDown, home/end + allow page navigation
- <b>Gallery: focus visible (if no current)...</b>
- <s>Gallery/Lightbox: Selection of images (space / ctrl-a / ctrl-d / ctrl-i)</s>
- <s>Lightbox: show selection marker</s>
- <b>Gallery: constructor (list of urls)</b>
- <b>Gallery: views</b>
- "make view from selection"
- close view
- multiple view stack
- Gallery: drop images
- Gallery: drag to sort
- Gallery: remove image
- <b>Gallery: serialize / deserialize</b>
- <s>Lightbox: navigation (keyboard / mouse)</s>
- Lightbox: fullscreen mode
- Gallery: element (???)
- ...
</pre>
<hr>
<div class="gallery">
<!-- gallery: content -->
<div class="images">
<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>
<!-- 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>

View File

@ -19,8 +19,9 @@
// XXX need to account for scrollbar -- add hysteresis???
var patchFlexRows =
function(elems){
var W = elems[0].parentElement.clientWidth - 2
function(elems, prevent_row_expansion=false){
// NOTE: -1 here is to compensate for rounding errors...
var W = elems[0].parentElement.clientWidth - 1
var w = 0
var h
var row = []
@ -37,21 +38,26 @@ function(elems){
if(elem.offsetTop == top){
w += elem.offsetWidth
row.push(elem)
// next row...
// row donw + prep for next...
} else {
// NOTE: we are checking which will require a lesser resize
// the current row or it with the next image...
var r1 = W / w
var r2 = W / (w + elem.offsetWidth)
var expanded_row = 1/r1 < r2
var expanded_row =
prevent_row_expansion ?
false
: 1/r1 < r2
if(!expanded_row){
var r = r1
} else {
var r = r2
row.push(elem) }
// patch the row...
var nw = 0
for(var e of row){
e.style.height = Math.floor(h * r) + 'px' }
e.style.height = (h * r) + 'px'
nw += e.offsetWidth }
// prep for next row...
if(!expanded_row){
w = elem.offsetWidth
@ -87,6 +93,7 @@ function(elem) {
//---------------------------------------------------------------------
// XXX add shift+arrow to select...
// XXX add home/end, pageup/pagedown...
var keyboard = {
ArrowLeft: function(){
@ -108,8 +115,31 @@ var keyboard = {
Enter: function(){
gallery.lightbox.toggle() },
Escape: function(){
gallery.lightbox.shown
&& gallery.lightbox.hide() },
gallery.lightbox.shown ?
gallery.lightbox.hide()
// XXX should we remember which image was current and select
// it again when needed???
: gallery.deselect_current ?
(gallery.current = null)
: null },
// selection...
' ': function(evt){
gallery.current
&& evt.preventDefault()
gallery.toggleSelect() },
// XXX use key codes...
'a': function(evt){
evt.preventDefault()
if(evt.ctrlKey){
gallery.selectAll() } },
'd': function(evt){
evt.preventDefault()
if(evt.ctrlKey){
gallery.deselectAll() } },
'i': function(evt){
evt.preventDefault()
if(evt.ctrlKey){
gallery.selectInverse() } },
}
@ -117,6 +147,27 @@ var keyboard = {
//---------------------------------------------------------------------
var Gallery = {
// options...
//
deselect_current: true,
loop_images: true,
allow_row_expansion: true,
click_to_select: true,
code: `
<div class="gallery">
<!-- gallery: content -->
<div class="images">
</div>
<!-- lightbox -->
<div class="lightbox">
<img>
<div class="button close"></div>
</div>
</div>`,
dom: undefined,
__lightbox: undefined,
@ -131,44 +182,70 @@ var Gallery = {
return undefined },
get current(){
return this.dom.querySelector('img.current') },
return this.dom.querySelector('.images img.current') },
set current(img){
for(var i of this.dom.querySelectorAll('img.current')){
// unset...
if(img == null){
this.current?.classList.remove('current')
return }
// set...
for(var i of this.dom.querySelectorAll('.images img.current')){
i.classList.remove('current') }
img.classList.add('current')
// XXX add offsets from borders...
img.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
offset: 10,
}) },
// XXX should this be writable???
get images(){
return [...this.dom.querySelectorAll('.images img')] },
get urls(){
return this.images
.map(function(img){
// XXX not sure if we should remove the preview dir...
return img.src }) },
/*/
return img.src
// remove preview dir...
.replace(/\/[0-9]+px\//, '/') }) },
//*/
// XXX add .loop_images support...
getRow: function(img, direction='current'){
if(['above', 'current', 'below'].includes(img)){
direction = img
img = null }
img = this.current }
// get above/below row...
// XXX these are wastefull...
if(direction == 'above'){
var row = this.getRow(img)
var e = row[0].previousSibling
var e = row[0].previousElementSibling
while(e && e.tagName != 'IMG'){
e = e.previousSibling }
e = e.previousElementSibling }
return e ?
this.getRow(e)
: this.getRow([...this.dom.querySelectorAll('img')].at(-1))
: this.getRow(this.images.at(-1))
} else if(direction == 'below'){
// special case: nothing selected...
if(img == null){
return this.getRow() }
var row = this.getRow(img)
var e = row.at(-1).nextSibling
var e = row.at(-1).nextElementSibling
while(e && e.tagName != 'IMG'){
e = e.nextSibling }
e = e.nextElementSibling }
return e ?
this.getRow(e)
: this.getRow([...this.dom.querySelectorAll('img')][1]) }
: this.getRow(this.images[0]) }
// 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)
var images = this.images
for(cur of images){
if(cur.offsetTop >= scroll){
break } } }
@ -177,17 +254,18 @@ var Gallery = {
var e = cur
while(e && e.offsetTop == top){
row.push(e)
e = e.nextSibling
e = e.nextElementSibling
while(e && e.tagName != 'IMG'){
e = e.nextSibling } }
e = e.nextElementSibling } }
e = cur
while(e && e.offsetTop == top){
e === cur
|| row.unshift(e)
e = e.previousSibling
e = e.previousElementSibling
while(e && e.tagName != 'IMG'){
e = e.previousSibling } }
e = e.previousElementSibling } }
return row },
// XXX add .loop_images support...
getImage: function(img, direction='current'){
if(['left', 'above', 'current', 'below', 'right'].includes(img)){
direction = img
@ -196,7 +274,7 @@ var Gallery = {
if(direction == 'current'){
return img
?? this.current
?? this.getRow(img)
?? this.getRow(img)[0]
// above/below...
} else if(direction == 'above' || direction == 'below'){
var row = this.getRow(direction)
@ -231,8 +309,9 @@ var Gallery = {
return target },
// XXX cache image list???
// XXX add .loop_images support...
prev: function(){
var images = [...this.dom.querySelectorAll('img')].slice(1)
var images = this.images
var i = this.current == null ?
images.length-1
: images.indexOf(this.current)-1
@ -242,7 +321,7 @@ var Gallery = {
this.current = images[i]
return this },
next: function(){
var images = [...this.dom.querySelectorAll('img')].slice(1)
var images = this.images
var i = this.current == null ?
0
: images.indexOf(this.current)+1
@ -276,29 +355,115 @@ var Gallery = {
this.current = this.getImage('below')
return this },
// XXX
select: function(){
},
// selection...
get selected(){
return this.dom.querySelectorAll('.images img.selected') },
// NOTE: this is here because we can't use :before / :after directly
// on the img tag...
// XXX make this generic and use a .marks list...
updateMarkers: function(){
var that = this
// select...
for(var img of this.dom.querySelectorAll('.images img.selected')){
var mark = img.nextElementSibling
while(mark && mark.tagName != 'IMG' && !mark.classList.contains('mark')){
mark = img.nextElementSibling }
if(!mark || !mark.classList.contains('mark')){
mark = document.createElement('div')
mark.classList.add('selected', 'mark')
mark.addEventListener('click', function(evt){
evt.stopPropagation()
that.deselect(mark) })
img.after(mark) } }
// clear deselected...
for(var mark of this.dom.querySelectorAll('.images img:not(.selected)+.mark')){
mark.remove() }
// update lightbox...
this.lightbox.shown
&& this.lightbox.update()
return this },
select: function(img){
img = img ?? this.current
img?.classList.add('selected')
return this.updateMarkers() },
deselect: function(img){
img = img ?? this.current
img?.classList.remove('selected')
return this.updateMarkers() },
toggleSelect: function(img){
img = img ?? this.current
img?.classList.toggle('selected')
this.updateMarkers()
return this },
selectAll: function(){
for(var img of this.images){
img.classList.add('selected') }
return this.updateMarkers() },
deselectAll: function(){
for(var img of this.images){
img.classList.remove('selected') }
return this.updateMarkers() },
selectInverse: function(){
for(var img of this.images){
img.classList.toggle('selected') }
return this.updateMarkers() },
show: function(){
this.lightbox.show()
return this },
// XXX
update: function(){
patchFlexRows(this.images, !this.allow_row_expansion)
return this },
load: function(urls){
},
this.clear()
var images = this.dom.querySelector('.images')
for(var url of urls){
var img = document.createElement('img')
img.src = url
images.appendChild(img) }
return this },
clear: function(){
this.dom.querySelector('.images').innerHTML = ''
return this },
setup: function(dom){
var that = this
this.dom = dom
this.dom.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 },
this.dom.querySelector('.images')
.addEventListener('click', function(evt){
evt.stopPropagation()
var target = evt.target
if(target.tagName == 'IMG'){
// shift+click: toggle selections...
if(evt.shiftKey){
that.toggleSelect(target)
// first click selects, second shows...
} else if(that.click_to_select){
target.classList.contains('current') ?
that.show()
: (that.current = target)
// first click selects and shows...
} else {
that.current = target
that.show() }
} else if(that.deselect_current){
that.current = null } })
this.dom
.addEventListener('click', function(evt){
that.deselect_current
&& (that.current = null) })
// handle resizing...
new ResizeObserver(
function(elems){
that.update() })
.observe(this.dom)
return this
.update() },
}
@ -326,12 +491,24 @@ var Lightbox = {
&& this.cache() },
get shown(){
return this.dom.style.display == 'block' },
return this.gallery.dom.classList.contains('lightboxed') },
show: function(url){
this.url = url
?? (this.gallery.current
?? this.gallery.next().current
?? {}).src
this.update()
this.gallery.dom.classList.add('lightboxed')
return this },
hide: function(){
this.gallery.dom.classList.remove('lightboxed')
return this },
toggle: function(){
return this.shown ?
this.hide()
: this.show() },
update: function(){
// set caption...
this.dom.setAttribute('caption',
(this.gallery.current
@ -339,15 +516,11 @@ var Lightbox = {
?? {})
.getAttribute('caption')
?? '')
this.dom.style.display = 'block'
// set selection...
this.gallery.current.classList.contains('selected') ?
this.dom.classList.add('selected')
: this.dom.classList.remove('selected')
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()
@ -392,13 +565,14 @@ var Lightbox = {
var deadzone = this.navigation_deadzone ?? 100
this.dom
.addEventListener('click', function(evt){
evt.stopPropagation()
// click left/right side of view...
// NOTE: this is vewport-relative...
evt.clientX < that.dom.offsetWidth / 2 - deadzone/2
&& that.prev()
evt.clientX > that.dom.offsetWidth / 2 + deadzone/2
&& that.next() })
// mouseofver...
// mousemove...
var hysteresis = this.caption_hysteresis ?? 10
this.dom
.addEventListener('mousemove', function(evt){
@ -435,8 +609,6 @@ var setupGallery = function(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...
@ -445,10 +617,7 @@ var setup = function(){
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')]) })
}
keyboard[key](evt) } }) }