File: thumbspage/template-viewpage.html

<!DOCTYPE HTML>
<html>
<!-- Generated %(PAGEGENDATE)s, by thumbspage %(VERSION)s: learning-python.com/thumbspage.html -->

    <!-- 
    ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    Context: thumbspage.py makes pages from template-viewpage.html - 
    a combination of HTML, CSS, and JavaScript, expanded by Python
    for each image, with uppercase string replacements set by the .py.
    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. 
    [1.7] Added generation date + version above, info popup display.
    [2.0] Added slideshow via Auto toggle, drop Raw (now image tap).
    [2.0] Added fullscreen via Full toggle; iOS Chrome history fixed.
    [2.0] Added custom dialog for 1.7's info popup (filename tap).
    ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    -->

<head>

<!-- Be Unicode and mobile friendly -->
<meta http-equiv="content-type" content="text/html; charset=%(ENCODING)s">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

<!-- Plus analytics code, custom styles, etc. (replace me) -->




<!--
====================================================================================
CSS: style HTML elements by id, class, or element type
====================================================================================
-->



<style type="text/css">    /* in head only, 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: %(BGCOLOR)s;               /* bg is entire background */
    color: %(FGCOLOR)s;                          /* text, not border or no-JS note */
}


/* Don't display scrollbars (else Chrome on Windows/Linux may, briefly) */
body {
    overflow: hidden;    /* added late, 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;            /* slighly more uniform then 'fixed' [2.0] */
    min-width: 175px;              /* usable min: else no scrolls [2.0] */
}

.navlink {
    display: block;                /* buttons; nicer links through ad hoc css? */
    text-decoration: none;
    text-align: center;
}


/* 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 */
}


/* [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>


/* [2.0] Optional toolbar buttons: not if no JS, or disabled in configs */ 
.optional {
    display: none;    /* initially HIDDEN (changed on load if enabled) */
}



/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
/* 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.
   -------------------------------------------------------------------
*/
/*---DEAD CODE
.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;
}
---*/


/* 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.
    ------------------------------------------------------------------
*/
/*---DEAD CODE
@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;
}
}
---*/



/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
/* Message posted when JavaScript disabled (top of page)            */
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/


.nojsnote {
    text-align: center; 
    font-style: italic; 
    margin-top: 0px;        /* other: text-shadow */
    color: %(JSCOLOR)s;     /* 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 %(BDCOLOR)s;    /* 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 */
}

</style>


<!-- Iff JavaScript not enabled: use former 1.5 CSS scaling -->
<noscript>    
<style>
#theimg {
    width:  %(IMAGEWIDE)s; 
    height: %(IMAGEHIGH)s;
}
</style>
</noscript>


<!-- There's more CSS ahead (info popup): 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.
    ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    */



//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// SLIDESHOW
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~



// Global delay for timer loop, per-gallery configurable
var showDelayMS = %(SLIDESHOWDELAY)d;

// 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;

// No-op or alert or console.log
var trace = function(msg) {};



function hasSessionStore() {
    /*
    ------------------------------------------------------------------
    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.
    ------------------------------------------------------------------
    */
    try {
        sessionStorage.setItem('testkey', 'testval');
        return (sessionStorage.getItem('testkey') == 'testval');
    }
    catch (err) {
        return false;
    }
}



function autoClick() {
    /*
    ------------------------------------------------------------------
    [2.0] On toolbar Auto button clicks: toggle per-tab, cross-page
    slideshow state, and schedule or clear timer event to flip image.

    The cross-page state is used by both the current and following 
    viewer pages.  The show persists until cancelled by a toggle or
    the gallery is exited, and (in most contexts) is restarted on 
    gallery returns if on.  To loop: exiting a viewer page stops 
    the timer automatically, but new pages reschedule the timer on
    load if the toggle is on.  This is subject to the vagaries of
    browser back-forward caches, but 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.
    ------------------------------------------------------------------
    */
    if (hasSessionStore()) {
        // use HTML5 cross-page session storage where available (new, safari ok)
        trace('autoClick: using session storage');

        // if set and on: clear flag, cancel next timer event
        if (sessionStorage.getItem(toggleKey) == toggleOn) {
            sessionStorage.setItem(toggleKey, toggleOff);
            clearTimeout(showTimerID);
        }
        // if off or not set: start loop (reset by next page)
        else {
            sessionStorage.setItem(toggleKey, toggleOn);
            showTimerID = setTimeout(function() {navClick('%(NEXTPAGE)s');}, showDelayMS);
        }
    }
    else {
        // use cross-page window.name as a fallback (original, Safari fail) 
        trace('autoClick: using window.name');

        // if set and on: clear flag, cancel next timer event
        if (window.name == toggleOn) {
            window.name = toggleOff;
            clearTimeout(showTimerID);
        }
        // if off or not set: start loop (reset by next page)
        else if (window.name == toggleOff || window.name == '') {
            window.name = toggleOn;
            showTimerID = setTimeout(function() {navClick('%(NEXTPAGE)s');}, 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?).
    ------------------------------------------------------------------
    */
    var reschedule;
    trace('autoContinue');

    // on Safari Back stop, deferred: 'null::false'
    persisted = event ? event.persisted : 'none';
    trace(sessionStorage.getItem(toggleKey) +':'+ window.name +':'+ persisted);

    if (hasSessionStore()) {
        reschedule = (sessionStorage.getItem(toggleKey) == toggleOn);   // HTML5 storage
    }
    else {
        reschedule = (window.name == toggleOn);   // else fallback
    }

    if (reschedule) {
        trace('reschedule');
        showTimerID = setTimeout(function() {navClick('%(NEXTPAGE)s');}, showDelayMS);
    }
    else {
        trace('NOT resheduled');   // off, or Safari Back stop
    }
}



//
// Safari (only) work-around attempt: run timer reset code in a pageshow load-time
// event, not immediately/toplevel.  This was harmless in other browsers; for Safari, 
// it tried to both reset the show on a next page or Back return, and avoid Safari 
// Back-failure popup messages: "... Operation not permitted" (NSPOSIXErrorDomain:1)".
// THIS FAILED: 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.
//
/* 
try {
    // alternative
    trace('run timer reschedule deferred');
    window.addEventListener('pageshow', autoContinue);
}
catch (err) {
    // original
    trace('run timer reschedule immediately');
    autoContinue(null);    // run now in older browsers (or window.onpageshow)
}
*/

autoContinue(null);   // run on page load everywhere: deferral didn't help



//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// FULLSCREEN
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~



/*
----------------------------------------------------------------------
FULLSCREEN CAVEAT

JavaScript-initiated fullscreen mode is limited to a single page, 
and does nothing on some browsers - including iOS Safari and Chrome.
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 bowsers 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, and Hide 
Toolbar on iOS 13+).  Manual fullscreen doesn't work everywhere, 
but it probably 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 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"> 
----------------------------------------------------------------------
*/



//
// This 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> */
    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();
    }
}



function closeFullscreen() {                       /* documentElement not required */
    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();
    }
}



function isFullscreen() {
    /* any may be undefined: false */
    return document.fullscreenElement        ||    /* "standard" */
           document.webkitFullscreenElement  ||    /* some Chrome, Safari, Opera */
           document.mozFullScreenElement     ||    /* some Firefox */
           document.msFullscreenElement;           /* some IE/Edge */
}



function fullClick() {
    /*
    ------------------------------------------------------------------
    [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 would also 
    require catching (nonportable?) callbacks on user-invoked changes. 
    ------------------------------------------------------------------
    */
    if (!isFullscreen()) {
        openFullscreen();
    } 
    else {
        closeFullscreen();
    }
}



//
// Caveat: we can't force fullscreen for a next page here, because it 
// must be requested from a short-running, user-initiated event.  Hence,
// it lasts for one page only, where it works at all, and won't persist 
// for slideshows; it's useful anyhow as a zoom, but disable as desired.
//



//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// PLATFORM
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~



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.
    ------------------------------------------------------------------
    */

    // 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
}



// to test
/*
console.log(navigator.userAgent);
console.log('runningInSafari: '      + (runningInSafari() ? 'yes' : 'no'));
console.log('runningInSafariOniOS: ' + (runningInSafariOniOS() ? 'yes' : 'no'));
*/



//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// NAVIGATION
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~



function navClick(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. 
    ------------------------------------------------------------------
    */

    // 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() && ! %(CHROMEIOSBACKFIXED)s) {
        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)
}



//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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 
    uses a very 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).
    ------------------------------------------------------------------
    */
    var theimg  = document.getElementById('theimg');
    var domwide = theimg.width, domhigh = theimg.height;

    var popup = showInfo;  // [2.0] alert() no more... see above/ahead

    popup('%(IMAGETAKENKIND)s: %(IMAGETAKENDATE)s\n'  +          // Py: Exif (origin date)
          'Modified: %(IMAGEMODDATE)s\n' +                       // Py: OS
          'File size: %(IMAGESIZE)s bytes\n'  +                  // Py: OS
          'Image size: %(ORIGWIDE)sw x %(ORIGHIGH)sh\n' +        // Py: Pillow (also in DOM)
          'Display size: ' + domwide + 'w x ' + domhigh + 'h' +  // JS: DOM (dynamic)
          %(DEVICELINEORNOT)s)                                   // Py: Exif (device|software?)

    return false;   // don't try to follow a fake <a> href, if coded
}



//
// [2.0] See the rest of the implementation at 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?
    ------------------------------------------------------------------
    */
    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;
            if (debug) alert('usedHeight = ' + usedHeight);
        }
        else {
            usedHeight = defaultHeight;
            if (debug) alert('usedHeight defaulted: zero for element');
        }
    }
    catch (err) {
        usedHeight = defaultHeight;
        if (debug) alert('usedHeight defaulted: 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 (%(IOSSAFARILANDSCAPECSS)s &&
        runningInSafariOniOS() && viewportsize.width > viewportsize.height) {
        // sizes and UI munged: use version 1.5 CSS scaling
        theimg.style.width  = '%(IMAGEWIDE)s'; 
        theimg.style.height = '%(IMAGEHIGH)s';
    }
    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,
                            %(IMAGESTRETCH)s);
  
        // 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, etc.): search for "<script"
</script>




<!--
====================================================================================
Back to HTML: layout, inline CSS styles, CSS/JavaScript fodder/linkage
====================================================================================
-->



<title>%(IMAGENAME)s</title>
</head>

<!-- initial sizing, else opens fullsize ("...code..." = func) -->

<body onload="resizeImage();">



<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
<!-- JS NOTE: iff JavaScript not enabled, alert user                -->
<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->


<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();">
   %(IMAGENAME)s
</a>

<!-- DEAD CODE
<span style="cursor: pointer;" onclick='alert("...")'>
%(IMAGENAME)s
</span>
-->

</p></div>



<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
<!-- IMAGE: scaled to fit window/viewport (and 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 -->

<img id=theimg src="%(IMAGEPATH)s"
     class=cursorpointer
     onclick='window.location.href = "%(IMAGEPATH)s"'>

<!-- 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: presumably too small to overflow mobile viewport      -->
<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->


<!-- [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 -->

<div id=thetoolbar class=navdiv>     <!-- [1.7] id for space -->
<p>
<table class=navtable>
<tr>  

<!-- hrefs used if JS is disabled (or chrome+ios browser bug work-around used) -->
<!-- it may be nice to drop the url popups on mouseover, but hrefs cause them  -->

<td><a class=navlink 
       href="%(PREVPAGE)s"
       onclick="return navClick('%(PREVPAGE)s');">Prev</a></td>

<td><a class=navlink 
       href="%(NEXTPAGE)s"
       onclick="return navClick('%(NEXTPAGE)s');">Next</a></td>

<td><a class=navlink 
       href="../index.html">Index</a></td>

<td><a class="navlink"
       href="#toggle slideshow"
       onclick="autoClick(); return false">Auto</a></td>    <!-- [2.0] new, tag=info -->

<td class=optional id="fullBtn"><a class="navlink"
       href="#toggle fullscreen"
       onclick="fullClick(); return false">Full</a></td>    <!-- [2.0] new, tag=info -->

<script>
//
// show optional buttons now; here iff JS enabled;
// doing same for Auto failed for unknown reasons;
//
if (%(FULLSCREENBUTTON)s) {
    document.getElementById("fullBtn").style.display = "block";
}
</script>

<!-- DEAD CODE --> 
<!-- [2.0] raw display is now image-tap only (see also filename-tap info)
<td><a class=navlink 
       href="%(IMAGEPATH)s">Raw</a></td>
-->

</tr></table>
</p>             <!-- the /p adds space below links bar (oddly) -->
</div>



<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
<!-- 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.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''-->

<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 */
    overflow: auto;          /* enable full-page vertical scrolls, if needed */
    height: 100%%;
    background-color: rgb(0, 0, 0);         /* fallback color: blank bg */
    background-color: rgba(0, 0, 0, 0.4);   /* black, with opacity: dim bg */
}

/* Modal-dialog box: text + button content on page */
.InfoBox {
    margin: 8px auto;        /* offset from top, and centered (or: 4%%) */
    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%%;
    max-width: 400px;        /* but not too wide on desktop or landscape */
}

/* Modal-dialog message: preformatted text in box */
.InfoText {
    overflow: auto;          /* enable text-area horizontal scrolls, if needed */
}

/* Modal-dialog close: OK button in box */
.InfoClose {
    display: block;
    margin: auto;            /* centered in box */
    margin-bottom: 6px;      /* but hug the box bottom */
}

.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>&times;</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>&nbsp;OK&nbsp;</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 */

// 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() {
    infodisplay.style.display = 'none';       // hide/close display
}

// On clicks outside the box, close entire modal overlay
window.onclick = function(event) {
    if (event.target == infodisplay) {        // if tap bg, not box
        infodisplay.style.display = 'none';
    }
} 

// On filename clicks, open the modal display (called above, formerly alert())
function showInfo(message) {
    infotext.innerHTML = message;
    infodisplay.style.display = 'block';      // show/popup display
}

</script>


</body>
</html>



[Home] Books Programs Blog Python Author Training Search Email ©M.Lutz