File: trnpix/_viewable/_thumbspage-thumbsonly/2003 Dublin 2.JPG.html
<!DOCTYPE HTML> <html> <head> <!-- Generated 2025-06-20 @11:17:13, by thumbspage 3.0: learning-python.com/thumbspage.html --> <!-- Be Unicode and mobile friendly (and in the first 1k bytes here) --> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Context: thumbspage.py generates viewer pages from this template file - a combination of HTML, CSS, and JavaScript. Python expands this for each image, replacing all its 'percent(uppercase)s' keys. Edit with _care_ - some of this code is both subtle and fragile. This file's Unicode encoding defaults to UTF-8 in user_configs.py. Recent noteworthy mods here: [3.0] Temp messages for raw view, gallery navigation wraparounds [3.0] Swipes for touchpads and mousewheels (not just touchscreens) [3.0] Keypress shortcuts for viewer-page actions (if keyboard) [3.0] Force Notes to stay open on return until explicitly closed [3.0] Dark-mode theme, provisional and somewhat experimental [2.3] Note button+popup, up-swipe=Note, popup colors and opacity [2.3] Drop hrefs in toolbar, more tooltips, tooltips on by default [2.2] Add swipe gestures for touch screens, and optional tooltips [2.1] Use underline font for Auto button when slideshow is on [2.1] Use underline for Full for parity (but fullscreen obvious) [2.0] Add slideshow via Auto toggle, one-page fullscreen via Full [2.0] Cut Raw (it's now image tap); iOS Chrome history bug fixed [2.0] Use custom dialog for 1.7's info popup (on filename tap) [1.7] Add generation date+version above, and info popup display ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> <!-- [3.0] before analytics replacement key so title already set --> <title>2003 Dublin 2</title> <!-- Plus analytics code, custom styles, etc. (replace me) --> <!-- Anonymous analytics to prioritize work, enabled in online resources only. Automatically inserted at publish time by insert-analytics.py. --> <!-- 1) Universal Analytics tag (custom): stops collecting data on Jul-1-2023 --> <SCRIPT> // Start async JS-file fetch, if not already cached (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); // Queue actions to run in order after async JS-file fetch finished ga('create', 'UA-52579036-1', 'auto'); // Create tracker object (and queue) ga('set', 'anonymizeIp', true); // Anonymize IP addr (&aip) [Jun-2019] ga('send', 'pageview'); // Send page-view event now </SCRIPT> <!-- 2) Google Analytics 4 tag: added to site Oct-2022 (okay to keep UA tag) --> <!-- Google tag (gtag.js) --> <script async src="https://www.googletagmanager.com/gtag/js?id=G-J8CTEZHX3L"></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-J8CTEZHX3L'); </script> <!-- End analytics insert --> <!-- ==================================================================================== CSS: style HTML elements by id, class, or element type ==================================================================================== --> <style type="text/css"> /* in head (or elsewhere per HTML 5.2), type optional */ /* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Context: this is CSS in HTML expanded and generated by Python. The all-upper names here are Python dict-key replacement targets. The JavaScript ahead uses both HTML attributes and CSS properties, forming a nest of cross-language coupling. This file is close to incomprehensible if embedded in the .py script itself. ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ /* For all elements in doc (unless redef, includes load note) */ * { font-family: Arial, Helvetica, sans-serif; background-color: #333; /* bg is entire background */ color: white; /* text, not border or no-JS note */ } /* [3.0] Mod top * rule now if dark mode is 'host' or 'always' (only) */ /* Augments template-autothemes.html insert and relies on substitutions: @media false-media-type <= always false @media all <= always true, same as empty @media (prefers-color-scheme: dark) <= per host setting */ @media (prefers-color-scheme: dark) { * { /* viewer pages only, unless redefined */ background-color: black; /* black, to avoid img clashes (unless black!) */ color: #e0e0e0; /* text: off-white or other light color */ } } /* Don't display scrollbars (else Chrome on Windows/Linux may, briefly) */ body { overflow: hidden; /* added late in 1.X, but no other display impact anywhere */ } /* Don't upscale text in landscape mode on iOS Safari */ @media screen and (max-device-width: 640px) { html { -webkit-text-size-adjust: 100%; /* webkit browsers (old chrome too) */ } } /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ /* Navigation toolbar and its buttons (bottom of page) */ /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ .navdiv { width: 100%; /* span page: else no scrolls */ position: fixed; /* anchor to page bottom */ bottom: 0; overflow-x: auto; /* auto-scroll on viewport overflow, iff min-width */ left: 4px; /* use more empty space on left [2.0] */ } .navtable { width: 100%; /* span page: else won't fill entire page */ border-collapse: collapse; table-layout: auto; /* 'fixed' less uniform? + fails on .optional [2.0] */ min-width: 150px; /* usable min: else no scrolls [2.0] */ } .navlink { display: block; /* buttons; nicer links through ad hoc css? */ text-decoration: none; /* no link colors or underlines */ text-align: center; /* how did this work without this before [2.3]? */ /*min-width: 3em;*/ /* this changes layout slightly for the worse */ } /* Add horizontal padding for large fonts in Android Firefox */ /* This also ensures space on right, despite new min-width [2.0] */ .navtable td { padding-right: 10px; /* for all <td> nested in a class=navtable */ } /* [2.0] Optional toolbar buttons: not if no JS, or disabled in configs */ .optional { display: none; /* initially HIDDEN (changed on load if JS + enabled) */ } /* [1.7] Filename+image cursor change on desktop, in lieu of hover effects */ .cursorpointer { cursor: pointer; } /* [1.7] But no cursor change if no JS, for no-op widgets: image, filename */ </style> <noscript> <style> .cursorpointer { cursor: auto; } </style> </noscript> <style> /* See also "Dead/olde/abandoned" code ahead for toolbar history */ /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ /* Message posted when JavaScript disabled (top of page) */ /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ .nojsnote { text-align: center; font-style: italic; margin-top: 0px; /* other: text-shadow */ color: red; /* allow user configs */ /*color: red;*/ /* no: may be BGCOLOR */ } /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ /* Filename-related styles (top of page) */ /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ .filename { text-align: center; margin-top: 4px; margin-bottom: 10px; } .overflowscroll { /* also auto-scroll via this style in <div> */ overflow-x: auto; /* used by JS warning too, but wraps at ' ' */ } /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ /* Image-related styles (middle of page) */ /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ img { max-width: 100%; /* for before onload resize: else Chrome starts.. */ max-height: 100%; /* fullsize, and botches sizes on Android later; */ display: block; /* allow centering sans <p>; [kill bottom space] */ /*vertical-align: text-bottom; /* [didn't work fully, but bottom space now moot] */ } #theimg { border: thin solid white; /* not 1px, else Chrome may not draw (see .py) */ margin-left: auto; /* center-align image, but horizontally only */ margin-right: auto; /* set bdcolor=bgcolor to hide image border */ } /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ /* Dead/olde/abandoned navigation toolbar code (delete me soon) */ /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ /* Switch toolbar button's font effects on mouseover as a visual indicator */ /* [1.7] PUNT: drop hover, because it's broken on mobile browsers, and may still appear in mobile landscape mode's larger screens; but force cursor=pointer for an indicator on desktops; see UserGuide.html#1.7. */ /*--DEFUNCT .navlink:hover { -- [1.7] punt; mobile browsers get stuck 'on' text-decoration: underline; -- was: desktop browsers only (mobiles may botch) font-style: italic; } .navlink:active { font-style: italic; } DEFUNCT--*/ /* Smaller mobile devices: scale up toolbar links and no hover */ /* [1.7] PUNT: abandon former toolbar button scale up here, because it allows less space for images, and can lead to button text running together on small screens when font size is set very high by users. */ /*--DEFUNCT @media screen and (max-device-width: 640px) { .navtable { height: 1.25em; -- [1.7] punt on scale up for touch (per above) } -- was: for 0..640 pixel screens only .navlink { font-size: 1.25em; -- [1.7] punt on scale up for touch (per above) } .navlink:hover { -- [1.7] punt: abandon hover altogether (per earlier) text-decoration: none; -- else underline/italic may get stuck on font-style: normal; } } DEFUNCT--*/ </style> <!-- Iff JavaScript not enabled: use former 1.5 CSS scaling --> <noscript> <style> #theimg { width: 100%; height: auto; } </style> </noscript> <!-- There's more CSS ahead (info+note popups, etc.): search for "<style" --> <!-- ==================================================================================== JavaScript: dynamically scale HTML image element per DOM view size (etc.) ==================================================================================== --> <script type="text/javascript"> /* ok anywhere in page, type optional */ /* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Context: thumbspage is now a four-language "stew" - Python code generates HTML with embedded CSS for styling and the JavaScript here for dynamic image scaling - the last two of which regularly vary per browser. Web coding has become an interoperability nightmare. Getting the comments right alone is brutal; handling the many browser-specific quirks seems the stuff of madness... None of the following code is used if JavaScript is disabled. ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // TOUCH SWIPES //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* ---------------------------------------------------------------------- [2.2] Update: image-viewer pages now support left/right and up/down swipe gestures. Left/right swipes trigger previous- and next-image events (like the Prev/Next menu buttons) per config; down invokes the image-info dialog (like filename taps); and up opens raw image view (like image tap) where supported. Touch events are simple but glitchy (swipes to raw-view pages aren't supported on Chrome); work on both mobiles/smartphones and Windows PCs with touchscreens; and are no-ops on non-touch displays. Simple taps, browser gestures (e.g., pinch/spread zoom, back/forward swipe), and mice still work. Note that the physical _direction_ of swipes may depend on users' system settings: "down swipe" may be towards the bottom or top of display, and "left swipe" may move left or right. This is normal. [2.3] Up-swipe is now changed to be the same as a new Note toolbar button tap, if Notes are enabled via config and .note presence. Else, up-swipe is as in 2.2, including the browser-specific support levels. [3.0] Code here works for touchscreens on phones and PCs, but not touchpads and mousewheels on PCs. See padswipe() ahead for unique but similar code that supports swipes on touchpads and wheels too. ---------------------------------------------------------------------- */ // state of one-touch gestures we care about var touchStartX = null; var touchStartY = null; var touchInProgress = false; // [3.0] preclude wheel, to be safe var wheelInProgress = false; // [3.0] preclude touch, just in case function onTouchStart(event) { //-------------------------------------------------------------- // Finger down: save screen-position state in globals. // These are used in the move and end event handlers. // Assume a swipe is starting; move may abandon it. // event's touches[] and targetTouches[] have no pageX/Y. //-------------------------------------------------------------- logevent('touchstart'); if (wheelInProgress) return; // skip touch if wheel happening touchInProgress = true; // skip wheel if touch happening // use only first touch within image's area touchStartX = event.changedTouches[0].pageX; touchStartY = event.changedTouches[0].pageY; } function onTouchMove(event) { //-------------------------------------------------------------- // Finger move: see if it's still a one-touch event. // Some browsers may do pinch/zoom, back/forward, etc. // To allow pinch/spread gestures: clear swipe's state // if any 2+ touch events fire between start and end. //-------------------------------------------------------------- logevent('touchmove'); // don't do this: it prevents pinch/zoom // event.preventDefault(); if (event.touches.length > 1) { // abandon 2+ point gestures: pinch/spread touchStartX = null; touchStartY = null; } } // [3.0] Swipes factored out for reuse by touchpad/mousewheel ahead function doLeftSwipe() { // left swipe: nav per config if (false) { // prev image onNavClick_Prev('2003%20Dublin%201.jpg.html'); } else { // next image onNavClick_Next('2003%20LP2E%20Book.jpg.html'); } } function doRightSwipe() { // right swipe: nav per config if (false) { // next image onNavClick_Next('2003%20LP2E%20Book.jpg.html'); } else { // prev image onNavClick_Prev('2003%20Dublin%201.jpg.html'); } } function doDownSwipe() { // down swipe: image info popup imageInfoDialog(); } function doUpSwipe() { // up swipe: Note popup [2.3], or raw-image view [2.2] if (true) { showNote(); } else { if (runningInFirefox() || runningInSamsung() || runningOniOS() || false) { // where currently supported // [3.0] raw-view message here too, not just for image taps // window.location.href = "../2003%20Dublin%202.JPG"; show_message("Raw view: return to gallery with Back", "../2003%20Dublin%202.JPG", true); } else { // punt with notice alert('Use tap instead of up-swipe on this browser.'); } } } function onTouchEnd(event) { //-------------------------------------------------------------- // Finger up: classify the swipe, and trigger page action. // The LRSWIPESPERBUTTONS config mods the meaning of left/right // swipes: true=order of Prev/Next in toolbar; false='natural'. // // Horizontal|vertical direction is determined by the dimension // with the most change. This also must ignore motions below // a pixel-distance threshold, else simple taps may trigger swipe // actions (and even both in some cases). A 25-pixel threshold // seems adequate for both finger and more-precise stylus. // // Caveat: up-swipe is enabled only for browsers which support // it today. Chrome and its derivatives (Opera and Edge) do not. // In all browsers, [window.location.href = 'url'] works fine in // tap/click handlers. In the unsupporting browsers, though, the // same code in touch handlers like this frequently fails to add // the calling page to history, such that Back doesn't return to // the gallery's viewer page. Assigning to just window.location // or using .assign() or a setTimeout() callback doesn't help. // // Hence: rather than penalizing all browsers, up-swipe is coded // to work on Firefox (all platforms), iOS (all browsers), and // Samsung Browser on Android. Other touch browsers get alerts. // This will have to be revisited if Chrome ever fixes its bug; // UPSWIPEONALL can be used to test/enable up-swipe everywhere. // // [2.3] the preceding caveat is moot if Note popups are enabled // via config and .note presence - up-swipe is then the same as a // Note tap, not raw-image view (and image tap is still raw view). // Up-swipe to Note is in-page, and so supported on all browsers. //-------------------------------------------------------------- logevent('touchend'); touchInProgress = false; if (wheelInProgress) return; // ignore 2+ point gestures: pinch/spread if (touchStartX == null) { return; } // use only first touch within image's area var touchEndX = event.changedTouches[0].pageX; var touchEndY = event.changedTouches[0].pageY; // how far was the motion, in pixels? var diffX = touchStartX - touchEndX; // horizontal var diffY = touchStartY - touchEndY; // vertical // to see the distances // alert('hori, vert: ' + diffX + ', ' + diffY); // ignore if not far enough to be a swipe: tap var threshold = 25; if (Math.abs(diffX) < threshold && Math.abs(diffY) < threshold) { return; } // disable any other touch events: we're all in event.preventDefault(); // which changed most? if (Math.abs(diffX) > Math.abs(diffY)) { // horizontal swipe if (touchStartX > touchEndX) doLeftSwipe(); else doRightSwipe(); } else { // vertical swipe if (touchStartY < touchEndY) doDownSwipe(); else doUpSwipe(); } // moot if start or move always precede end, but... touchStartX = null; touchStartY = null; } document.addEventListener('DOMContentLoaded', function() { //-------------------------------------------------------------- // Register touch-event handlers on image, after image created. // Don't register on whole window: wrongly generates on buttons. // A load callback avoids moving this past the image build code. //-------------------------------------------------------------- // the image now exists var theimg = document.getElementById('theimg'); // this coding worked on mobile but not Windows; hmm // theimg.ontouchstart = onTouchStart; // this coding works universally theimg.addEventListener('touchstart', onTouchStart); theimg.addEventListener('touchmove', onTouchMove); theimg.addEventListener('touchend', onTouchEnd); }); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SLIDESHOW //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* ---------------------------------------------------------------------- [2.1] Updates: 1) The Auto button is now changed to underlined font as an 'on' indicator, whenever a slideshow is active. Else it can be hard to tell the state of the show, especially when first started, and after coming back from another page later. Italics alone seems too subtle (and makes other buttons shift noticeably on Android), bold changes the toolbar's layout jarringly, and colors are arbitrary via configs. 2) Prototyped but did not use half the normal delay (showDelayMS / 2) for the page on which Auto is first tapped to start a show. This page is already open and perhaps already viewed, and the full delay might make the show seem inoperable. However, the new Auto font change (1) provides ample indication that the show is on; and assuming that the first page has been viewed seems too presumptuous - it may have been jumped to from the index in order to start a show there immediately. ---------------------------------------------------------------------- */ // Global delay for timer loop, per-gallery configurable var showDelayMS = 5000; // Values have to be strings: ternary op inconvenient var toggleKey = 'thumbspageAuto'; var toggleOn = 'show-timer-on', toggleOff = 'show-timer-off'; // Cancel id, global to this page var showTimerID; // Auto button HTML element, for font change [2.1] var autoBtn; // {}=no-op, {alert(msg)}, or {console.log(msg)} var trace = function(msg) {}; // original: messages var logevent = function(msg) {}; // later: show events function hasSessionStorage() { /* ------------------------------------------------------------------ Return true iff HTML5 session storage is present and enabled. Very old browsers (e.g., IE7-) don't support the API, and some browsers may allow users to disable session storage manually. This seems overkill, but allows for browser-specific disables. [3.0] Make test key less generic to avoid name-clash potential. ------------------------------------------------------------------ */ try { sessionStorage.setItem('thumbspageTestKey', 'testval'); return (sessionStorage.getItem('thumbspageTestKey') == 'testval'); } catch (err) { return false; } } /* ------------------------------------------------------------------ Factor out common start/stop code, shared by button-click handler (in two state-retention flavors), and initial page-show handler. Global autoBtn has been fetched by autoContinue() post Auto build. Update: don't use italics - it causes buttons to shift noticeably on Android, and underline is enough (and looks more like a link). ------------------------------------------------------------------ */ function startSlideShow(delay) { //autoBtn.style['font-style'] = "italic"; autoBtn.style['text-decoration'] = "underline"; showTimerID = setTimeout(function() {onNavClick_Next('2003%20LP2E%20Book.jpg.html');}, delay); } function stopSlideShow() { //autoBtn.style['font-style'] = "normal"; autoBtn.style['text-decoration'] = "none"; clearTimeout(showTimerID); } function onAutoClick() { /* ------------------------------------------------------------------ [2.0] On toolbar Auto button clicks: toggle per-tab, cross-page slideshow state, and schedule or clear timer event to flip image. This is essentially automatic Next clicks, run on timer events. The cross-page toggle's state is per-tab, and used by both the current and following viewer pages. The slideshow persists until cancelled by a toggle or the gallery is exited, and is restarted on gallery returns (in most contexts). To loop: exiting a viewer page stops the timer automatically, but new pages reschedule the timer on load if the toggle is on. Subject to the vagaries of browser back-forward caches, but it works on all browsers tested. Auto requires JavaScript. About cross-page state in serverless pages: this uses window.name, because it's simple and cannot be disabled. Alternatives: client-side cookies, and HTML5 stores (window.sessionStorage.{getItem('key'), setItem('key', 'val')}). Coding note: unlike window.name, page-defined attributes (e.g., window.autoToggle) do not persist across pages: Auto switches pages once, but the attribute is 'undefined' in the next page. UPDATE, Jul-18-20: this now uses HTML5 session storage, not window.name. Else, state fails in Safari (only) when viewing galleries locally/offline (only). The new scheme works the same as window.name, which is still used as a fallback where session storage is unsupported or disabled. More: UserGuide.html#_20C. Caveat: with this fix, local-gallery slideshows work on Safari, but stop (and rarely even crash) when returning with Back from another page in some contexts (e.g., from remote); see ahead. [2.1] Change Auto button font to underline/normal for slideshow on/off; this happens in the factored utility functions above. [2.1] Do _not_ use half the normal delay for the page on which Auto is first clicked to start the show; this is too presumptuous. ------------------------------------------------------------------ */ if (hasSessionStorage()) { // use HTML5 cross-page session storage where available (newer, safari ok) trace('onAutoClick: using session storage'); // if set and on: clear flag, cancel next timer event if (sessionStorage.getItem(toggleKey) == toggleOn) { sessionStorage.setItem(toggleKey, toggleOff); stopSlideShow(); } // if off or not set: start loop (rescheduled by next page) else { sessionStorage.setItem(toggleKey, toggleOn); startSlideShow(showDelayMS); } } else { // use cross-page window.name as a fallback (original, Safari fail) trace('onAutoClick: using window.name'); // if set and on: clear flag, cancel next timer event if (window.name == toggleOn) { window.name = toggleOff; stopSlideShow(); } // if off or not set: start loop (rescheduled by next page) else if (window.name == toggleOff || window.name == '') { window.name = toggleOn; startSlideShow(showDelayMS); } } } function autoContinue(event) { /* ------------------------------------------------------------------ On page load: reset timer if show on, but not if off or not set (any 'falsy'). Run on first open, but also any navigation entry, and might be run on back-forward cache restores (Safari issue?). [2.1] Change Auto button to underline font if slideshow is active. [2.1] The next page (and thereafter) always uses the full delay. [2.1] Now called just after Auto built so font change unnoticeable. ------------------------------------------------------------------ */ var reschedule; trace('autoContinue'); // assume it's built by now autoBtn = document.getElementById("autoBtn"); // global // on Safari Back stop, deferred: 'null::false' persisted = event ? event.persisted : 'none'; trace(sessionStorage.getItem(toggleKey) +':'+ window.name +':'+ persisted); if (hasSessionStorage()) { reschedule = (sessionStorage.getItem(toggleKey) == toggleOn); // HTML5 storage } else { reschedule = (window.name == toggleOn); // else fallback } if (reschedule) { trace('reschedule'); startSlideShow(showDelayMS); } else { trace('NOT rescheduled'); // off, or Safari Back stop } } /* ------------------------------------------------------------------------------------ [2.1] The call to autoContinue() has been moved to end of this file, so it occurs immediately after the Auto button is built. It cannot be called here because the button must exist for its font to be changed, and the pageshow event-handler code below made the button's font change noticeable on some browsers/platforms (a timer event is another option, but would similarly flash and is heuristic). [2.0] Safari (only) work-around attempt: run timer reschedule code in a pageshow load-time event, not immediately. This was harmless in other browsers; for Safari, it tried to both restore the show on a next page or Back return, and avoid Safari Back-failure popup messages: "... Operation not permitted" (NSPOSIXErrorDomain:1)". DID NOT HELP: a Safari glitch changes the Auto toggle to null in HTML5 session state after a Back from remote; see UserGuide.html#moresafarijunk for the whole story. ------------------------------------------------------------------------------------ */ /*--DEFUNCT try { // callback trace('run timer reschedule deferred'); window.addEventListener('pageshow', autoContinue); // when widgets built } catch (err) { // immediate trace('run timer reschedule immediately'); autoContinue(null); // run now in older browsers? (alt: window.onpageshow) } DEFUNCT--*/ // original: font-change fails here, because autoBtn is still null here // autoContinue(null); // run on page load immediately everywhere // now called near end of file as soon as Auto built, so font change not visible // document.addEventListener('DOMContentLoaded'....) not used: need to run asap //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // FULLSCREEN //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* ---------------------------------------------------------------------- [2.1] Update: the Full button is changed to underline font as an 'on' indicator, whenever fullscreen has been activated by Full. This is done only for parity with Auto: fullscreen is obvious, and lasts for just one page as currently implemented (better == major redesign). Unlike Auto, all Full font changes happen in an event handler, because a fullscreen request can be denied and raise an exception in an async 'promise,' and users may cancel Full without pressing Full (e.g., via an Escape key). On browsers that don't support this event (e.g., some Safari), Full's font will simply never morph. Users may also manually turn a different fullscreen mode on and off outside the page's control at any time with shortcut keys or similar, but the page might not receive an event for, and be unable to cancel, that fullscreen mode. ---------------------------------------------------------------------- */ /* ---------------------------------------------------------------------- FULLSCREEN CAVEAT JavaScript-initiated fullscreen mode is limited to a single page, and does nothing on some browsers - including Safari and Chrome on iOS. It's unclear whether this is useful as a blowup/zoom feature, or just a "stupid browser trick" in thumbspage galleries that wastes UI space. Hence, the Full toolbar button that invokes code here can be disabled on a per-gallery basis via a build setting in user_configs.py. Users may be better served by browsers that have manual fullscreen options, which persist across page changes and span Auto slideshows (e.g., try F11 on Windows, control-command-f on Mac OS, Hide Toolbar on iOS 13+, and Opera 63+ on Android). Manual fullscreen doesn't work everywhere, but it likely beats thumbspage's Full where it does. For more about this feature, see the User Guide's coverage: - UserGuide.html#fullscreenmanual (manual options) - UserGuide.html#fullscreen20 (2.0 release docs) - UserGuide.html#_dnG (initial feature proposal) Coding note: these alternatives were also tried (forced scrolls, and "Add to home screen" support), but as coded did not help; either more is required, or they are old and fully trounced hacks: - window.scrollTo(0,1); (this may require bogus space) - <meta name="mobile-web-app-capable" content="yes"> - <meta name="apple-mobile-web-app-capable" content="yes"> [2.2] Some mobile browsers now popup a temporary but annoying message every time JavaScript fullscreen is entered, just as many do on PCs. Which makes it even less useful; see Opera's landscape fullscreen. ---------------------------------------------------------------------- */ // // The fullscreen API has been, well, fluid... // https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API // Some cases below are untested; the standard calls are now widespread. // function openFullscreen() { var de = document.documentElement; // documentElement: <html> // request fullscreen if (de.requestFullscreen) { // "standard" (undefined: false) de.requestFullscreen(); } else if (de.webkitRequestFullscreen) { // some Chrome, Safari, Opera de.webkitRequestFullscreen(); } else if (de.mozRequestFullScreen) { // some Firefox de.mozRequestFullScreen(); } else if (de.msRequestFullscreen) { // some IE/Edge de.msRequestFullscreen(); } // if it worked: change font in fullscreen-change handler } function closeFullscreen() { // documentElement not required here if (document.exitFullscreen) { // "standard" (undefined: false) document.exitFullscreen(); } else if (document.webkitExitFullscreen) { // some Chrome, Safari, Opera document.webkitExitFullscreen(); } else if (document.mozCancelFullScreen) { // some Firefox document.mozCancelFullScreen(); } else if (document.msExitFullscreen) { // some IE/Edge document.msExitFullscreen(); } // if it worked: change font in fullscreen-change handler } function isFullscreen() { // any may be undefined: false in JS return document.fullscreenElement || // "standard" document.webkitFullscreenElement || // some Chrome, Safari, Opera document.mozFullScreenElement || // some Firefox document.msFullscreenElement; // some IE/Edge } function onFullClick() { /* ------------------------------------------------------------------ [2.0] On toolbar Full button taps: toggle one-page fullscreen on or off. Fullscreen persists until cancelled by a Full toggle, user action, or page exit, and it doesn't work in some browsers. This requires JavaScript (and perhaps a horizontal toolbar scroll on smaller displays). It doesn't need to use cross-page state, because the fullscreen API has state queries. This could track on/off state manually with HTML5 storage, but that might also require catching (nonportable?) callbacks on user-invoked changes. ------------------------------------------------------------------ */ if (!isFullscreen()) { openFullscreen(); } else { closeFullscreen(); } } function onFullscreenChange() { /* ------------------------------------------------------------------ [2.1] Change Full's font as a perhaps-redundant visual indicator, whenever fullscreen state is changed by the Full button itself, or the Full button's fullscreen is otherwise cancelled by a user. The fullBtn element is already built here, because we're in a user-event callback. Run by an event handler, because fullscreen requests are async and can fail with an uncaught exception, and a Full may be cancelled without pressing Full (e.g., an Escape key). This won't be invoked for user-initiated fullscreen mode (which weirdly differs from page/JavaScript-initiated fullscreen), but it is called when users cancel a Full without pressing Full. Update: don't use italics - it causes buttons to shift noticeably on Android, and underline is enough (and looks more like a link). ------------------------------------------------------------------ */ var fullBtn = document.getElementById("fullBtn"); if (isFullscreen()) { //fullBtn.style['font-style'] = "italic"; fullBtn.style['text-decoration'] = "underline"; } else { //fullBtn.style['font-style'] = "normal"; fullBtn.style['text-decoration'] = "none"; } } // // register for fullscreen on/off changes invoked by page // document.documentElement.onfullscreenchange = onFullscreenChange; /* ---------------------------------------------------------------------- Caveat: we can't force fullscreen for a next page here, even in a deferred pageshow event, because it must be requested only from a short-running, user-initiated event handler (per Chrome's console error message, the API can be initiated only by a user gesture). Hence, it lasts for one page only, where it works at all, and won't persist for Auto slideshows. It's useful anyhow as a zoom, but disable as desired, and use browser/platform-specific fullscreen if available. A "real" fullscreen would require morphing a single persistent page, and more thumbspage redesign than its use cases currently warrant, and might sacrifice linkable viewer-page URLs for individual images. For now, users can increase page size manually in most browsers. [2.2] JavaScript fullscreen also fails in touch events (see above). Which would be okay if all browsers had multi-page fullscreen modes. ---------------------------------------------------------------------- */ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // PLATFORM //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* These are all really bad ideas; why hasn't this been standardized? */ function runningOniOS() { /* ------------------------------------------------------------------ More heinous web hacks; MS says iPhone in IE11 too. Use a JS regexp on an unreliable but widely used string. A generally bad idea, but often the only option today. ------------------------------------------------------------------ */ // alert(navigator.userAgent); return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; // hack! } function runningInSafariOniOS() { /* ------------------------------------------------------------------ More heinous web hacks; Chrome says Safari on iOS too, and all iOS browsers must use the same Apple code base. Use a JS regexp on an unreliable but widely used string. These tests matter; why haven't they been standardized? Was unused; formerly employed to allow for more vertical cruft on landscape+iOS+Safari only (but failed: use 1.5): safariFudge = (runningOniOS() && runningInSafari() && viewportsize.width > viewportsize.height) ? 50 : 0; [1.7] May-2020: Now used for Safari+iOS+landscape CSS-scaling legacy option (but UNUSED by default, per user_configs.py). This new, renamed coding works only for Safari on iOS, not desktop, but suffices... though its !others.test() coding means that any not in others are Safari (where others means {Chrome+iOS, Firefox+iOS, the new Edge, UC Browser}); yuck. [3.0] now also used for custom 'click' logic on iOS Safari (which also botches media-query header syntax: see the .py). ------------------------------------------------------------------ */ // this doesn't work today (! list incomplete?) // return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); // hack too! // close, but not quite right (see above) var vendor = navigator.vendor; var uagent = navigator.userAgent; return runningOniOS() && vendor && vendor.indexOf('Apple') > -1 && // Apple code base uagent && !/CriOS|FxiOS|Edg|UCBrowser/.test(uagent); // not other browsers } function runningInChromeOniOS() { /* ------------------------------------------------------------------ Ditto: used for history-destacking disable on Next/Prev onclick. [2.0] This is now UNUSED by default too: the bug has been fixed. ------------------------------------------------------------------ */ return navigator.userAgent.match('CriOS'); // also hack! } function runningInSafari() { /* ------------------------------------------------------------------ Same: add to work around Safari-only Back fails for Auto [2.0]. UNUSED: Back fail is a session-state bug in Safari (see ahead). Coding note: some browsers 'spoof' a Safari string in userAgent. ------------------------------------------------------------------ */ // this doesn't work today... // return window.safari !== undefined; // also also hack! var vendor = navigator.vendor; var uagent = navigator.userAgent; return vendor && vendor.indexOf('Apple') > -1 && // Apple code base uagent && !/CriOS|FxiOS|Edg|UCBrowser/.test(uagent); // not other browsers } function runningInFirefox() { // [2.2] added for Chrome post-touch Back bug return navigator.userAgent.toLowerCase().indexOf('firefox') != -1; } function runningInSamsung() { // [2.2] added for Chrome post-touch Back bug return navigator.userAgent.match(/SamsungBrowser/i); // i=caseinsens, notfound:null } // to test /* console.log(navigator.userAgent); console.log('runningInSafari: ' + (runningInSafari() ? 'yes' : 'no')); console.log('runningInSafariOniOS: ' + (runningInSafariOniOS() ? 'yes' : 'no')); */ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // NAVIGATION //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ function onNavClick(page) { /* ------------------------------------------------------------------ When opening another viewer page with the Prev or Next links, simply replace this page with the target, to avoid adding this soon-to-be-prior viewer page to the browser's history stack. The net effect is that viewer pages are never added to browser history, except for just one when navigating away from the gallery. This way, the browser's Back skips over the entire gallery navigation session, returning immediately to the gallery entry point: the index, or other direct viewer-page link source. Otherwise, N image views may require N Back clicks to return to pre-gallery pages (if there are any, and Index-page context is not enough). This was much worse than making Back skip all nav steps, given that viewer pages' Prev and Next do similar work. The Index link still goes to the gallery's index page regardless of the entry point - as it should, for direct viewer-page links. Moreover, after navigating away from the gallery, browser Back and Forward clicks return to the last viewer page viewed (only). Relies on the DOM location.replace() redirect-sans-history call, which might fail if classified as unsecure (crossing domains?). This could use <button>s, but keeping original <a>s appearance. history.go(-N) + counter stacks may work, but can't catch Back. onclick() here fires before target's onload(): see "Loading...". Former typo: "return True;" is exception (but href used anyhow). CAVEAT: this history destacking feature is disabled for Chrome on iOS (only), because location.replace() fails in that browser (only!). If used, Back clicks redisplay the _same_ page N times for N image views, which is worse than allowing pages to be stacked. thumbspage.py's version 1.6 notes have more details. No coding work-around could be found: go(-1), timer, paths, ... UPDATE [1.7]: this is still broken as of May 2020's iOS Chrome 75; if enabled, the .replace() here now silently stacks all pages. UPDATE [2.0]: since iOS Chrome's current behavior is no different than the work-around, the work-around bypass config switch now defaults to true; set it back to false if this ever becomes worse again. The new true will also pick up a future Chrome fix. UPDATE [2.0]: the iOS Chrome history bug has been fixed, as of June 2020's Chrome 83 (and perhaps earlier). The true setting of the config switch adopts the fix automatically, and thumbspage navigation pages are no longer stacked or retraced in iOS Chrome. Hence, this issue is closed; its code below is retained as info. UPDATE [2.2]: see also onTouchEnd() above for a related issue, plus https://developer.mozilla.org/docs/Web/API/Window/location. ------------------------------------------------------------------ */ // absolute URL not required, and URL-encoded filenames okay // var loc = window.location; // var url = loc.protocol + '//' + loc.host + ... + page; // no longer used: browser-bug work-around, till fixed? if (runningInChromeOniOS() && ! true) { return true; // use URL in <a> (same as no-JS case) } // all other browsers try { window.location.replace(page); // goto this, don't add page to history } catch(err) { window.location.href = page; // work on errors, add to history (=assign()) } return false; // do not goto <a> href link (if one coded) } function onNavClick_Prev(page) { // [3.0] announce end of gallery? if (false) show_message('Wrapping around to last image', '2003%20Dublin%201.jpg.html', false); else return onNavClick('2003%20Dublin%201.jpg.html'); } function onNavClick_Next(page) { // [3.0] announce end of gallery? if (false) show_message('Wrapping around to first image', '2003%20LP2E%20Book.jpg.html', false); else return onNavClick('2003%20LP2E%20Book.jpg.html'); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // INFO POPUP //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ function imageInfoDialog() { /* ------------------------------------------------------------------ [1.7] Feb-2020: popup an image-info dialog when the image's filename at the top of the viewer page is clicked/tapped. This initially used a simple alert() call; it could instead build new HTML element objects, but that seems overkill in this context. The 'Image size' comes from the static .py (via Pillow); it's the same as the JS DOM's theimg.natural{Width, Height}. Display size alone is from JS DOM, and reflects the size requested in the last resizeImage() callback (extraneous, but interesting). This looks funky in generated pages, due to Py str replacements. [2.0] This now uses a custom modal 'dialog' - which is really a full-page overlay with opacity, plus a display box within it, all shown and hidden on demand. Most implementation code (CSS, HTML, JavaScript) is at end of file; this formats the message. Why go custom? Some browser vendors discourage alert(), and even treat it as a threat, asking users if they wish to silence it. Worse, mobiles format alert() text badly with wrapping, and iOS 13's Hide Toolbars can fully botch it with text outside the box. The custom dialog instead scrolls text vertically when needed. Bonus: the new dialog box inherits viewer-page bg/fg color settings in user_configs.py; respects user font-size choices; supports copy/paste; and does not kill Full fullscreen displays. [1.7] May-2020: now labels date as 'Digitized' for photo scans. [2.0] Jul-2020: now shows device|software line iff in Exif tags. [2.0] Jul-2020: use "Created" for unknown data, and "Nw x Mh". [2.0] Jul-2020: use a custom modal-dialog display (@end of file). [2.3] Apr-2022: bg/fg/bd colors may now be custom, as for Note. [2.3] Apr-2022: device text is arbitrary: now JS+HTML escaped. [3.0] Jun-2025: show filename too for CAPTIONS.py and !extension. ------------------------------------------------------------------ */ var theimg = document.getElementById('theimg'); var domwide = theimg.width, domhigh = theimg.height; // info added here var popup = showInfo; // [2.0] alert() no more... see above/ahead // legacy: see ahead popup('Filename: 2003 Dublin 2.JPG\n' + // Py: filename [3.0] 'Taken: 2003-04-30 @13:13:52\n' + // Py: Exif (origin date) 'Modified: 2003-04-30 @13:13:54\n' + // Py: OS 'File size: 88,180 bytes\n' + // Py: OS 'Image size: 640w x 480h\n' + // Py: Pillow (also in DOM) 'Display size: ' + domwide + 'w x ' + domhigh + 'h' + // JS: DOM (dynamic) '\nDevice: DCR-IP7 (SONY)') // Py: Exif (device|software?) return false; // don't try to follow a fake <a> href, if coded } // // [2.0] The info-popup implementation continues near the end of this file. // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // IMAGE SCALING //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ function refitAspectRatio(trueWidth, trueHeight, // image size displayWidth, displayHeight, // display size stretchBeyondActual) { // expand if small? /* ------------------------------------------------------------------ Constrain image size to display area, retaining original aspect ratio. Use to shrink or enlarge images as needed to fit a new display size while showing the entire image. The math scales both sides per the smallest display ratio: trueWidth, trueHeight: width, height of actual/original source image (pixels) displayWidth, displayheight: maximum available width, height for image display (pixels) If 'stretch' is true, images may expand beyond their actual sizes to fit the display; if false, image maximum sizes are limited to actual size, to avoid blurring of small images. Returns an Object (see Python dict), with .width, .height. ------------------------------------------------------------------ */ var ratio; // per smallest-fit side ratio = Math.min(displayWidth / trueWidth, displayHeight / trueHeight); if (! stretchBeyondActual) { ratio = Math.min(ratio, 1.0); // don't expand beyond actual size } return {width: (trueWidth * ratio), height: (trueHeight * ratio)}; } function getViewportSize() { /* ------------------------------------------------------------------ Display size: yet another browser-specific mess... iOS reports available size differently (of course). A Math.max() might work here, but seems a bit iffy. These are often the same, but supported unevenly; in theory, client* sizes discount border and margin. ------------------------------------------------------------------ */ // to watch: // alert('window: ' + window.innerWidth + ' x ' + window.innerHeight); // alert('docelmt: ' + document.documentElement.clientWidth + // ' x ' + document.documentElement.clientHeight); if (! runningOniOS() && window.innerWidth !== undefined && window.innerHeight !== undefined) { return {width: window.innerWidth, height: window.innerHeight}; } else { return {width: document.documentElement.clientWidth, height: document.documentElement.clientHeight}; // portable much? } } function getUsedHeight() { /* ------------------------------------------------------------------ [1.7] Feb-2020: Return the total pixel height of all non-image elements on the page, used to deduce space left for the image. Though rare, the 110px fudge-factor constant formerly used to calculate image-area size could result in clipping of the image's bottom border and part of its content, when viewing very tall images and applying unusually large font settings. This was first seen for mobile portrait screenshots in Firefox on an S8+ smartphone, but could also happen on both desktop and mobile browsers if users selected larger fonts. This page's CSS font-size boost used for toolbar buttons on small screens made the clipping more likely, but user font/scaling settings alone could trigger it for some images and browsers. The new scheme deduces image space from space already taken, rather than relying on constants, or the undrawn image's space. The net effect now accommodates arbitrary user font settings; as a bonus, it also typically yields a larger image display. Caveat: this must still use a small constant size (fudgeExtra) to account for margins and the image border, but this seems to work universally (so far); alas, the DOM offsetHeight includes content+padding+border, but not margin, which is oddly complex. Coding Notes: - The <noscript> warning size is moot: never gets here to resize. - The result isn't cached, because font size can change any time. - This is coded defensively; browser variability is a nightmare. - See also elt.getBoundingClientRect(), a possible alternative. - Failed: elt.outerHeight(), getComputedStyle(elt)['marginTop']. - TBD: cache getElementById() results in global vars for speed? ------------------------------------------------------------------ */ // locals var debug = false; // show alerts? var defaultHeight = 110; // prior-constant fallback, prefudge var fudgeExtra = 12; // for margins + image border var usedHeight; try { var titleElement = document.getElementById('thetitle'); var toolbarElement = document.getElementById('thetoolbar'); var titleHeight = titleElement.offsetHeight; var toolbarHeight = toolbarElement.offsetHeight; if (titleHeight != 0 && toolbarHeight != 0) { usedHeight = titleHeight + toolbarHeight; } else { usedHeight = defaultHeight; if (debug) alert('usedHeight defaulted: zero element size'); } } catch (err) { usedHeight = defaultHeight; if (debug) alert('usedHeight defaulted: size fetch failed'); } usedHeight += fudgeExtra; if (debug) alert('getUsedHeight: ' + usedHeight); return usedHeight; } function setLoadingDisplay() { /* ------------------------------------------------------------------ To minimize 'flash' hide the image during initial download or load and post an indicator message. The message is erased and the image is unhidden when the image is sized. Otherwise, the only indicator of progress is the browser's busy indicator. The <P> isn't coded in HTML - else it could not be erased if JavaScript is disabled. This uses a global variable that can be reference later (though it might instead set a fetchable DOM id with loadingIndicator.id = 'theid'). ------------------------------------------------------------------ */ // hide the image temp document.getElementById('theimg').style.visibility = 'hidden'; // post a temp <P> indicator loadingIndicator = document.createElement('P'); // no 'var' means global loadingIndicator.style.fontStyle = 'italic'; // global means window loadingIndicator.style.textAlign = 'center'; loadingIndicator.innerHTML = 'Loading...'; // var text = document.createTextNode('Loading...'); // innerHTML is enough // loadingIndicator.appendChild(text); document.getElementById('thetitle').appendChild(loadingIndicator); } function resizeImage() { /* ------------------------------------------------------------------ Run on initial page load, and again on each window resize. As of 1.6, viewer pages use JS here to rescale the page's image to the current display area, retaining the image's original aspect ratio. CSS alone cannot preserve aspect ratio on all window resizes; when used, images can overflow containers and run off-screen, requiring scrolls to view. This emulates native display well on all desktop browsers, and scales images to the available space on mobile browsers. Exception: iOS landscape falls back on 1.5's CSS scrolled scaling, because no way could be found to do better (TBD; Safari munges sizes and UI, and it is most of iOS traffic). 1.5 CSS scaling is also used when JavaScript is disabled. [1.7] Update May-2020: JavaScript scaling is now also used for all iOS browsers in landscape mode. This works well on all non-Safari iOS browsers (and matches their Android behavior); is better on Safari in iOS < 13 (image bottom is easier to view); and works perfectly in iOS Safari if users enable the new toolbar-hiding option added in iOS 13. Hence, JS scaling is now used everywhere by default, when JS is enabled. As a legacy option, 1.5's CSS-based display can still be used for landscape in iOS Safari, because its toolbars mar pages sans the iOS 13 fix. Scaling to the full viewport size is partly heuristic: the constants here work everywhere tested, but may need tweaking; alas, no universal way to get a scaling-area size was found. Tried but failed: a <div> with display:flex + <canvas>/Image(). [1.7] Update Feb-2020: the available image height is now deduced from the sizes of other page elements instead of constants, to accommodate font settings; see getUsedHeight(). Some Python % replacements here could be dynamic JavaScript. Caution: browser settings can have big side effects - Android Chrome's "Force enable zoom" may break same "Raw" displays. Note: the DOM's theimg.natural{Width, Height} here is original in-file image size, and the same as the {ORIGWIDE, ORIGHIGH} fetched from Pillow and expanded in imageInfoDialog() by .py. ------------------------------------------------------------------ */ // erase message paragraph loadingIndicator.style.display = 'none'; // HTML element: via DOM, per <img> tag var theimg = document.getElementById('theimg'); // new display area size for image scaling var viewportsize = getViewportSize(); // [1.7] calc size of other page elements var usedheight = getUsedHeight(); // [1.7] landscape on iOS in Safari: use legacy CSS display? if (false && runningInSafariOniOS() && viewportsize.width > viewportsize.height) { // sizes and UI munged: use version 1.5 CSS scaling theimg.style.width = '100%'; theimg.style.height = 'auto'; } else { // erase any iOS landscape settings theimg.style.width = null; theimg.style.height = null; // calc new scaled size (less borders, title+toolbar+cruft) // [1.7] usedheight was 110, now actual space used var refitsize = refitAspectRatio( theimg.naturalWidth, theimg.naturalHeight, viewportsize.width - 18, viewportsize.height - usedheight, false); // change in DOM/page: redraws theimg.width = refitsize.width; // see also .style.cssText('width:n;') theimg.height = refitsize.height; // see also .style.height=n (inline CSS) } // show now, minimizing initial flash theimg.style.visibility = 'visible'; } // // Register resize callback handler now // Or: <body onresize="resizeImage()" onload="resizeImage()"> // window.addEventListener('resize', resizeImage); // There's more JS ahead (info popup, touchpads, etc.): search for "<script" </script> <!-- ==================================================================================== Back to HTML: layout, inline CSS styles, CSS/JavaScript fodder/linkage ==================================================================================== --> </head> <!-- initial sizing, else opens fullsize ("...code..." = func) --> <body id="body-viewer" onload="resizeImage();"> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!-- JS NOTE: iff JavaScript not enabled, alert user --> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!-- [2.3] JS is now required, as hrefs for Prev/Next/Index were dropped to --> <!-- kill URL popups; otoh, viewers were largely broken without it before; --> <!-- 2.3 also omits Note/Auto/Full btns if no JS, but keeps other (layout); --> <noscript> <div class=overflowscroll> <p class=nojsnote> This page is best viewed with JavaScript enabled </p></div> </noscript> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!-- FILENAME: possibly long, <h1> takes away too much image space --> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!-- [1.7] popup mod/taken dates + size info dialog on filename click/tap --> <!-- use same hover style as bottom buttons for indicator and consistency --> <div id=thetitle class=overflowscroll> <!-- already scrolled [2.0] --> <p class=filename> <!-- with <p>+<span>, android chrome (only!) selects text and prompts for a copy --> <!-- instead, use simple <a> + onclick for JS use only + manual cursor (no href) --> <a class="navlink cursorpointer" onclick="return imageInfoDialog();" title="View image info"> 2003 Dublin 2 </a> </p></div> <!--DEFUNCT <span style="cursor: pointer;" onclick='alert("...")'> 2003 Dublin 2 </span> DEFUNCT--> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!-- IMAGE: (re)scaled to fit window/viewport, clickable for raw --> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!-- [1.7] make image click/tap same as Raw (view fullsize) for convenience; --> <!-- especially useful for smaller image displays of most mobile landscapes; --> <!-- doesn't use a simple href, because the mouseover url popup is too busy; --> <!-- [2.2] image tap still works as before with image-swipe touch gestures; --> <!-- [3.0] add raw-view message, else may be hard to tell have left gallery; --> <img id=theimg src="../2003%20Dublin%202.JPG" class=cursorpointer alt="Full image" title="View raw image" onclick='show_message("Raw view: return to gallery with Back", "../2003%20Dublin%202.JPG", true);'> <!-- iff JavaScript enabled, hide now to minimize initial flash, show indicator --> <script> // // on page load // setLoadingDisplay(); // to test: // setTimeout(setLoadingDisplay, 2000); // func, msec // setTimeout(resizeImage, 4000); </script> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!-- BUTTONS: small but scrolled to avoid mobile-viewport overflow --> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!-- [1.7] large user fonts could run together: avoid by dropping CSS scale-up --> <!-- this, plus the new used-space calcs, yield slightly larger image displays --> <!-- [2.0] toolbar buttons now scroll, so overflow seems a largely moot point --> <!-- [2.2] add optional tooltip popups to Auto, Full, images, and filename --> <!-- [2.3] add optional Note button, drop hrefs, more tooltips, delete buttons --> <!-- [3.0] add Next/Prev wraparound (rollover) messages, auto end-gallery page --> <div id=thetoolbar class=navdiv> <!-- [1.7] id for space --> <p> <table class=navtable id=toolbartable> <tr> <!-- hrefs used if JS is disabled (or ios-chrome browser bug work-around used); --> <!-- it may be nice to drop the URL popups on mouseover, but hrefs trigger them; --> <!-- [2.3] Dropped all hrefs here (e.g., href="2003%20Dublin%201.jpg.html", href="#slideshow"): they are no longer used as fallbacks for Prev/Next if JS, Index is now a func, and tooltips are default. This avoids obtrusive URL popups on mouseover, which are now redundant with tips. It also requires JS for Prev/Next/Index, but viewer pages were already unusable without it. The 2.3 code renders identically to 2.2, when no Note button is present. 2.3 also changed hardcoded 'index.html' to use INDEX config: name may differ per build. [3.0] Augment Next/Prev to show gallery wraparound message if enabled and @image N/0. --> <td><a class=navlink title="Go to previous image" onclick="return onNavClick_Prev('2003%20Dublin%201.jpg.html')" >Prev</a></td> <td><a class=navlink title="Go to next image" onclick="return onNavClick_Next('2003%20LP2E%20Book.jpg.html')" >Next</a></td> <td><a class=navlink title="Go to thumbnails page" onclick="window.location.href='../index-thumbsonly.html'; return false;">Index</a></td> <td><a class="navlink optional" id=noteBtn title="View image description" onclick="showNote(); return false">Note</a></td> <!-- [2.3] new --> <td><a class="navlink optional" id=autoBtn title="Toggle slideshow" onclick="onAutoClick(); return false">Auto</a></td> <!-- [2.0] new --> <td><a class="navlink optional" id=fullBtn title="Toggle one-page fullscreen" onclick="onFullClick(); return false">Full</a></td> <!-- [2.0] new (lesser) --> </tr></table> </p> <!-- the /p adds space below links bar, oddly: don't remove! --> </div> <script> // // reschedule Auto timer event on the new page, and change // Auto's font to underline now so change is not noticeable; // autoContinue(null); // // show Auto button now, if JS is enabled (else it's a no-op) // document.getElementById("autoBtn").style.display = "block"; // // [2.3] show Note button if JS && feature enabled via config+files // if (true) { document.getElementById("noteBtn").style.display = "block"; } else { // delete noteBtn, else throws off spacing, especially on mobile document.getElementById("toolbartable").rows[0].deleteCell(3); } // // show optional Full button now iff JS and config enabled // if (true) { document.getElementById("fullBtn").style.display = "block"; } else { // [2.3] ditto for spacing, now (4) if Note was deleted, else (5) document.getElementById("toolbartable").rows[0].deleteCell(true ? 5 : 4); } </script> <!--DEFUNCT [2.0] raw display is now image-tap or down-swipe (see also filename-tap info) <td><a class=navlink href="../2003%20Dublin%202.JPG">Raw</a></td> DEFUNCT--> <!-- POPUP OVERLAYS --> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!-- INFO POPUP: custom modal dialog, via div overlay+opacity [2.0] --> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ CSS: why not in <head>? - OK per HTML 5.2, OK in all browsers tested, this is really a new page def, and code proximity is much better here. Dialog-box colors are from bg/fg user settings for entire viewer page. Page or text scrolls may be needed for mobile and/or very large fonts. Parts of the following are also used for the Note display in [2.3]. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <style> /* Modal-dialog display: full page, background */ .InfoDisplay { display: none; /* hidden initially */ position: fixed; /* stay in place */ z-index: 1; /* open div on top: overlay */ left: 0; /* fill entire page */ top: 0; width: 100%; /* full width+height */ height: 100%; overflow: auto; /* enable full-page vertical scrolls, if ever? */ /* [2.3 now customizable */ background-color: rgb(0, 0, 0); /* fallback color: blank bg */ background-color: rgba(0, 0, 0, 0.45); /* black, with opacity: dim bg */ } /* Modal-dialog box: text + button content on page */ .InfoBox { margin: 8px auto; /* top+bottom, right+left; offset from top, centered */ padding: 4px; /* top offset can't be much on mobile */ padding-left: 20px; /* margin=outside, padding=inside */ border: thin solid; /* #888 color nice, unless same as bg */ width: 80%; /* up to this much of the overlay/page */ max-width: 400px; /* but not too wide on desktop or landscape */ border-radius: 6px; /* rounded borders (for the kids) */ /* [2.3] colorize dialog per configs */ color: #e0e0e0; /* foreground: text (not OK) */ background-color: #333; /* background: full widget (not OK) */ border-color: white; /* dialog (and OK button) border */ } /* Modal-dialog message: preformatted text in box */ .InfoText { overflow: auto; /* enable text-area horizontal scrolls, if needed */ /* [2.3] inherit colors from InfoBox dom parent, not page or 'initial' */ color: inherit; background-color: inherit; /* [3.0] non-default text line spacing? */ line-height: 1.25em; } /* Modal-dialog close: OK button in box */ .InfoClose { display: block; margin: auto; /* centered in box */ margin-bottom: 6px; /* but hug the box bottom */ /* [2.2] customize to make larger on iOS */ padding: 2px 8px 2px 8px; font-size: 0.8em; /* [2.3] use page's bg/fg colors, but inherit border from InfoBox dom parent */ border-color: inherit; border-radius: 6px; /* round corners to match popup, while we're at it */ } .InfoClose:hover, .InfoClose:focus { cursor: pointer; /* nothing fancy: mobiles may botch hover */ } </style> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ HTML: make elements that use CSS and are referenced in JavaScript. Build a div that overlays entire page, to be shown/hidden in demand. This essentially defines a new page, but it looks like a popup box. Fails for big fonts: <span id=infoClose class=InfoClose>×</span> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!-- Modal dialog display: full-page container --> <div id=infoDisplay class=InfoDisplay> <!-- Modal dialog box: text + close content --> <div class=InfoBox> <pre id=infoText class=InfoText>Replace me on opens...</pre> <button id=infoClose class=InfoClose> OK </button> </div> </div> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JavaScript: must be after HTML to cache elements in globals on load (or: run these from an onload event handler for DOM-element access). showInfo() is called on filename tap from imageInfoDialog() above, which formats the message text, and documents this dialog further. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <script type="text/javascript"> /* ok anywhere in page, type optional */ // [3.0] disable wheel swipes while Note, info, or transient message open var modaldialogopen = false; // Get the full modal display <div> in a global var infodisplay = document.getElementById('infoDisplay'); // Get the text-message element to be replaced var infotext = document.getElementById('infoText'); // Get the OK-button element that closes the display // or document.getElementsByClassName('InfoClose')[0] var infoclose = document.getElementById('infoClose'); // On OK click/tap, close entire modal overlay infoclose.onclick = function() { modaldialogopen = false; noteOrInfoCloser = null; infodisplay.style.display = 'none'; // OK: hide/close display } // On clicks outside the box, close entire modal overlay function onClickInfo(event) { if (event.target == infodisplay) // if tap bg, not box infoclose.onclick(); } // but not window.onclick = function(){}: also for Note overlay; // [3.0] like note, the first of these never fired on iOS Safari, // but unknown if this was fixed by this code, other mods, or iOS; if (!runningInSafariOniOS) window.addEventListener('click', onClickInfo); // original else document.querySelector('body').addEventListener('click', onClickInfo); // [3.0] // On filename clicks, open the modal display (called above, formerly alert()) function showInfo(message) { modaldialogopen = true; // [3.0] for wheel swipes noteOrInfoCloser = infoclose.onclick; // [3.0] for Enter = OK press infotext.innerHTML = message; infodisplay.style.display = 'block'; // show/popup display } </script> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!-- NOTE POPUP: custom modal dialog, via div overlay+opacity [2.3] --> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ CSS: largely reused from Info popup above - see it for more intro docs. Dialog-box colors are from bg/fg user settings for entire viewer page. Page or text scrolls may be needed for mobile and/or very large fonts. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <style> /* Modal-dialog display: full page, background */ /* Reuse .InfoDisplay for overlay: identical CSS */ /* Modal-dialog box: text + button content on page */ .NoteBox { /* margin=outside, padding=inside */ padding: 4px 20px 4px 20px; /* top, right, bottom, left */ border: thin solid; /* #888 color nice, unless same as bg */ border-radius: 6px; /* rounded borders (for the kids) */ position: absolute; /* anchor to page bottom (for up-swipe) */ bottom: 8px; left: 15%; /* box is what's left, large=good for mobile */ right: 15%; /* e.g., 15 percent leaves 70 percent for box */ max-height: 100%; /* overflow works on sized block elms like <p> */ overflow: auto; /* vscroll notes too wordy for the page */ /* [2.3] colorize dialog per configs: border doesn't work in a mixin class here */ color: #e0e0e0; /* foreground: text (not OK) */ background-color: #333; /* background: full widget (not OK) */ border-color: white; /* dialog (and OK button) border */ } /* Punt: no longer centered on page -> [max-width: 600px;] */ /* Punt: not needed and fails on IE<9 -> [transform: translate(-25%, 0);] */ /* Punt: not centered -> [margin: 8px 20% 8px 20%; min-width: 53.5%;] */ /* Modal-dialog message: collapsed <p> text in box */ .NoteText { /* != InfoText: hscroll irrelevant here for <p>, and vscroll via NoteBox div */ /* [2.3] inherit colors from NoteBox dom parent, not page or 'initial' */ color: inherit; background-color: inherit; /* [3.0] non-default text line spacing? */ line-height: 1.25em; } .NoteText * { /* [3.0] so text and embedded tags in note assigned to innerHTML inherit colors too */ color: inherit; background-color: inherit; } .NoteText A { color: cyan; /* [3.0] color <a> links in Note if set, else 'inherit' */ } /* Modal-dialog close: OK button in box */ /* Reuse .InfoClose for OK: identical CSS, but inherits bd color from NoteBox here */ </style> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ HTML: make elements that use CSS and are referenced in JavaScript. Build a div that overlays entire page, to be shown/hidden in demand. This essentially defines a new page, but it looks like a popup box. The <p> doesn't support paragraph breaks: use <br><br> in the insert. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!-- Modal dialog display: full-page container --> <div id=noteDisplay class=InfoDisplay> <!-- Modal dialog box: text + close content --> <div class=NoteBox> <p id=noteText class=NoteText>Replace me on opens...</p> <button id=noteClose class=InfoClose> OK </button> </div> </div> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JavaScript: must be after HTML to cache elements in globals on load (or: run these from an onload event handler for DOM-element access). showNote() is called on Note tap and from up-swipe handler above. [3.0] NOTECONTENT can now have embedded HTML tags: links, bold, etc.; innerText may have worked here too, but embedded tags makes it moot; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <script type="text/javascript"> /* ok anywhere in page, type optional */ // Nit: this code is pointless but harmless for note-less galleries // (i.e., !ENABLENOTES); adding a nesting level seems pointless too // Get the full modal display <div> in a global var notedisplay = document.getElementById('noteDisplay'); // Get the text-message element to be replaced var notetext = document.getElementById('noteText'); // Get the OK-button element that closes the display var noteclose = document.getElementById('noteClose'); // The note's file's content from Python, Unicode decoded and JS+HTML escaped var notecontent = ''; // Distinguish if no real note: don't waste users' time; noteBtn may be deleted! if (true && !notecontent) { document.getElementById("noteBtn").style["text-decoration"] = "line-through"; } // On OK click/tap, close entire modal overlay noteclose.onclick = function() { modaldialogopen = false; noteOrInfoCloser = null; notedisplay.style.display = 'none'; // OK: hide/close display setNoteClosedState(); // [3.0] mod cross-page sessionStore } // On clicks outside the box, close entire modal overlay function onClickNote(event) { if (event.target == notedisplay) // if tap bg, not box noteclose.onclick(); } // but not window.onclick = function(){}: also for Info overlay; // [3.0] like info, the first of these never fired on iOS Safari, // but unknown if this was fixed by this code, other mods, or iOS; if (!runningInSafariOniOS) window.addEventListener('click', onClickNote); // original else document.querySelector('body').addEventListener('click', onClickNote); // [3.0] // On Note, up-swipe, and possibly page load, open the modal display function showNote() { if (!notecontent) noteText.innerHTML = '(No note)'; // [3.0] not innerText: embedded tags OK else notetext.innerHTML = notecontent; // text from imagename.note file or NOTES.py modaldialogopen = true; // [3.0] for wheel swipes noteOrInfoCloser = noteclose.onclick; // [3.0] for Enter = OK press notedisplay.style.display = 'block'; // show/popup display (inline- same) setNoteOpenState(); // [3.0] mod cross-page sessionStore } </script> <!-- TRANSIENT MESSAGES [3.0] --> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!-- Transient informational messages: start/end of gallery, raw-view mode [3.0] --> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!-- Raw-view mode message always displayed on image tap. Start/end of gallery message displayed on Next/Prev tap at image N/0 and Auto at image N if enabled. These messages andNote and info preclude touch events but not wheel events. Tip: auto end-gallery page is a lighter-weight way to denote end-of-gallery. --> <!-- Modal dialog display: full-page container --> <div id=msgDisplay class=InfoDisplay> <!-- Modal dialog box: text + close content --> <div class=InfoBox> <pre style="text-align: center;" id=msgText class=InfoText>Replace me on opens...</pre> </div> </div> <script> function show_message(text, nextpage, enableback, showmsecs=2000) { // // called for all image taps, and Next/Prev wrap-around taps // also called for upswipe when no notes (== image tap) // var msgdiv = document.getElementById('msgDisplay'); var msgtxt = document.getElementById('msgText'); // set text and show msgtxt.innerHTML = text; msgdiv.style.display = 'block'; // alert(text) too abrupt modaldialogopen = true; // blocks swipes during pause // goto nextpage (and erase text?) in 2 seconds if (enableback) { // add to Back stack: Back is this page doclose = function() { msgdiv.style.display = 'none'; // else msg still open on Back! modaldialogopen = false; window.location.href = nextpage; } } else { // don't stack page: Back is index page doclose = function() { onNavClick(nextpage); // current page and its modal state moot } } setTimeout(doclose, showmsecs); } </script> <!-- DARK-THEME MODE STYLES [3.0] --> <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Color themes based on and responsive to host mode, if enabled [3.0] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> <!-- Later in file to override earlier code, when order trounces CSS specificity. Empty if theme disabled in user_configs.py. --> <STYLE> /*--------------------------------------------------------------------- [3.0] CSS code for automatic dark theme per host device's settings. Auto-added to generated index and viewer pages if useCannedDarkTheme in user_configs.py is 'host' or 'always'. 'always' uses the dark mode here whether set on host or not, and 'host' toggles between dark mode here and other color configs per the host device's mode setting. This is a template with two uppercase replacements and a % escape. Its BODY:not(#body-viewer) and BODY#body-viewer mean only index and pages, respectively (<body> may be in a custom HEADER.html for indexes). Its rules generally override host pages (and hence, user_configs.py), subject to CSS specificity and ordering. Themes can be coded in JavaScript too, but CSS is auto-responsive to host-setting changes. Edit this file as desired, but please mod this brittle code with care if you're unfamiliar with CSS; it's complex because thumbspage was not originally designed for this use case, and this file overrides and does not mesh with the original and simpler color-configs model. For more info, see the comments at user_configs.py's useCannedDarkTheme switch and the overview in UserGuide.html#3.0. Nit: this may have been coded more simply as a top-level media query that selects between light and dark color sets, but we also need to override colors in custom index-page headers but not custom index-page footers, and these may use arbitrary styling. Feedback is welcome. -----------------------------------------------------------------------*/ /* LIGHT MODE, PER HOST */ @media (prefers-color-scheme: light) { /* if light mode on host, now or post change */ /* tbd: if used, overrides some user_configs.py settings */ } /* DARK MODE, PER HOST OR FORCED */ @media (prefers-color-scheme: dark) { /* if dark mode on host, now or post user change */ /* used for light _or_ dark if forcing dark theme */ BODY:not(#body-viewer) { /* index page only, may have HEADER/FOOTER.html */ color: #e0e0e0; /* off-white, #e0e0e0 between #eee and #ddd */ background-color: #121212; /* off-black, #121212 per material design (black=#000) */ } /* viewer-page global fg/bg colors use a template-viewpage media query */ IMG { /* all image borders, index and viewer pages */ border-color: /* !important to override style= attr in HTML */ #e0e0e0 !important; } #theimg { /* border of images in viewer pages only */ border-color: #e0e0e0 !; } BODY:not(#body-viewer) IMG { /* else bright imgs can make text hard to read */ filter: brightness(90%); /* but just for index, not viewer pages; %=one */ } #thumbslinks { /* index-page table (fixed) or div (dynamic) */ border-color: #e0e0e0 !important; /* !important: override HTML style= */ background-color: #242424 !important; } #thumbslinks TD { /* image labels/captions in thumbs table (fixed) */ color: #e0e0e0 !important; } #thumbslinks DIV:last-child { /* image labels/captions in thumbs div (fixed) */ color: #e0e0e0 !important; } BODY A { /* index-page hyperlinks, overrides HEADER.html */ color: cyan; /* not !important: may mod links in FOOTER.html */ } /* & not > specificity "BODY:not(#body-viewer)" */ BODY#body-viewer A { /* hyperlinks on viewer pages + <a> tags in Notes */ color: cyan !important; /* there is no footer to impact on viewer pages */ } .NoteBox, .InfoBox { /* viewer popups in dark theme: note, info, msg */ color: wheat; /* can be configed in user_configs.py vs here */ background-color: black; /* viewer page is implied: not in indexes */ border-color: white; /* border of popups viewer (!= border of image) */ } } </STYLE> <!-- TOUCHPAD/MOUSEWHEEL SWIPES [3.0] --> <script> /*----------------------------------------------------------------------------------- [3.0] In addition to existing left/right/up/down swipe support on touchscreens of phones and PCs, allow both two-finger swipes on touchpads and horizonal mousewheel scrolls (including 1-finger swipes on macOS "magic" mice) to trigger the same actions on PCs. Along with the new keypress support, this makes gallaries more PC friendly. This is just an alternative to clicks on Prev/Next/Note/filename-or-caption. It also prevents these swipes from triggering any default browser actions, so it can be disabled per gallery in user_configs.py. Shares touchscreen handler code above. Caveat: this uses the DOM wheel event, which fires a stream of events that must be debounced; that makes it unavoidably laggy vs touch (see also the notes below). About events: touch and wheel events should be mutually exclusive (i.e., never both fired for the same gesture) and have not been seen to both fire on any PC or mobile browser tested, but references are a bit ambiguous on this. event.preventDefault() _might_ ensure this on hybrid devices (screen + pad-or-mouse), but it's impossible to test this universally. Instead, this page is coded defensively to explicitly disallow wheel events while processing touch events, and vice versa. See also the notes about pointer, scroll, and mouse events below; it's quite a jamboree. Ref: https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent Ref: https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event -------------------------------------------------------------------------------------*/ function padswipe(event) { // actual handler, once events stop coming in // both are 1, 0, -1, or greater abs var moveX = event.deltaX; var moveY = event.deltaY; //alert('horiz=' + moveX + ' vert=' + moveY); // which changed most? if (Math.abs(moveX) > Math.abs(moveY)) { // horizontal swipe if (moveX < 0) doLeftSwipe(); else doRightSwipe(); } else { // vertical swipe if (moveY > 0) doDownSwipe(); else doUpSwipe(); } } /* Debounce wheel event to avoid firing multiple actions (e.g., multiple Prev/Next hops). This makes pad swipes laggy, but no known alternative: a mouse wheel spin or pad scroll sends a barrage of wheel events and we cannot take action until all have been sent. Left/right = a new page that may receive pending events and refire. Two debouncing options were coded, and both had same lagginess: inherent in wheel. wheelwaitmsecs=200 was almost OK, but Windows Chrome/Edge occasionally skipped ahead. */ // Debouncing option 1 (used): // cancel+reschedule timer on each wheel instead of counting events during pause. // Note that these functions run asynchronously and don't overlap in time. wheeltimerid = null; // null = no-op in clearTimeout wheelwaitmsecs = 300; // higher is safer but slower function onWheelTimer(event) { wheeltimerid = null; // timer expired before new wheel wheelInProgress = false; padswipe(event); // event is last wheel's event }; function onWheelEvent(event) { // on each new wheel event: cancel+resched logevent('wheel'); if (modaldialogopen) // skip if Note or info (above) are open return; // overlay precludes touch on image if (touchInProgress) return; // skip wheel if touch is happening wheelInProgress = true; // skip touch if wheel is happening event.preventDefault(); // else browser intercepts too: can't do both clearTimeout(wheeltimerid); wheeltimerid = setTimeout(onWheelTimer, wheelwaitmsecs, event); }; // detect swipes when mouse anywhere over window (not just image) if (true) { window.addEventListener('wheel', onWheelEvent, {passive: false}); } /* UNUSED ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Debouncing option 2 (not used - but seemed just as fast, err, slow): // count wheel events during pause and check wheels counter every N msecs. // Note that these functions run asynchronously and don't overlap in time. debouncing = false; wheelcount = 0; pausemsecs = 200 // a bit of a guess (too low for some?) function onWheelTimer(event) { // pause timer expired if (wheelcount == 0) { debouncing = false; // if no new wheels during wait, call now wheelInProgress = false; // else reschedule timer and wait again padswipe(event); } else { wheelcount = 0; setTimeout(onWheelTimer, pausemsecs, event); } } function onWheel(event) { // touchpad or mousewheel swipe logevent('wheel'); if (modaldialogopen) // skip if Note or info (above) are open return; // overlay precludes touch on image if (touchInProgress) return; // skip wheel if touch is happening wheelInProgress = true; // skip touch if wheel is happening event.preventDefault(); // else browser intercepts too: can't do both //event.stopPropagation(); // moot: no other listeners in this app if (debouncing) { wheelcount += 1; // if pausing, increment incoming-events counter } // else save first event, check for pause in N msecs else { debouncing = true; wheelcount = 0; setTimeout(onWheelTimer, pausemsecs, event); } } // detect swipes when mouse anywhere over window (not just image) if (true) { //window.onwheel = onWheel; // not faster either way window.addEventListener("wheel", onWheel, {passive: false}); // punts: {capture: true}, {once: true} } UNUSED ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ /* DOC ONLY ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ABOUT UI EVENTS: this page now uses events touch* for touchscreens and wheel for touchpads and mice, but not scroll*, pointer*, or mouse*. Here's why. scroll* requires scrollable content, which viewer pages don't have. Exception: Firefox fires scrollend (but not scroll) along with wheel for swipes on Windows always, and on macOS iff event.preventDefault() isn't called. In other contexts, scrollend is fired only for scrollable content like Note/info dialogs - except on Safari where it may be wholly MIA. All of which makes scroll events unusable here. pointer* don't fire on PCs unless the mouse pointer actually moves (e.g., 2-finger pad scrolls fire just wheel and no pointer*, preventDefault() or not). Conversely, pointer* do fire for all touchscreen ops on mobile and PCs (e.g., even for Note OK taps). We want to intercept just scrolls (not all 1-finger touchpad moves on PCs), and it's tough to tell touchpads apart from touchscreens (we need 2-finger for the former but 1-finger for the latter). Hence, pointer* can't replace touch* here. mouse* events were not explored, but they don't seem usable for a scroll gesture. Beyond all this, browsers' developer tools tend to emulate devices with poor precision, and tracing events on actual mobile devices requires connecting a PC with USB debugging, adb, and ad-hoc support in Chrome. What a circus! if (true) { window.addEventListener("scroll", function (_) {logevent('scroll');}, {passive: false}); window.addEventListener("scrollend", function (_) {logevent('scrollend');}, {passive: false}); } if (true) { window.addEventListener("pointerup", function (_) {logevent('pointerdown');}, {passive: false}); window.addEventListener("pointermove", function (_) {logevent('pointermove');}, {passive: false}); window.addEventListener("pointerdown", function (_) {logevent('pointerup');}, {passive: false}); } // to be defensive... // if (true) { // if ('onscrollend' in window) { // window.addEventListener("scroll", onScroll, {passive: false}); // window.addEventListener("scrollend", onScrollend, {passive: false}); // } // else // alert('no scrollend support'); // } DOC ONLY ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ </script> <!-- KEYPRESS EVENTS [3.0] --> <script> /*----------------------------------------------------------------------------------- [3.0] In viewer pages, keyboard keys: [p, n, i, t, a, f, ?, ., Enter] are now the same as taps (or their equivalent swipes) of: [Prev, Next, Index, Note, Auto, Full, label (info), image (raw view), OK (if open)] when a keyboard is present on the host. -------------------------------------------------------------------------------------*/ noteOrInfoCloser = null; // global set by Note and info popups document.addEventListener('keydown', function(event) { // alert('keydown'); switch (event.key) { case 'p': onNavClick_Prev('2003%20Dublin%201.jpg.html'); break; case 'n': onNavClick_Next('2003%20LP2E%20Book.jpg.html'); break; case 'i': window.location.href='../index-thumbsonly.html'; break; case 't': showNote(); break; case 'a': onAutoClick(); break; case 'f': onFullClick(); break; case '?': imageInfoDialog(); break; case '.': show_message("Raw view: return to gallery with Back", "../2003%20Dublin%202.JPG", true); break; case 'Enter': if (noteOrInfoCloser != null) noteOrInfoCloser(); break } return false; }); </script> <!-- PERSIST NOTE OVERLAYS [3.0] --> <script> /*----------------------------------------------------------------------------------- [3.0] In viewer pages, ensure that a Note remains open for further reading if a user follows one of its new embedded links and returns to the Note later. A note stays open until explicitly closed, even after navigating to the index, another gallery image, or to+from a link. This is crucial (notes are now mini web pages), and browsers don't all restore the note automatically or consistently, in part due to their back-forward caching, (a.k.a bfcache): pages are reset in full outside cache limits. Like Auto slideshows, this uses sessionStorage (not localStorage) cross-page storage for state memory, to limit it to the current browser tab/window; a note shouldn't open when the page is revisited in a future unrelated session. sessionStorage is per tab and localized to page origin (scheme + domain + port), but not pathname, so we must use page pathnames in keys here to make them unique. Else, an open note elsewhere does would trigger one in a different page. For non-existant keys, storage removeItem() is a no-op and getItem() returns a falsy none. All of this applies to Notes only: the info popup has no links and is mostly modal, and 3.0's first/last/raw message popups are transient. This is a no-op if the browser keeps the Note open on return--it simply resets the open overlay to be visible redundantly. showNote() may also reset storage needlessly when called on page load, but this is harmless. Must use 'pageshow' instead of 'DOMContentLoaded' because the latter may not fire if a browser retrieves the page from its back-forward cache (bfcache). In pageshow, event.persisted might detect return to page, but it seems iffy and has no obvious purpose here. Caveats: this may leave Notes open in ways a user doesn't want, but keeping the Note open after returning from a link outweighs other use cases. This also may not work at all if users disable sessionStorage (where still possible). ---- Update: URL normalization Late in 3.0, the page's URL is now normalized here so that different forms of it map to the same sessionStorage key. Else, a page's note will not auto reopen on pageshow if the page is first opened for a URL with spaces (or other) and a revisit uses an equivalent URL with %20 (or other) escapes--and vice versa. Normalization uses JavaScript's encodeURIComponent() global, which escapes most characters--including percents for already-encoded characters: map these back to a percent as a post step, else they won't match newly escaped equivalents having a percent. This may be overkill (e.g., path separators get escaped too), but galleries generate URLs that use arbitrary filename characters, and we need URLs to be normalized to a common form of any sort. Also tried: URL().toString() didn't remove all differences, and encodeURI() didn't escape enough characters. ---- Update: forced closes too Depending on the browser's bfcache, it's also possible that a note closed along another navigation path to a page may remain open when returning to the page by browser Back: in addition to forced open when sessionStorage key is set, also force close the note here if the sessionStoorage key is gone. This is rare, but occurs if a page's note has links to other pages with links to the page. The close here is naive, because notedisplay.style.display is always empty and window.getComputedStyle(element).display is none: the page has been rerendered. There is a slight chance that noteclose.onclick() will clear modaldialogopen for another dialog opened on return via bfcache (e.g., Info) thereby allowing swipes to trigger actions, but this is unlikely (or impossible?) and seems harmless. Caveat: this cannot be guaranteed to reclose on browser Back in all browsers. E.g., per devtools on macOS, Firefox's Back restores sessionStorage keys set by a prior visit that have been deleted in a later visit to the same page (but Chrome does not). This seems like a bug in Firefox: SS is supposed to span all pages within a tab and origin, and all pages are 'file://same' where this was seen. OTOH, this is an extemely rare use case and its UI impact is very minor: punt! -------------------------------------------------------------------------------------*/ // trace = function (m) {console.log(m);} // use page's pathname as storage key (one per page per session) thisPagePath = window.location.pathname; // normalize path so all variations map to same key try { normPath = encodeURIComponent(thisPagePath); // all, including pre-encoding percents normPath = normPath.replace(/%25/g, '%'); // undo percent escapes so match adds } // double percents for Py formatting catch (e) { trace('Note bad url'); normPath = thisPagePath; // on all errors: punt } noteOpenKey = 'thumbspageNoteOpen: ' + normPath; trace('note key: ' + noteOpenKey); function setNoteOpenState() { // record open state on overlay opens trace('note opened'); if (hasSessionStorage()) sessionStorage.setItem(noteOpenKey, 'true'); } function setNoteClosedState() { // discard open state on overlay closes trace('note closed'); if (hasSessionStorage()) sessionStorage.removeItem(noteOpenKey); } function checkNoteOpenState() { // check open state on pageshow events: reopen or reclose note trace('note pageshow'); if (hasSessionStorage()) { if (sessionStorage.getItem(noteOpenKey) === 'true') { // returned from link or opened on another nav path: forced open trace('note reopen'); showNote(); } else { // bfcache opened on return but closed on another path?: forced close trace('note reclose'); noteclose.onclick(); } } } // register to check note state on return to page window.addEventListener('pageshow', checkNoteOpenState); </script> </body> </html>