2023-07-17 13:37:10 +03:00
|
|
|
//=====================================================================
|
|
|
|
|
//
|
|
|
|
|
//
|
|
|
|
|
//
|
|
|
|
|
// TODO:
|
|
|
|
|
// - drag-n-drop
|
|
|
|
|
// - sort/move
|
|
|
|
|
// - crop selection
|
|
|
|
|
// - make the gallery into a web component
|
|
|
|
|
//
|
|
|
|
|
//
|
|
|
|
|
//=====================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-07-17 13:13:09 +03:00
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
|
2023-08-07 17:16:24 +03:00
|
|
|
// This compansates for any resize rounding errors in patchFlexRows(..).
|
|
|
|
|
var PATCH_MARGIN = 2
|
2023-08-11 13:21:01 +03:00
|
|
|
var DRAG_DEAD_ZONE = 0.2
|
2023-08-07 17:16:24 +03:00
|
|
|
|
2023-07-17 13:13:09 +03:00
|
|
|
var patchFlexRows =
|
2023-08-07 17:21:55 +03:00
|
|
|
function(elems,
|
|
|
|
|
prevent_row_expansion=false,
|
|
|
|
|
last_row_resize=1.5,
|
|
|
|
|
patch_margin=PATCH_MARGIN){
|
2023-07-28 19:41:44 +03:00
|
|
|
if(elems.length == 0){
|
|
|
|
|
return }
|
2023-07-21 14:40:30 +03:00
|
|
|
// NOTE: -1 here is to compensate for rounding errors...
|
2023-08-07 17:21:55 +03:00
|
|
|
var W = elems[0].parentElement.clientWidth - patch_margin
|
2023-07-17 13:13:09 +03:00
|
|
|
var w = 0
|
|
|
|
|
var h
|
|
|
|
|
var row = []
|
|
|
|
|
var top = elems[0].offsetTop
|
2023-07-28 23:19:39 +03:00
|
|
|
// 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){
|
2023-08-07 17:16:24 +03:00
|
|
|
if(r == 0 || h == 0){
|
|
|
|
|
e.style.height = ''
|
|
|
|
|
} else {
|
|
|
|
|
e.style.height = (h * r) + 'px' }
|
2023-07-28 23:19:39 +03:00
|
|
|
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...
|
2023-07-17 13:13:09 +03:00
|
|
|
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)
|
2023-07-28 23:19:39 +03:00
|
|
|
// row done + prep for next...
|
2023-07-17 13:13:09 +03:00
|
|
|
} else {
|
2023-07-21 14:40:30 +03:00
|
|
|
var expanded_row =
|
|
|
|
|
prevent_row_expansion ?
|
2023-07-28 23:19:39 +03:00
|
|
|
handleRow()
|
|
|
|
|
: handleRow('expand')
|
2023-07-17 13:13:09 +03:00
|
|
|
// 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
|
2023-07-28 23:19:39 +03:00
|
|
|
row = [] }}}
|
|
|
|
|
// handle last row...
|
|
|
|
|
last_row_resize
|
|
|
|
|
&& handleRow(last_row_resize) }
|
2023-07-17 13:13:09 +03:00
|
|
|
|
|
|
|
|
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) }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
|
2023-07-22 15:33:55 +03:00
|
|
|
// XXX add shift+arrow to select...
|
2023-07-17 13:13:09 +03:00
|
|
|
// XXX add home/end, pageup/pagedown...
|
2023-08-03 15:20:42 +03:00
|
|
|
// XXX need a real ui stack -- close things top to bottom (Enter/Escape/...)...
|
2023-07-17 13:13:09 +03:00
|
|
|
var keyboard = {
|
|
|
|
|
ArrowLeft: function(){
|
2023-08-03 14:10:59 +03:00
|
|
|
gallery.prev() },
|
2023-07-17 13:13:09 +03:00
|
|
|
ArrowRight: function(){
|
2023-08-03 14:10:59 +03:00
|
|
|
gallery.next() },
|
2023-07-24 21:04:42 +03:00
|
|
|
// NOTE: up/down will not prevent the default scrolling behavior
|
|
|
|
|
// when at top/bottom of the gallery.
|
2023-07-17 13:13:09 +03:00
|
|
|
ArrowUp: function(evt){
|
2023-07-24 21:04:42 +03:00
|
|
|
gallery.__at_top_row
|
|
|
|
|
|| evt.preventDefault()
|
2023-08-03 14:10:59 +03:00
|
|
|
;(gallery.dom.classList.contains('lightboxed')
|
|
|
|
|
|| gallery.dom.classList.contains('detailed'))
|
|
|
|
|
&& evt.preventDefault()
|
2023-07-17 13:13:09 +03:00
|
|
|
gallery.lightbox.shown
|
2023-08-03 14:10:59 +03:00
|
|
|
|| gallery.details.shown
|
2023-07-17 13:13:09 +03:00
|
|
|
|| gallery.up() },
|
|
|
|
|
ArrowDown: function(evt){
|
2023-07-24 21:04:42 +03:00
|
|
|
gallery.__at_bottom_row
|
|
|
|
|
|| evt.preventDefault()
|
2023-08-03 14:10:59 +03:00
|
|
|
;(gallery.dom.classList.contains('lightboxed')
|
|
|
|
|
|| gallery.dom.classList.contains('detailed'))
|
|
|
|
|
&& evt.preventDefault()
|
2023-07-17 13:13:09 +03:00
|
|
|
gallery.lightbox.shown
|
2023-08-03 14:10:59 +03:00
|
|
|
|| gallery.details.shown
|
2023-07-17 13:13:09 +03:00
|
|
|
|| gallery.down() },
|
|
|
|
|
Enter: function(){
|
2023-08-03 15:20:42 +03:00
|
|
|
gallery.details.shown ?
|
|
|
|
|
gallery.details.hide()
|
|
|
|
|
: gallery.lightbox.toggle() },
|
2023-07-28 14:47:24 +03:00
|
|
|
Escape: function(evt){
|
2023-08-03 14:10:59 +03:00
|
|
|
gallery.details.shown ?
|
|
|
|
|
gallery.details.hide()
|
|
|
|
|
: gallery.lightbox.shown ?
|
2023-07-23 06:46:14 +03:00
|
|
|
gallery.lightbox.hide()
|
|
|
|
|
// XXX should we remember which image was current and select
|
|
|
|
|
// it again when needed???
|
2023-07-28 23:19:39 +03:00
|
|
|
: gallery.unmark_current ?
|
2023-07-23 06:46:14 +03:00
|
|
|
(gallery.current = null)
|
|
|
|
|
: null },
|
2023-07-20 21:42:17 +03:00
|
|
|
// selection...
|
|
|
|
|
' ': function(evt){
|
|
|
|
|
gallery.current
|
|
|
|
|
&& evt.preventDefault()
|
2023-07-28 23:19:39 +03:00
|
|
|
gallery.toggleMark() },
|
2023-07-20 21:42:17 +03:00
|
|
|
// XXX use key codes...
|
|
|
|
|
'a': function(evt){
|
|
|
|
|
evt.preventDefault()
|
|
|
|
|
if(evt.ctrlKey){
|
|
|
|
|
gallery.selectAll() } },
|
|
|
|
|
'd': function(evt){
|
|
|
|
|
evt.preventDefault()
|
|
|
|
|
if(evt.ctrlKey){
|
2023-07-28 23:19:39 +03:00
|
|
|
gallery.unmarkAll() } },
|
2023-07-20 21:42:17 +03:00
|
|
|
'i': function(evt){
|
|
|
|
|
evt.preventDefault()
|
|
|
|
|
if(evt.ctrlKey){
|
2023-07-28 23:19:39 +03:00
|
|
|
gallery.markInverse() } },
|
2023-07-17 13:13:09 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
var Gallery = {
|
2023-07-21 14:40:30 +03:00
|
|
|
|
2023-07-24 21:04:42 +03:00
|
|
|
// Options...
|
2023-07-22 15:33:55 +03:00
|
|
|
//
|
2023-07-28 23:19:39 +03:00
|
|
|
unmark_current: true,
|
2023-07-24 21:04:42 +03:00
|
|
|
|
|
|
|
|
// 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.
|
2023-07-28 23:19:39 +03:00
|
|
|
// XXX might be a good idea to auto-disable this when paging is required.
|
2023-07-24 21:04:42 +03:00
|
|
|
loop_images: false,
|
|
|
|
|
|
2023-07-28 23:19:39 +03:00
|
|
|
// 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.
|
2023-07-21 14:40:30 +03:00
|
|
|
allow_row_expansion: true,
|
2023-07-24 21:04:42 +03:00
|
|
|
|
2023-07-28 23:19:39 +03:00
|
|
|
// 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...
|
2023-07-22 00:11:20 +03:00
|
|
|
click_to_select: true,
|
2023-07-21 14:40:30 +03:00
|
|
|
|
2023-07-27 17:00:34 +03:00
|
|
|
exit_fullscreen_on_lightbox_close: true,
|
|
|
|
|
|
2023-07-25 21:57:18 +03:00
|
|
|
// 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',
|
|
|
|
|
|
2023-07-22 15:33:55 +03:00
|
|
|
|
2023-07-21 03:16:20 +03:00
|
|
|
code: `
|
|
|
|
|
<div class="gallery">
|
|
|
|
|
<!-- gallery: content -->
|
|
|
|
|
<div class="images">
|
|
|
|
|
</div>
|
|
|
|
|
<!-- lightbox -->
|
|
|
|
|
<div class="lightbox">
|
|
|
|
|
<img>
|
|
|
|
|
<div class="button close"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>`,
|
|
|
|
|
|
2023-07-17 13:13:09 +03:00
|
|
|
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 },
|
2023-08-03 14:10:59 +03:00
|
|
|
__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 },
|
2023-07-17 13:13:09 +03:00
|
|
|
|
2023-07-24 21:04:42 +03:00
|
|
|
__at_top_row: undefined,
|
|
|
|
|
__at_bottom_row: undefined,
|
2023-07-17 13:13:09 +03:00
|
|
|
get current(){
|
2023-07-20 21:42:17 +03:00
|
|
|
return this.dom.querySelector('.images img.current') },
|
2023-07-17 13:13:09 +03:00
|
|
|
set current(img){
|
2023-07-23 06:46:14 +03:00
|
|
|
// unset...
|
|
|
|
|
if(img == null){
|
|
|
|
|
this.current?.classList.remove('current')
|
|
|
|
|
return }
|
|
|
|
|
// set...
|
2023-07-20 21:42:17 +03:00
|
|
|
for(var i of this.dom.querySelectorAll('.images img.current')){
|
2023-07-17 13:13:09 +03:00
|
|
|
i.classList.remove('current') }
|
|
|
|
|
img.classList.add('current')
|
2023-07-22 15:33:55 +03:00
|
|
|
// XXX add offsets from borders...
|
2023-07-17 13:13:09 +03:00
|
|
|
img.scrollIntoView({
|
|
|
|
|
behavior: 'smooth',
|
|
|
|
|
block: 'nearest',
|
2023-07-24 21:04:42 +03:00
|
|
|
})
|
|
|
|
|
// helpers...
|
|
|
|
|
this.__at_top_row = !this.getRow('above')
|
|
|
|
|
this.__at_bottom_row = !this.getRow('below') },
|
2023-07-17 13:13:09 +03:00
|
|
|
|
2023-07-20 21:42:17 +03:00
|
|
|
// XXX should this be writable???
|
|
|
|
|
get images(){
|
|
|
|
|
return [...this.dom.querySelectorAll('.images img')] },
|
|
|
|
|
|
2023-07-20 22:06:51 +03:00
|
|
|
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\//, '/') }) },
|
|
|
|
|
//*/
|
|
|
|
|
|
2023-07-28 23:19:39 +03:00
|
|
|
get marked(){
|
|
|
|
|
return [...this.dom.querySelectorAll('.images img.marked')] },
|
|
|
|
|
|
2023-07-25 21:57:18 +03:00
|
|
|
get length(){
|
|
|
|
|
return this.images.length },
|
|
|
|
|
get index(){
|
|
|
|
|
return this.images.indexOf(this.current) },
|
|
|
|
|
|
2023-08-02 12:48:47 +03:00
|
|
|
getRow: function(img, direction='current', images){
|
2023-07-17 13:13:09 +03:00
|
|
|
if(['above', 'current', 'below'].includes(img)){
|
2023-08-02 12:48:47 +03:00
|
|
|
images = direction
|
2023-07-17 13:13:09 +03:00
|
|
|
direction = img
|
2023-07-20 21:42:17 +03:00
|
|
|
img = this.current }
|
2023-08-02 12:48:47 +03:00
|
|
|
if(direction instanceof Array){
|
|
|
|
|
images = direction
|
|
|
|
|
direction = null }
|
|
|
|
|
direction ??= 'current'
|
|
|
|
|
images ??= this.images
|
2023-07-17 13:13:09 +03:00
|
|
|
// get above/below row...
|
|
|
|
|
// XXX these are wastefull...
|
|
|
|
|
if(direction == 'above'){
|
|
|
|
|
var row = this.getRow(img)
|
2023-07-20 21:42:17 +03:00
|
|
|
var e = row[0].previousElementSibling
|
2023-07-17 13:13:09 +03:00
|
|
|
while(e && e.tagName != 'IMG'){
|
2023-07-20 21:42:17 +03:00
|
|
|
e = e.previousElementSibling }
|
2023-07-17 13:13:09 +03:00
|
|
|
return e ?
|
2023-07-24 21:04:42 +03:00
|
|
|
this.getRow(e)
|
|
|
|
|
: this.loop_images ?
|
2023-08-02 12:48:47 +03:00
|
|
|
this.getRow(images.at(-1))
|
2023-07-24 21:04:42 +03:00
|
|
|
: undefined
|
2023-07-17 13:13:09 +03:00
|
|
|
} else if(direction == 'below'){
|
2023-07-28 23:19:39 +03:00
|
|
|
// special case: nothing marked...
|
2023-07-20 21:42:17 +03:00
|
|
|
if(img == null){
|
|
|
|
|
return this.getRow() }
|
2023-07-17 13:13:09 +03:00
|
|
|
var row = this.getRow(img)
|
2023-07-20 21:42:17 +03:00
|
|
|
var e = row.at(-1).nextElementSibling
|
2023-07-17 13:13:09 +03:00
|
|
|
while(e && e.tagName != 'IMG'){
|
2023-07-20 21:42:17 +03:00
|
|
|
e = e.nextElementSibling }
|
2023-07-17 13:13:09 +03:00
|
|
|
return e ?
|
2023-07-24 21:04:42 +03:00
|
|
|
this.getRow(e)
|
|
|
|
|
: this.loop_images ?
|
2023-08-02 12:48:47 +03:00
|
|
|
this.getRow(images[0])
|
2023-07-24 21:04:42 +03:00
|
|
|
: undefined }
|
2023-07-17 13:13:09 +03:00
|
|
|
// get current row...
|
|
|
|
|
var cur = img
|
|
|
|
|
?? this.current
|
|
|
|
|
if(cur == null){
|
|
|
|
|
var scroll = getScrollParent(this.dom).scrollTop
|
2023-08-02 12:48:47 +03:00
|
|
|
var images = images
|
2023-07-17 13:13:09 +03:00
|
|
|
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)
|
2023-07-20 21:42:17 +03:00
|
|
|
e = e.nextElementSibling
|
2023-07-17 13:13:09 +03:00
|
|
|
while(e && e.tagName != 'IMG'){
|
2023-07-20 21:42:17 +03:00
|
|
|
e = e.nextElementSibling } }
|
2023-07-17 13:13:09 +03:00
|
|
|
e = cur
|
|
|
|
|
while(e && e.offsetTop == top){
|
|
|
|
|
e === cur
|
|
|
|
|
|| row.unshift(e)
|
2023-07-20 21:42:17 +03:00
|
|
|
e = e.previousElementSibling
|
2023-07-17 13:13:09 +03:00
|
|
|
while(e && e.tagName != 'IMG'){
|
2023-07-20 21:42:17 +03:00
|
|
|
e = e.previousElementSibling } }
|
2023-07-17 13:13:09 +03:00
|
|
|
return row },
|
2023-07-24 21:04:42 +03:00
|
|
|
// XXX add .loop_images support???
|
2023-08-02 12:48:47 +03:00
|
|
|
getImage: function(img, direction='current', images){
|
|
|
|
|
// .getImage(direction[, images])
|
2023-07-17 13:13:09 +03:00
|
|
|
if(['left', 'above', 'current', 'below', 'right'].includes(img)){
|
2023-08-02 12:48:47 +03:00
|
|
|
images = direction
|
2023-07-17 13:13:09 +03:00
|
|
|
direction = img
|
|
|
|
|
img = null }
|
2023-08-02 12:48:47 +03:00
|
|
|
// .getImage(img, images)
|
|
|
|
|
if(direction instanceof Array){
|
|
|
|
|
images = direction
|
|
|
|
|
direction = null }
|
|
|
|
|
direction ??= 'current'
|
|
|
|
|
images ??= this.images
|
2023-07-17 13:13:09 +03:00
|
|
|
// current...
|
|
|
|
|
if(direction == 'current'){
|
|
|
|
|
return img
|
|
|
|
|
?? this.current
|
2023-08-02 12:48:47 +03:00
|
|
|
?? this.getRow(img, images)[0]
|
2023-07-17 13:13:09 +03:00
|
|
|
// above/below...
|
2023-07-25 21:57:18 +03:00
|
|
|
// get image with closest center to target image center...
|
2023-07-17 13:13:09 +03:00
|
|
|
} else if(direction == 'above' || direction == 'below'){
|
2023-08-02 12:48:47 +03:00
|
|
|
var row = this.getRow(direction, images)
|
2023-07-24 21:04:42 +03:00
|
|
|
if(row == null){
|
|
|
|
|
return undefined }
|
2023-07-17 13:13:09 +03:00
|
|
|
var cur = this.current
|
|
|
|
|
?? row[0]
|
2023-07-25 21:57:18 +03:00
|
|
|
// image center point...
|
2023-07-17 13:13:09 +03:00
|
|
|
var c = cur.offsetLeft + cur.offsetWidth/2
|
|
|
|
|
var target
|
|
|
|
|
var min
|
|
|
|
|
for(var img of row){
|
2023-07-25 21:57:18 +03:00
|
|
|
// 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 }
|
2023-07-17 13:13:09 +03:00
|
|
|
var d = Math.abs(n - c)
|
|
|
|
|
min = min ?? d
|
|
|
|
|
if(d <= min){
|
|
|
|
|
min = d
|
|
|
|
|
target = img } }
|
|
|
|
|
// left/right...
|
|
|
|
|
} else {
|
2023-08-02 12:48:47 +03:00
|
|
|
var row = this.getRow(img, images)
|
2023-07-17 13:13:09 +03:00
|
|
|
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???
|
2023-08-02 12:48:47 +03:00
|
|
|
prev: function(images){
|
|
|
|
|
images ??= this.images
|
2023-07-17 13:13:09 +03:00
|
|
|
var i = this.current == null ?
|
|
|
|
|
images.length-1
|
|
|
|
|
: images.indexOf(this.current)-1
|
2023-07-24 21:04:42 +03:00
|
|
|
i = i >= 0 ?
|
|
|
|
|
i
|
|
|
|
|
: this.loop_images ?
|
|
|
|
|
images.length-1
|
|
|
|
|
: 0
|
2023-07-17 13:13:09 +03:00
|
|
|
this.current = images[i]
|
2023-08-03 14:10:59 +03:00
|
|
|
this.update()
|
2023-07-17 13:13:09 +03:00
|
|
|
return this },
|
2023-08-02 12:48:47 +03:00
|
|
|
next: function(images){
|
|
|
|
|
images ??= this.images
|
2023-07-17 13:13:09 +03:00
|
|
|
var i = this.current == null ?
|
|
|
|
|
0
|
|
|
|
|
: images.indexOf(this.current)+1
|
2023-07-24 21:04:42 +03:00
|
|
|
i = i < images.length ?
|
|
|
|
|
i
|
|
|
|
|
: this.loop_images ?
|
|
|
|
|
0
|
|
|
|
|
: images.length-1
|
2023-07-17 13:13:09 +03:00
|
|
|
this.current = images[i]
|
2023-08-03 14:10:59 +03:00
|
|
|
this.update()
|
2023-07-17 13:13:09 +03:00
|
|
|
return this },
|
|
|
|
|
|
|
|
|
|
// navigate images visually...
|
2023-08-03 09:52:15 +03:00
|
|
|
// XXX BUG: these seem not to work with passed list of images...
|
2023-08-02 12:48:47 +03:00
|
|
|
left: function(images){
|
2023-07-17 13:13:09 +03:00
|
|
|
var cur = this.current
|
2023-08-02 12:48:47 +03:00
|
|
|
var row = this.getRow(cur, images)
|
2023-07-17 13:13:09 +03:00
|
|
|
var i = row.indexOf(cur) - 1
|
|
|
|
|
this.current = row[i < 0 ?
|
2023-07-23 06:46:14 +03:00
|
|
|
row.length-1
|
2023-07-17 13:13:09 +03:00
|
|
|
: i]
|
|
|
|
|
return this },
|
2023-08-02 12:48:47 +03:00
|
|
|
right: function(images){
|
2023-07-17 13:13:09 +03:00
|
|
|
var cur = this.current
|
2023-08-02 12:48:47 +03:00
|
|
|
var row = this.getRow(cur, images)
|
2023-07-17 13:13:09 +03:00
|
|
|
var i = row.indexOf(cur) + 1
|
|
|
|
|
this.current = row[i >= row.length ?
|
2023-07-23 06:46:14 +03:00
|
|
|
0
|
2023-07-17 13:13:09 +03:00
|
|
|
: i]
|
|
|
|
|
return this },
|
2023-08-02 12:48:47 +03:00
|
|
|
up: function(images){
|
|
|
|
|
var img = this.getImage('above', images)
|
2023-07-24 21:04:42 +03:00
|
|
|
img
|
|
|
|
|
&& (this.current = img)
|
2023-07-17 13:13:09 +03:00
|
|
|
return this },
|
2023-08-02 12:48:47 +03:00
|
|
|
down: function(images){
|
|
|
|
|
var img = this.getImage('below', images)
|
2023-07-24 21:04:42 +03:00
|
|
|
img
|
|
|
|
|
&& (this.current = img)
|
2023-07-17 13:13:09 +03:00
|
|
|
return this },
|
|
|
|
|
|
2023-07-20 21:42:17 +03:00
|
|
|
// selection...
|
2023-07-28 23:19:39 +03:00
|
|
|
//
|
2023-07-20 22:06:51 +03:00
|
|
|
// NOTE: this is here because we can't use :before / :after directly
|
|
|
|
|
// on the img tag...
|
2023-07-22 15:33:55 +03:00
|
|
|
// XXX make this generic and use a .marks list...
|
2023-07-20 21:42:17 +03:00
|
|
|
updateMarkers: function(){
|
2023-07-22 15:33:55 +03:00
|
|
|
var that = this
|
2023-07-20 21:42:17 +03:00
|
|
|
// select...
|
2023-07-28 23:19:39 +03:00
|
|
|
for(var img of this.dom.querySelectorAll('.images img.marked')){
|
2023-07-20 21:42:17 +03:00
|
|
|
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')
|
2023-07-28 23:19:39 +03:00
|
|
|
mark.classList.add('marked', 'mark')
|
2023-07-22 15:33:55 +03:00
|
|
|
mark.addEventListener('click', function(evt){
|
|
|
|
|
evt.stopPropagation()
|
2023-07-28 23:19:39 +03:00
|
|
|
that.unmark(mark) })
|
2023-07-20 21:42:17 +03:00
|
|
|
img.after(mark) } }
|
2023-07-28 23:19:39 +03:00
|
|
|
// clear unmarked...
|
|
|
|
|
for(var mark of this.dom.querySelectorAll('.images img:not(.marked)+.mark')){
|
2023-07-20 21:42:17 +03:00
|
|
|
mark.remove() }
|
2023-07-21 14:40:30 +03:00
|
|
|
// update lightbox...
|
2023-07-22 15:33:55 +03:00
|
|
|
this.lightbox.shown
|
|
|
|
|
&& this.lightbox.update()
|
2023-07-20 21:42:17 +03:00
|
|
|
return this },
|
2023-07-28 23:19:39 +03:00
|
|
|
mark: function(img){
|
2023-07-22 15:33:55 +03:00
|
|
|
img = img ?? this.current
|
2023-07-28 23:19:39 +03:00
|
|
|
img?.classList.add('marked')
|
2023-07-20 21:42:17 +03:00
|
|
|
return this.updateMarkers() },
|
2023-07-28 23:19:39 +03:00
|
|
|
unmark: function(img){
|
2023-07-22 15:33:55 +03:00
|
|
|
img = img ?? this.current
|
2023-07-28 23:19:39 +03:00
|
|
|
img?.classList.remove('marked')
|
2023-07-20 21:42:17 +03:00
|
|
|
return this.updateMarkers() },
|
2023-07-28 23:19:39 +03:00
|
|
|
toggleMark: function(img){
|
2023-07-22 15:33:55 +03:00
|
|
|
img = img ?? this.current
|
2023-07-28 23:19:39 +03:00
|
|
|
img?.classList.toggle('marked')
|
2023-07-20 21:42:17 +03:00
|
|
|
this.updateMarkers()
|
|
|
|
|
return this },
|
|
|
|
|
selectAll: function(){
|
|
|
|
|
for(var img of this.images){
|
2023-07-28 23:19:39 +03:00
|
|
|
img.classList.add('marked') }
|
2023-07-20 21:42:17 +03:00
|
|
|
return this.updateMarkers() },
|
2023-07-28 23:19:39 +03:00
|
|
|
unmarkAll: function(){
|
2023-07-20 21:42:17 +03:00
|
|
|
for(var img of this.images){
|
2023-07-28 23:19:39 +03:00
|
|
|
img.classList.remove('marked') }
|
2023-07-20 21:42:17 +03:00
|
|
|
return this.updateMarkers() },
|
2023-07-28 23:19:39 +03:00
|
|
|
markInverse: function(){
|
2023-07-20 21:42:17 +03:00
|
|
|
for(var img of this.images){
|
2023-07-28 23:19:39 +03:00
|
|
|
img.classList.toggle('marked') }
|
2023-07-20 21:42:17 +03:00
|
|
|
return this.updateMarkers() },
|
|
|
|
|
|
2023-07-17 13:13:09 +03:00
|
|
|
show: function(){
|
2023-08-03 18:06:30 +03:00
|
|
|
return this.showLightbox() },
|
2023-08-03 14:10:59 +03:00
|
|
|
showLightbox: function(){
|
|
|
|
|
this.lightbox.show()
|
|
|
|
|
return this },
|
|
|
|
|
showDetails: function(){
|
|
|
|
|
this.details.show()
|
|
|
|
|
return this },
|
2023-07-17 13:13:09 +03:00
|
|
|
|
2023-08-07 17:16:24 +03:00
|
|
|
__update_grid_size_timeout: undefined,
|
|
|
|
|
__update_grid_size_running: false,
|
2023-08-03 14:10:59 +03:00
|
|
|
__update_grid_size: function(){
|
2023-08-07 17:16:24 +03:00
|
|
|
// 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
|
2023-07-20 22:06:51 +03:00
|
|
|
return this },
|
2023-08-03 14:10:59 +03:00
|
|
|
update: function(){
|
|
|
|
|
this.__update_grid_size()
|
2023-08-11 13:21:01 +03:00
|
|
|
// XXX should this update markers???
|
|
|
|
|
//this.updateMarkers()
|
2023-08-03 14:10:59 +03:00
|
|
|
this.lightbox.shown
|
|
|
|
|
&& this.lightbox.update()
|
|
|
|
|
this.details.shown
|
|
|
|
|
&& this.details.update()
|
|
|
|
|
return this },
|
2023-07-20 22:06:51 +03:00
|
|
|
|
2023-07-28 19:41:44 +03:00
|
|
|
// .load(<image>)
|
|
|
|
|
// .load(<images>)
|
|
|
|
|
// .load(<image>, <index>)
|
|
|
|
|
// .load(<images>, <index>)
|
|
|
|
|
//
|
|
|
|
|
// <images> ::=
|
|
|
|
|
// <image>
|
|
|
|
|
// | [ <image>, .. ]
|
|
|
|
|
// <image> ::=
|
|
|
|
|
// <url>
|
|
|
|
|
// | [ <url>, <caption>, .. ]
|
|
|
|
|
// | { url: <url>, caption: <caption>, .. }
|
|
|
|
|
//
|
2023-08-03 18:09:56 +03:00
|
|
|
// XXX BUG: for some reason this breaks the gallery:
|
|
|
|
|
// gallery.load(gallery.json('marked'))
|
2023-07-28 19:41:44 +03:00
|
|
|
// XXX do we handle previews here???
|
|
|
|
|
load: function(images, index=undefined){
|
2023-08-07 17:16:24 +03:00
|
|
|
var that = this
|
2023-07-28 19:41:44 +03:00
|
|
|
images = images instanceof Array ?
|
|
|
|
|
images
|
|
|
|
|
: [images]
|
|
|
|
|
// create images...
|
|
|
|
|
var elems = []
|
|
|
|
|
for(var data of images){
|
|
|
|
|
if(typeof(data) == 'string'){
|
2023-08-07 17:16:24 +03:00
|
|
|
var [url, data] = [data, {}]
|
2023-07-28 19:41:44 +03:00
|
|
|
} else if(data instanceof Array){
|
2023-08-07 17:16:24 +03:00
|
|
|
var [url, caption] = data
|
|
|
|
|
data = {}
|
|
|
|
|
caption
|
|
|
|
|
?? (data.caption = caption)
|
2023-07-28 19:41:44 +03:00
|
|
|
} else {
|
|
|
|
|
var {url, ...data} = data }
|
|
|
|
|
var elem = document.createElement('img')
|
2023-08-07 17:16:24 +03:00
|
|
|
elem.onload = function(){
|
|
|
|
|
that.update() }
|
2023-07-28 19:41:44 +03:00
|
|
|
elem.src = url
|
2023-07-28 21:23:00 +03:00
|
|
|
elem.setAttribute('draggable', 'true')
|
2023-07-28 19:41:44 +03:00
|
|
|
for(var [key, value] of Object.entries(data)){
|
|
|
|
|
value
|
2023-08-03 18:06:30 +03:00
|
|
|
// XXX is this a good way to destinguish classes and attrs???
|
|
|
|
|
&& (typeof(value) == 'boolean' ?
|
|
|
|
|
elem.classList.add(key)
|
|
|
|
|
: elem.setAttribute(key, value)) }
|
2023-07-28 19:41:44 +03:00
|
|
|
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
|
2023-08-03 18:06:30 +03:00
|
|
|
.updateMarkers()
|
2023-07-28 19:41:44 +03:00
|
|
|
.update() },
|
|
|
|
|
__image_attributes__: [
|
|
|
|
|
'caption',
|
2023-08-11 15:41:11 +03:00
|
|
|
'filename',
|
2023-07-28 19:41:44 +03:00
|
|
|
],
|
2023-08-03 18:06:30 +03:00
|
|
|
__image_classes__: [
|
|
|
|
|
// XXX should this be here or set as a root attribute???
|
|
|
|
|
'current',
|
|
|
|
|
'marked',
|
|
|
|
|
],
|
2023-08-11 15:41:11 +03:00
|
|
|
// XXX add option to include images as data urls...
|
2023-07-28 19:41:44 +03:00
|
|
|
// XXX do we handle previews here???
|
2023-07-28 23:19:39 +03:00
|
|
|
json: function(images=undefined){
|
2023-07-28 19:41:44 +03:00
|
|
|
var that = this
|
2023-07-28 23:19:39 +03:00
|
|
|
images =
|
|
|
|
|
(images == 'all' || images == '*') ?
|
|
|
|
|
this.images
|
|
|
|
|
: images == 'marked' ?
|
|
|
|
|
this.marked
|
|
|
|
|
: !images ?
|
|
|
|
|
this.images
|
|
|
|
|
: images
|
|
|
|
|
return images
|
2023-08-03 18:06:30 +03:00
|
|
|
.map(function(img, i){
|
2023-07-28 19:41:44 +03:00
|
|
|
var res = { url: img.src }
|
2023-08-03 18:06:30 +03:00
|
|
|
for(var key of that.__image_attributes__ ?? []){
|
2023-07-28 19:41:44 +03:00
|
|
|
var value = img.getAttribute(key)
|
|
|
|
|
value
|
|
|
|
|
&& (res[key] = value) }
|
2023-08-03 18:06:30 +03:00
|
|
|
for(var key of that.__image_classes__ ?? []){
|
|
|
|
|
img.classList.contains(key)
|
|
|
|
|
&& (res[key] = true) }
|
2023-07-28 19:41:44 +03:00
|
|
|
return res }) },
|
|
|
|
|
|
2023-08-11 15:41:11 +03:00
|
|
|
// XXX
|
|
|
|
|
zip: function(){
|
|
|
|
|
var json = this.json()
|
|
|
|
|
// XXX
|
|
|
|
|
},
|
|
|
|
|
|
2023-07-28 21:23:00 +03:00
|
|
|
remove: function(...images){
|
|
|
|
|
if(images.includes('all')){
|
|
|
|
|
return this.clear() }
|
|
|
|
|
// NOTE: we need to remove images from the end so as not to be
|
|
|
|
|
// affected by shifed indexes...
|
|
|
|
|
images
|
|
|
|
|
.sort()
|
|
|
|
|
.reverse()
|
|
|
|
|
for(var img of images){
|
|
|
|
|
typeof(img) == 'number' ?
|
|
|
|
|
this.images.at(img)?.remove()
|
|
|
|
|
: img instanceof Element ?
|
|
|
|
|
(this.images.contains(img)
|
|
|
|
|
&& img.remove())
|
|
|
|
|
: null }
|
2023-07-28 19:41:44 +03:00
|
|
|
return this
|
2023-07-28 21:23:00 +03:00
|
|
|
.update() },
|
2023-07-20 22:06:51 +03:00
|
|
|
clear: function(){
|
|
|
|
|
this.dom.querySelector('.images').innerHTML = ''
|
|
|
|
|
return this },
|
|
|
|
|
|
2023-07-17 13:13:09 +03:00
|
|
|
setup: function(dom){
|
|
|
|
|
var that = this
|
|
|
|
|
this.dom = dom
|
|
|
|
|
|
2023-07-21 03:16:20 +03:00
|
|
|
this.dom.querySelector('.images')
|
2023-07-20 22:06:51 +03:00
|
|
|
.addEventListener('click', function(evt){
|
2023-07-23 06:46:14 +03:00
|
|
|
evt.stopPropagation()
|
2023-07-20 22:06:51 +03:00
|
|
|
var target = evt.target
|
2023-07-21 03:16:20 +03:00
|
|
|
if(target.tagName == 'IMG'){
|
2023-07-22 15:33:55 +03:00
|
|
|
// shift+click: toggle selections...
|
|
|
|
|
if(evt.shiftKey){
|
2023-07-28 23:19:39 +03:00
|
|
|
that.toggleMark(target)
|
2023-07-22 00:11:20 +03:00
|
|
|
// first click selects, second shows...
|
2023-07-22 15:33:55 +03:00
|
|
|
} else if(that.click_to_select){
|
2023-07-22 00:11:20 +03:00
|
|
|
target.classList.contains('current') ?
|
|
|
|
|
that.show()
|
|
|
|
|
: (that.current = target)
|
|
|
|
|
// first click selects and shows...
|
|
|
|
|
} else {
|
|
|
|
|
that.current = target
|
2023-07-23 06:46:14 +03:00
|
|
|
that.show() }
|
2023-07-28 23:19:39 +03:00
|
|
|
} else if(that.unmark_current){
|
2023-07-23 06:46:14 +03:00
|
|
|
that.current = null } })
|
|
|
|
|
this.dom
|
|
|
|
|
.addEventListener('click', function(evt){
|
2023-07-28 23:19:39 +03:00
|
|
|
that.unmark_current
|
2023-07-23 06:46:14 +03:00
|
|
|
&& (that.current = null) })
|
2023-08-10 03:53:12 +03:00
|
|
|
// drag/drop: sort...
|
|
|
|
|
// XXX should we support multi-drag???
|
|
|
|
|
var dragged
|
|
|
|
|
this.dom
|
|
|
|
|
.addEventListener('dragstart', function(evt){
|
|
|
|
|
var i = that.images.indexOf(evt.target)
|
|
|
|
|
if(i >= 0){
|
2023-08-11 13:21:01 +03:00
|
|
|
dragged = evt.target
|
|
|
|
|
dragged.classList.add('dragging') } })
|
|
|
|
|
var skip_dragover = false
|
2023-08-11 15:47:42 +03:00
|
|
|
// XXX do we .__update_grid_size() live (current) or after the
|
|
|
|
|
// drag is over???
|
2023-08-10 03:53:12 +03:00
|
|
|
this.dom
|
|
|
|
|
.addEventListener('dragenter', function(evt){
|
2023-08-11 13:28:12 +03:00
|
|
|
// NOTE: this prevents jumping back and forth if moving
|
|
|
|
|
// image causes a resize that causes the image to
|
|
|
|
|
// move again...
|
2023-08-11 13:21:01 +03:00
|
|
|
if(skip_dragover){
|
|
|
|
|
return }
|
|
|
|
|
var target = evt.target
|
|
|
|
|
var i = that.images.indexOf(target)
|
2023-08-10 04:07:32 +03:00
|
|
|
if(dragged && i >= 0){
|
2023-08-11 13:21:01 +03:00
|
|
|
evt.offsetX < target.offsetWidth / 2 ?
|
2023-08-10 03:53:12 +03:00
|
|
|
target.after(dragged)
|
2023-08-10 03:56:03 +03:00
|
|
|
: target.before(dragged)
|
2023-08-11 13:21:01 +03:00
|
|
|
// skip a dragover event if triggered by (right
|
|
|
|
|
// after) resize...
|
|
|
|
|
skip_dragover = true
|
|
|
|
|
setTimeout(function(){
|
|
|
|
|
skip_dragover = false }, 20)
|
|
|
|
|
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:
|
2023-08-11 15:47:42 +03:00
|
|
|
// |[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)
|
2023-08-11 13:28:12 +03:00
|
|
|
// 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...
|
2023-08-11 13:21:01 +03:00
|
|
|
this.dom
|
|
|
|
|
.addEventListener('dragleave', function(evt){
|
|
|
|
|
if(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){
|
2023-08-11 13:28:12 +03:00
|
|
|
var prev = images[i-1]
|
|
|
|
|
// adjacent image is not on the same offsetTop (edge)
|
2023-08-11 13:21:01 +03:00
|
|
|
if(prev == null
|
|
|
|
|
|| prev.offsetTop != target.offsetTop){
|
|
|
|
|
target.before(dragged)
|
|
|
|
|
that.updateMarkers() }
|
|
|
|
|
// right edge...
|
|
|
|
|
} else {
|
2023-08-11 13:28:12 +03:00
|
|
|
var next = images[i+1]
|
|
|
|
|
// adjacent image is not on the same offsetTop (edge)
|
2023-08-11 13:21:01 +03:00
|
|
|
if(next == null
|
|
|
|
|
|| next.offsetTop != target.offsetTop){
|
|
|
|
|
target.after(dragged)
|
|
|
|
|
that.updateMarkers() } } } })
|
2023-07-28 14:47:24 +03:00
|
|
|
this.dom
|
|
|
|
|
.addEventListener('dragover', function(evt){
|
2023-08-07 17:16:24 +03:00
|
|
|
evt.preventDefault()
|
|
|
|
|
evt.stopPropagation()
|
2023-08-10 03:53:12 +03:00
|
|
|
evt.dataTransfer.dropEffect = 'copy' }, false)
|
|
|
|
|
// drag/drop: files...
|
|
|
|
|
// XXX handle serilized data...
|
2023-07-28 14:47:24 +03:00
|
|
|
this.dom
|
|
|
|
|
.addEventListener('drop', function(evt){
|
|
|
|
|
evt.preventDefault()
|
2023-08-07 17:16:24 +03:00
|
|
|
evt.stopPropagation()
|
|
|
|
|
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){
|
2023-08-10 03:53:12 +03:00
|
|
|
// XXX handle serilized data...
|
|
|
|
|
// XXX
|
|
|
|
|
|
|
|
|
|
// images...
|
|
|
|
|
if(file.type.match('image.*')){
|
2023-08-11 15:41:11 +03:00
|
|
|
// XXX TEST...
|
2023-08-10 03:53:12 +03:00
|
|
|
if(file.path){
|
2023-08-11 15:41:11 +03:00
|
|
|
return {
|
|
|
|
|
url: file.path,
|
|
|
|
|
filename: file.name,
|
|
|
|
|
}
|
2023-08-10 03:53:12 +03:00
|
|
|
} else {
|
|
|
|
|
return new Promise(function(resolve, reject){
|
|
|
|
|
var reader = new FileReader()
|
|
|
|
|
reader.onload = function(f){
|
2023-08-11 15:41:11 +03:00
|
|
|
resolve({
|
|
|
|
|
url: f.target.result,
|
|
|
|
|
filename: file.name,
|
|
|
|
|
// XXX any other metadata to include???
|
|
|
|
|
}) }
|
2023-08-10 04:07:32 +03:00
|
|
|
reader.readAsDataURL(file) }) } }
|
|
|
|
|
// other files...
|
|
|
|
|
return [] })
|
2023-08-07 17:16:24 +03:00
|
|
|
.flat())
|
|
|
|
|
.then(
|
|
|
|
|
function(images){
|
|
|
|
|
// no images...
|
|
|
|
|
if(images.length == 0){
|
|
|
|
|
return }
|
|
|
|
|
return that.load(images) },
|
|
|
|
|
function(err){
|
|
|
|
|
// XXX handle errors...
|
2023-08-10 04:07:32 +03:00
|
|
|
})
|
2023-08-11 13:21:01 +03:00
|
|
|
dragged
|
|
|
|
|
&& dragged.classList.remove('dragging')
|
2023-08-10 04:07:32 +03:00
|
|
|
// XXX if this is used in the promise, move to the point
|
|
|
|
|
// after we nned this...
|
|
|
|
|
dragged = undefined }, false)
|
2023-07-20 22:06:51 +03:00
|
|
|
|
2023-07-28 21:23:00 +03:00
|
|
|
// XXX
|
|
|
|
|
for(var img of this.images){
|
|
|
|
|
img.setAttribute('draggable', 'true') }
|
|
|
|
|
|
2023-07-20 22:06:51 +03:00
|
|
|
// handle resizing...
|
|
|
|
|
new ResizeObserver(
|
|
|
|
|
function(elems){
|
2023-08-03 14:10:59 +03:00
|
|
|
that.__update_grid_size() })
|
2023-07-20 22:06:51 +03:00
|
|
|
.observe(this.dom)
|
|
|
|
|
|
|
|
|
|
return this
|
2023-08-02 12:48:47 +03:00
|
|
|
.updateMarkers()
|
2023-07-20 22:06:51 +03:00
|
|
|
.update() },
|
2023-07-17 13:13:09 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
2023-08-03 15:20:42 +03:00
|
|
|
// This is a prototype for all modal views...
|
2023-07-17 13:13:09 +03:00
|
|
|
|
2023-08-03 14:10:59 +03:00
|
|
|
var Overlay = {
|
2023-07-17 13:13:09 +03:00
|
|
|
dom: undefined,
|
|
|
|
|
gallery: undefined,
|
2023-08-03 14:10:59 +03:00
|
|
|
cls: 'overlay',
|
2023-07-17 13:13:09 +03:00
|
|
|
|
|
|
|
|
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...
|
2023-08-03 14:10:59 +03:00
|
|
|
this.cache
|
|
|
|
|
&& this.cache_count != 0
|
2023-07-17 13:13:09 +03:00
|
|
|
&& this.cache() },
|
|
|
|
|
|
|
|
|
|
get shown(){
|
2023-08-03 14:10:59 +03:00
|
|
|
return this.gallery.dom.classList.contains(this.cls) },
|
|
|
|
|
show: function(){
|
2023-07-21 14:40:30 +03:00
|
|
|
this.update()
|
2023-08-03 14:10:59 +03:00
|
|
|
this.gallery.dom.classList.add(this.cls)
|
2023-07-17 13:13:09 +03:00
|
|
|
return this },
|
|
|
|
|
hide: function(){
|
2023-07-28 21:23:00 +03:00
|
|
|
this.gallery.exit_fullscreen_on_lightbox_close
|
|
|
|
|
&& document.fullscreenElement
|
|
|
|
|
&& document.exitFullscreen()
|
2023-08-03 14:10:59 +03:00
|
|
|
this.gallery.dom.classList.remove(this.cls)
|
2023-07-17 13:13:09 +03:00
|
|
|
return this },
|
|
|
|
|
toggle: function(){
|
|
|
|
|
return this.shown ?
|
|
|
|
|
this.hide()
|
|
|
|
|
: this.show() },
|
|
|
|
|
|
2023-08-03 14:10:59 +03:00
|
|
|
cache: null,
|
|
|
|
|
|
2023-07-21 14:40:30 +03:00
|
|
|
update: function(){
|
2023-08-03 14:10:59 +03:00
|
|
|
this.url =
|
|
|
|
|
(this.gallery.current
|
|
|
|
|
?? this.gallery.next().current
|
|
|
|
|
?? {}).src
|
2023-07-25 21:57:18 +03:00
|
|
|
var caption =
|
2023-07-21 14:40:30 +03:00
|
|
|
(this.gallery.current
|
|
|
|
|
?? this.gallery.next().current
|
|
|
|
|
?? {})
|
|
|
|
|
.getAttribute('caption')
|
2023-07-25 21:57:18 +03:00
|
|
|
?? ''
|
|
|
|
|
var index = (this.gallery.index+1) +'/'+ this.gallery.length
|
|
|
|
|
// set caption...
|
|
|
|
|
// XXX should these be separate elements???
|
|
|
|
|
this.dom.setAttribute('caption',
|
2023-08-03 14:10:59 +03:00
|
|
|
(this.caption_format ?? '${CAPTION}')
|
2023-07-25 21:57:18 +03:00
|
|
|
.replace(/\${CAPTION}/, caption)
|
|
|
|
|
.replace(/\${INDEX}/, index))
|
2023-07-21 14:40:30 +03:00
|
|
|
// set selection...
|
2023-07-28 23:19:39 +03:00
|
|
|
this.gallery.current.classList.contains('marked') ?
|
|
|
|
|
this.dom.classList.add('marked')
|
|
|
|
|
: this.dom.classList.remove('marked')
|
2023-07-21 14:40:30 +03:00
|
|
|
return this },
|
|
|
|
|
|
2023-08-03 14:10:59 +03:00
|
|
|
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() })
|
|
|
|
|
this.dom.querySelector('.fullscreen')
|
|
|
|
|
?.addEventListener('click', function(evt){
|
|
|
|
|
evt.stopPropagation()
|
|
|
|
|
document.fullscreenElement ?
|
|
|
|
|
document.exitFullscreen()
|
|
|
|
|
: that.dom.requestFullscreen() })
|
|
|
|
|
this.dom.querySelector('.info')
|
|
|
|
|
?.addEventListener('click', function(evt){
|
|
|
|
|
evt.stopPropagation()
|
|
|
|
|
that.gallery.showDetails() })
|
|
|
|
|
this.dom.querySelector('.prev')
|
|
|
|
|
?.addEventListener('click', function(evt){
|
|
|
|
|
evt.stopPropagation()
|
|
|
|
|
that.gallery.prev() })
|
|
|
|
|
this.dom.querySelector('.next')
|
|
|
|
|
?.addEventListener('click', function(evt){
|
|
|
|
|
evt.stopPropagation()
|
|
|
|
|
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() })
|
2023-07-17 13:13:09 +03:00
|
|
|
return this },
|
2023-08-03 14:10:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
var Lightbox = {
|
|
|
|
|
__proto__: Overlay,
|
|
|
|
|
|
|
|
|
|
cls: 'lightboxed',
|
|
|
|
|
|
|
|
|
|
caption_format: '${INDEX} ${CAPTION}',
|
|
|
|
|
|
|
|
|
|
navigation_deadzone: 100,
|
|
|
|
|
caption_hysteresis: 10,
|
|
|
|
|
cache_count: 1,
|
2023-07-17 13:13:09 +03:00
|
|
|
|
|
|
|
|
__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
|
2023-08-03 14:10:59 +03:00
|
|
|
|
|
|
|
|
Overlay.setup.call(this, ...arguments)
|
|
|
|
|
|
|
|
|
|
// click zones...
|
2023-07-17 13:13:09 +03:00
|
|
|
var deadzone = this.navigation_deadzone ?? 100
|
|
|
|
|
this.dom
|
|
|
|
|
.addEventListener('click', function(evt){
|
2023-07-23 06:46:14 +03:00
|
|
|
evt.stopPropagation()
|
2023-07-17 13:13:09 +03:00
|
|
|
// click left/right side of view...
|
|
|
|
|
// NOTE: this is vewport-relative...
|
|
|
|
|
evt.clientX < that.dom.offsetWidth / 2 - deadzone/2
|
2023-08-03 14:10:59 +03:00
|
|
|
&& that.gallery.prev()
|
2023-07-17 13:13:09 +03:00
|
|
|
evt.clientX > that.dom.offsetWidth / 2 + deadzone/2
|
2023-08-03 14:10:59 +03:00
|
|
|
&& that.gallery.next() })
|
|
|
|
|
// hover zones...
|
2023-07-17 13:13:09 +03:00
|
|
|
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 },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2023-08-03 14:10:59 +03:00
|
|
|
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
var Details = {
|
|
|
|
|
__proto__: Overlay,
|
|
|
|
|
|
|
|
|
|
cls: 'detailed',
|
|
|
|
|
|
|
|
|
|
// XXX
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-17 13:13:09 +03:00
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
var setupGallery = function(gallery){
|
|
|
|
|
return {__proto__: Gallery}
|
|
|
|
|
.setup(gallery) }
|
|
|
|
|
|
|
|
|
|
var setup = function(){
|
|
|
|
|
var galleries = document.body.querySelectorAll('.gallery')
|
|
|
|
|
for(var gallery of galleries){
|
2023-07-28 23:31:57 +03:00
|
|
|
// XXX this is wrong -- handle multiple galleries...
|
2023-07-17 13:13:09 +03:00
|
|
|
window.gallery = setupGallery(gallery) }
|
|
|
|
|
// keyboard...
|
|
|
|
|
document.addEventListener('keydown', function(evt){
|
|
|
|
|
var key = evt.key
|
|
|
|
|
if(key in keyboard){
|
2023-07-20 22:06:51 +03:00
|
|
|
keyboard[key](evt) } }) }
|
2023-07-17 13:13:09 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
// vim:set ts=4 sw=4 :
|