@@ -3,6 +3,267 @@
( function ( ) {
'use strict' ;
// ============================================================
// Lightbox Module
// ============================================================
var Lightbox = ( function ( ) {
var lb = null ;
var img = null ;
var cap = null ;
var items = [ ] ;
var index = 0 ;
// Zoom/pan state
var scale = 1 ;
var panX = 0 ;
var panY = 0 ;
var minScale = 1 ;
var maxScale = 5 ;
var isPanning = false ;
var panStartX = 0 ;
var panStartY = 0 ;
// History state
var pushedHistory = false ;
var closingFromPopstate = false ;
function ensureElement ( ) {
if ( lb ) return ;
lb = document . createElement ( 'div' ) ;
lb . className = 'luxtools-lightbox' ;
lb . setAttribute ( 'role' , 'dialog' ) ;
lb . setAttribute ( 'aria-modal' , 'true' ) ;
lb . setAttribute ( 'aria-hidden' , 'true' ) ;
lb . innerHTML =
'<div class="luxtools-lightbox-backdrop" data-luxtools-action="close"></div>' +
'<div class="luxtools-lightbox-stage">' +
'<button type="button" class="luxtools-lightbox-close" data-luxtools-action="close" aria-label="Close">× </button>' +
'<button type="button" class="luxtools-lightbox-zone luxtools-lightbox-zone-prev" data-luxtools-action="prev" aria-label="Previous"></button>' +
'<button type="button" class="luxtools-lightbox-zone luxtools-lightbox-zone-next" data-luxtools-action="next" aria-label="Next"></button>' +
'<div class="luxtools-lightbox-media">' +
'<img class="luxtools-lightbox-img" alt="" />' +
'</div>' +
'<div class="luxtools-lightbox-caption"></div>' +
'</div>' ;
document . body . appendChild ( lb ) ;
img = lb . querySelector ( 'img.luxtools-lightbox-img' ) ;
cap = lb . querySelector ( '.luxtools-lightbox-caption' ) ;
lb . addEventListener ( 'click' , onClick ) ;
}
function clampIndex ( n ) {
if ( n < 0 ) return items . length - 1 ;
if ( n >= items . length ) return 0 ;
return n ;
}
function applyTransform ( ) {
if ( scale <= 1 && panX === 0 && panY === 0 ) {
img . style . transform = '' ;
} else {
img . style . transform = 'scale(' + scale + ') translate(' + panX + 'px, ' + panY + 'px)' ;
}
img . style . cursor = scale > 1 ? 'grab' : '' ;
}
function resetZoom ( ) {
scale = 1 ;
panX = 0 ;
panY = 0 ;
applyTransform ( ) ;
}
function render ( ) {
var it = items [ index ] ;
img . src = it . full ;
img . setAttribute ( 'data-luxtools-index' , String ( index ) ) ;
if ( cap ) cap . textContent = ( it . name || '' ) . trim ( ) ;
resetZoom ( ) ;
}
function next ( ) {
index = clampIndex ( index + 1 ) ;
render ( ) ;
}
function prev ( ) {
index = clampIndex ( index - 1 ) ;
render ( ) ;
}
// Event handlers
function onWheel ( e ) {
e . preventDefault ( ) ;
var delta = e . deltaY > 0 ? - 0.15 : 0.15 ;
scale = Math . max ( minScale , Math . min ( maxScale , scale + delta ) ) ;
if ( scale <= 1 ) { panX = 0 ; panY = 0 ; }
applyTransform ( ) ;
}
function onDblClick ( e ) {
e . preventDefault ( ) ;
if ( scale > 1 ) {
scale = 1 ;
panX = 0 ;
panY = 0 ;
} else {
scale = 2.5 ;
}
applyTransform ( ) ;
}
function onMouseDown ( e ) {
if ( scale > 1 && e . button === 0 ) {
isPanning = true ;
panStartX = e . clientX - panX * scale ;
panStartY = e . clientY - panY * scale ;
img . style . cursor = 'grabbing' ;
e . preventDefault ( ) ;
}
}
function onMouseMove ( e ) {
if ( isPanning && scale > 1 ) {
panX = ( e . clientX - panStartX ) / scale ;
panY = ( e . clientY - panStartY ) / scale ;
applyTransform ( ) ;
img . style . cursor = 'grabbing' ;
}
}
function onMouseUp ( ) {
isPanning = false ;
img . style . cursor = scale > 1 ? 'grab' : '' ;
}
function onKeyDown ( e ) {
if ( ! lb || ! lb . classList . contains ( 'is-open' ) ) return ;
var key = e . key || '' ;
if ( key === 'Escape' ) {
e . preventDefault ( ) ;
close ( ) ;
} else if ( key === 'ArrowRight' ) {
e . preventDefault ( ) ;
next ( ) ;
} else if ( key === 'ArrowLeft' ) {
e . preventDefault ( ) ;
prev ( ) ;
}
}
function onPopState ( ) {
if ( ! lb || ! lb . classList . contains ( 'is-open' ) ) return ;
closingFromPopstate = true ;
try { close ( ) ; } finally { closingFromPopstate = false ; }
}
function onClick ( e ) {
var t = e . target ;
if ( ! t || ! t . getAttribute ) return ;
var action = t . getAttribute ( 'data-luxtools-action' ) || '' ;
if ( action === 'close' ) { e . preventDefault ( ) ; close ( ) ; return ; }
if ( action === 'next' ) { e . preventDefault ( ) ; next ( ) ; return ; }
if ( action === 'prev' ) { e . preventDefault ( ) ; prev ( ) ; return ; }
if ( t . closest && t . closest ( 'button.luxtools-lightbox-zone' ) ) return ;
if ( t . closest && t . closest ( 'img.luxtools-lightbox-img' ) ) return ;
e . preventDefault ( ) ;
close ( ) ;
}
function attachListeners ( ) {
document . addEventListener ( 'keydown' , onKeyDown , true ) ;
window . addEventListener ( 'popstate' , onPopState , true ) ;
img . addEventListener ( 'wheel' , onWheel , { passive : false } ) ;
img . addEventListener ( 'dblclick' , onDblClick ) ;
img . addEventListener ( 'mousedown' , onMouseDown ) ;
document . addEventListener ( 'mousemove' , onMouseMove ) ;
document . addEventListener ( 'mouseup' , onMouseUp ) ;
}
function detachListeners ( ) {
document . removeEventListener ( 'keydown' , onKeyDown , true ) ;
window . removeEventListener ( 'popstate' , onPopState , true ) ;
img . removeEventListener ( 'wheel' , onWheel ) ;
img . removeEventListener ( 'dblclick' , onDblClick ) ;
img . removeEventListener ( 'mousedown' , onMouseDown ) ;
document . removeEventListener ( 'mousemove' , onMouseMove ) ;
document . removeEventListener ( 'mouseup' , onMouseUp ) ;
}
function open ( galleryEl , startEl ) {
var links = galleryEl . querySelectorAll ( 'a.luxtools-gallery-item[data-luxtools-full]' ) ;
items = [ ] ;
links . forEach ( function ( a ) {
var full = a . getAttribute ( 'data-luxtools-full' ) || a . getAttribute ( 'href' ) || '' ;
var name = a . getAttribute ( 'data-luxtools-name' ) || a . getAttribute ( 'title' ) || '' ;
if ( ! full ) return ;
items . push ( { el : a , full : full , name : name } ) ;
} ) ;
if ( ! items . length ) return ;
index = 0 ;
for ( var i = 0 ; i < items . length ; i ++ ) {
if ( items [ i ] . el === startEl ) { index = i ; break ; }
}
ensureElement ( ) ;
pushedHistory = false ;
closingFromPopstate = false ;
lb . classList . add ( 'is-open' ) ;
lb . setAttribute ( 'aria-hidden' , 'false' ) ;
try { document . documentElement . classList . add ( 'luxtools-noscroll' ) ; } catch ( e ) { }
try { document . body . style . overflow = 'hidden' ; } catch ( e ) { }
attachListeners ( ) ;
try {
if ( window . history && window . history . pushState ) {
window . history . pushState ( { luxtoolsLightbox : 1 } , '' , window . location . href ) ;
pushedHistory = true ;
}
} catch ( e ) { }
render ( ) ;
}
function close ( ) {
if ( ! lb ) return ;
lb . classList . remove ( 'is-open' ) ;
lb . setAttribute ( 'aria-hidden' , 'true' ) ;
try { document . documentElement . classList . remove ( 'luxtools-noscroll' ) ; } catch ( e ) { }
try { document . body . style . overflow = '' ; } catch ( e ) { }
img . src = '' ;
resetZoom ( ) ;
detachListeners ( ) ;
if ( pushedHistory && ! closingFromPopstate ) {
try {
if ( window . history && window . history . state && window . history . state . luxtoolsLightbox === 1 ) {
window . history . back ( ) ;
}
} catch ( e ) { }
}
items = [ ] ;
}
return {
open : open ,
close : close
} ;
} ) ( ) ;
// ============================================================
// Gallery Thumbnails
// ============================================================
function initGalleryThumbs ( ) {
var imgs = document . querySelectorAll ( 'div.luxtools-gallery img[data-thumb-src]' ) ;
if ( ! imgs || ! imgs . length ) return ;
@@ -43,160 +304,9 @@
}
}
function ensureLightbox ( ) {
var existing = document . querySelector ( '.luxtools-lightbox' ) ;
if ( existing ) return existing ;
var root = document . createElement ( 'div' ) ;
root . className = 'luxtools-lightbox' ;
root . setAttribute ( 'role' , 'dialog' ) ;
root . setAttribute ( 'aria-modal' , 'true' ) ;
root . setAttribute ( 'aria-hidden' , 'true' ) ;
root . innerHTML =
'<div class="luxtools-lightbox-backdrop" data-luxtools-action="close"></div>' +
'<div class="luxtools-lightbox-stage">' +
'<div class="luxtools-lightbox-media">' +
'<img class="luxtools-lightbox-img" alt="" />' +
'<button type="button" class="luxtools-lightbox-zone luxtools-lightbox-zone-prev" data-luxtools-action="prev" aria-label="Previous"></button>' +
'<button type="button" class="luxtools-lightbox-zone luxtools-lightbox-zone-next" data-luxtools-action="next" aria-label="Next"></button>' +
'</div>' +
'<div class="luxtools-lightbox-caption"></div>' +
'</div>' ;
document . body . appendChild ( root ) ;
return root ;
}
function getGalleryItems ( galleryEl ) {
var links = galleryEl . querySelectorAll ( 'a.luxtools-gallery-item[data-luxtools-full]' ) ;
var items = [ ] ;
links . forEach ( function ( a ) {
var full = a . getAttribute ( 'data-luxtools-full' ) || a . getAttribute ( 'href' ) || '' ;
var name = a . getAttribute ( 'data-luxtools-name' ) || a . getAttribute ( 'title' ) || '' ;
if ( ! full ) return ;
items . push ( { el : a , full : full , name : name } ) ;
} ) ;
return items ;
}
function openLightboxFor ( galleryEl , startEl ) {
var items = getGalleryItems ( galleryEl ) ;
if ( ! items . length ) return ;
var index = 0 ;
for ( var i = 0 ; i < items . length ; i ++ ) {
if ( items [ i ] . el === startEl ) { index = i ; break ; }
}
var lb = ensureLightbox ( ) ;
var img = lb . querySelector ( 'img.luxtools-lightbox-img' ) ;
var cap = lb . querySelector ( '.luxtools-lightbox-caption' ) ;
var pushedHistory = false ;
var closingFromPopstate = false ;
function clamp ( n ) {
if ( n < 0 ) return items . length - 1 ;
if ( n >= items . length ) return 0 ;
return n ;
}
function render ( ) {
var it = items [ index ] ;
img . src = it . full ;
img . setAttribute ( 'data-luxtools-index' , String ( index ) ) ;
cap . textContent = ( it . name || '' ) . trim ( ) ;
}
function close ( ) {
lb . classList . remove ( 'is-open' ) ;
lb . setAttribute ( 'aria-hidden' , 'true' ) ;
try { document . documentElement . classList . remove ( 'luxtools-noscroll' ) ; } catch ( e ) { }
try { document . body . style . overflow = '' ; } catch ( e ) { }
// Clear src to stop downloads on close.
img . src = '' ;
document . removeEventListener ( 'keydown' , onKeyDown , true ) ;
window . removeEventListener ( 'popstate' , onPopState , true ) ;
// If we opened by pushing a state, pop it when closing via UI.
if ( pushedHistory && ! closingFromPopstate ) {
try {
if ( window . history && window . history . state && window . history . state . luxtoolsLightbox === 1 ) {
window . history . back ( ) ;
}
} catch ( e ) {
// ignore
}
}
}
function next ( ) {
index = clamp ( index + 1 ) ;
render ( ) ;
}
function prev ( ) {
index = clamp ( index - 1 ) ;
render ( ) ;
}
function onKeyDown ( e ) {
if ( ! lb . classList . contains ( 'is-open' ) ) return ;
var key = e . key || '' ;
if ( key === 'Escape' ) {
e . preventDefault ( ) ;
close ( ) ;
} else if ( key === 'ArrowRight' ) {
e . preventDefault ( ) ;
next ( ) ;
} else if ( key === 'ArrowLeft' ) {
e . preventDefault ( ) ;
prev ( ) ;
}
}
function onPopState ( ) {
if ( ! lb . classList . contains ( 'is-open' ) ) return ;
closingFromPopstate = true ;
try { close ( ) ; } finally { closingFromPopstate = false ; }
}
lb . onclick = function ( e ) {
var t = e . target ;
if ( ! t || ! t . getAttribute ) return ;
var action = t . getAttribute ( 'data-luxtools-action' ) || '' ;
if ( action === 'close' ) { e . preventDefault ( ) ; close ( ) ; return ; }
if ( action === 'next' ) { e . preventDefault ( ) ; next ( ) ; return ; }
if ( action === 'prev' ) { e . preventDefault ( ) ; prev ( ) ; return ; }
// Click outside the image closes (but don't interfere with controls).
if ( t . closest && t . closest ( 'button.luxtools-lightbox-zone' ) ) return ;
if ( t . closest && t . closest ( 'img.luxtools-lightbox-img' ) ) return ;
e . preventDefault ( ) ;
close ( ) ;
} ;
// Open
lb . classList . add ( 'is-open' ) ;
lb . setAttribute ( 'aria-hidden' , 'false' ) ;
try { document . documentElement . classList . add ( 'luxtools-noscroll' ) ; } catch ( e ) { }
try { document . body . style . overflow = 'hidden' ; } catch ( e ) { }
document . addEventListener ( 'keydown' , onKeyDown , true ) ;
window . addEventListener ( 'popstate' , onPopState , true ) ;
// Allow closing via browser back button.
try {
if ( window . history && window . history . pushState ) {
window . history . pushState ( { luxtoolsLightbox : 1 } , '' , window . location . href ) ;
pushedHistory = true ;
}
} catch ( e ) {
// ignore
}
render ( ) ;
}
// ============================================================
// Open Service (file:// links)
// ============================================================
function getServiceUrl ( el ) {
var url = el . getAttribute ( 'data-service-url' ) || '' ;
url = ( url || '' ) . trim ( ) ;
@@ -443,6 +553,9 @@
} , true ) ;
}
// ============================================================
// Click Handlers
// ============================================================
function findOpenElement ( target ) {
var el = target ;
while ( el && el !== document ) {
@@ -468,7 +581,7 @@
var gallery = galleryItem . closest ? galleryItem . closest ( 'div.luxtools-gallery[data-luxtools-gallery="1"]' ) : null ;
if ( gallery ) {
event . preventDefault ( ) ;
openLightboxFor ( gallery , galleryItem ) ;
Lightbox . open ( gallery , galleryItem ) ;
return ;
}
}
@@ -507,6 +620,9 @@
} ) ;
}
// ============================================================
// Initialize
// ============================================================
document . addEventListener ( 'click' , onClick , false ) ;
document . addEventListener ( 'DOMContentLoaded' , initGalleryThumbs , false ) ;
document . addEventListener ( 'DOMContentLoaded' , initScratchpads , false ) ;