SlideView - Image Slideshow [PHP JavaScript CSS]

Download Source Code

SlideView.20200108-2037.zip [Downloads: 671]
SHA1: f16432c2b3e9fbb333e857da826517a05631030f

SlideView uses PHP to read all image files in a folder and show them as previews prepared to be displayed in a full screen slideshow when clicked. Alternatively to reading all files an array of paths can be used to show only specific images or if the images are spread across multiple folders. When the full screen view is shown JavaScript is responsible for making the images navigable by touch gestures or mouse or keyboard interaction with a nice slide effect.

If you are curious SlideView can be seen in action many places on this website!

It's easy to add multiple SlideViews on the same page and each show needs an ID to identify it. By default an ID will automatically be assigned but one can also be specified manually. It's recommendable to manually specify an ID if you expect users to share or use direct links. A direct link is obtained simply by copying the browser's address line when the desired image is opened. Example link: http://www.example.com/path/?slide=1&img=3. The first number is the slideshow 's ID and the other number is the image index in that particular slideshow.

Preview Images

To make loading of previews faster it's recommend to make a miniature version of the image and append _small to its filename. E.g. if an image is 1280 x 960 px the preview could be 640 x 480 px or even smaller. The small version will automatically be used for previews when reading a folder. If manually feeding getSlides() with paths then point to the small versions. In both cases the big version will automatically be used when opening full screen view.

Previews (_small)

img01.jpg
img01_small.jpg
img02.jpg
img02_small.jpg
img03.jpg
img03_small.jpg

Note: filenames must only contain one occurrence of _small and it must be the last part of the filename just before the dot and extension.

When showing a specific SlideView you can specify an additional CSS class for that particular show if necessary.

Enjoy!

Source Code

SlideView.php

<?php
// ========================================================================
// SlideView: Image Slideshow with PHP JavaScript CSS
// Version: 2020-01-08 20:37 (UTC+01:00)
// https://www.dunweber.com/docs/scripts/releases/slideshow
// Copyright (C) Christian L. Dünweber
// This program is licensed under the MIT License:
// https://opensource.org/licenses/MIT
// ------------------------------------------------------------------------
// Usage:
// require_once('SlideView.php');
// $slideView = new SlideView();
// echo $slideView->getSlides('path/dir1');        // First call, gets ID 1
// echo $slideView->getSlides('path/dir2', '', 7); // Next call, force ID 7
// echo $slideView->getSlides(['img1','img2']);    // Specific images only
// Direct link to image: http://example.com/path/?slide=1&img=3
// ========================================================================

class SlideView
{
    
// Auto assigned ID for multiple shows if not specifying a positive $forceID
    
private $slideID 1;

    
// First call (with multilple shows) to only show slideShow container once
    
private $firstCall true;

    
// File extensions considered images
    
private const IMGTYPES = ['jpg','jpeg','png','gif','tif','tiff','ico','bmp'];

    
// ====================================================================
    // If $dir is a path all contained images are read.
    // If $dir is an array it must contain paths to images (e.g. to show
    // a selection of a larger set or for custom sorting).
    // In either case the images will be shown as previews and the
    // slideshow prepared for showing upon clicking a preview.
    // A custom css class can be specified to modify or add style.
    // If an integer ID is not specified one will be auto-assigned.
    // --------------------------------------------------------------------
    
public function getSlides($dirstring $css ''int $forceID 0): string
    
{
        
// $dir can be a path to a folder with images to read
        
if (is_string($dir)) {
            if (!
$files scandir($dirSCANDIR_SORT_ASCENDING)) {
                return 
'<p>Couldn\'t open: <em>'.$dir.'</em></p>';
            }

            
$imgs $this->getImages($dir$files);
            
$dir .= '/';
        }
        
// Alternatively $dir is an array of paths to images
        
else if (is_array($dir)) {
            
$imgs $dir;
            
$dir '';
        }
        else {
            return 
'<p>Unknown input type: <em>'.gettype($dir).'</em></p>';
        }

        
// Use given ID else auto-assign
        
$this->slideID $forceID $forceID $this->slideID;

        
$previews '';
        
$slideArea '';

        for (
$i 0$i count($imgs); $i++) {
            
$previews .= '<img class="imgSrcs'.$this->slideID.'"'.
                
' onclick="slide.openView('.$this->slideID.','.($i+1).')"'.
                
' onkeydown="slide.openPressed(event,'.$this->slideID.','.($i+1).')"'.
                
' src="'.$dir.$imgs[$i].'" tabindex="0" alt="" />';
        }

        
$previews '<div class="previews'.(strlen($css) > ' '.$css '').'">'.$previews.'</div>';

        
// Only show slideShow container for first call as it's reused for all shows
        
if ($this->firstCall) {
            
$slideArea $this->getSlideShow();
            
$this->firstCall false;
        }

        
$this->slideID++;
        return 
'<div class="slideView">'.$slideArea.$previews.'</div>';
    }

    
// ====================================================================
    // Get the slideshow full screen view container and reference to the
    // JavaScript script running the show. Is initially hidden.
    // --------------------------------------------------------------------
    
private function getSlideShow(): string
    
{
        
$path $this->getScriptWebPath();
        
$path htmlspecialchars($pathENT_QUOTES|ENT_SUBSTITUTE|ENT_HTML5'UTF-8'true);

        return
        
'<script src="'.$path.'/SlideView.js"></script>
        <script>const slide = new SlideView("'
.$path.'");</script>
        <div id="slideShow" tabindex="0" style="display:none;">
            <div id="slideArea">
                <div id="slides"></div>
            </div>
            <div class="button" id="slideCount"></div>
            <div class="button" id="slideInfo"></div>
            <div class="button buttonClose" onclick="slide.closeView()">&#10006;</div>
            <div class="button buttonLeft" onclick="slide.changeSlide(-1)">&#10094;</div>
            <div class="button buttonRight" onclick="slide.changeSlide(1)">&#10095;</div>
        </div>'
."\n";
    }

    
// ====================================================================
    // Filters images in $files and returns their paths in a new array.
    // --------------------------------------------------------------------
    
private function getImages(string $dir, array $files): array
    {
        
$imgs = [];
        
$path realpath($dir).DIRECTORY_SEPARATOR;

        for (
$i 0$i count($files); $i++) {
            
$filename $files[$i];
            
$name pathinfo($filenamePATHINFO_FILENAME);
            
$ext pathinfo($filenamePATHINFO_EXTENSION);

            
// Only image files
            
if (is_file($path.$filename) && $this->isImage($ext)) {
                
// If a _small is provided it's used for preview instead of the big version
                
$small $name.'_small.'.$ext;

                if (
is_file($path.$small)) {
                    
$imgs[] = $small;
                    
$i++; // Skip next as it in fact is the _small
                
}
                else {
                    
// Read all images even without corresponding _small
                    
$imgs[] = $filename;
                }
            }
        }

        return 
$imgs;
    }

    
// ====================================================================
    // Check if file extension indicates an image.
    // --------------------------------------------------------------------
    
private function isImage(string $ext): bool
    
{
        for (
$i 0$i count(Self::IMGTYPES); $i++) {
            if (
strcasecmp($extSelf::IMGTYPES[$i]) === 0) {
                return 
true;
            }
        }

        return 
false;
    }

    
// ====================================================================
    // Get absolute web path to this folder, where the script resides.
    // --------------------------------------------------------------------
    
private function getScriptWebPath(): string
    
{
        return 
str_replace('\\''/'mb_substr(__DIR__mb_strlen(realpath($_SERVER['DOCUMENT_ROOT']))));
    }
}

?>

SlideView.js

// ========================================================================
// SlideView: Image Slideshow with PHP JavaScript CSS
// Version: 2020-01-08 20:37 (UTC+01:00)
// https://www.dunweber.com/docs/scripts/releases/slideshow
// Copyright (C) Christian L. Dünweber
// This program is licensed under the MIT License:
// https://opensource.org/licenses/MIT
// ------------------------------------------------------------------------
// Usage: SlideView.php
// ========================================================================

class SlideView
{
    
// ====================================================================
    // Constructor: Init and open image given in the query.
    // Specify dir: path to the folder containing the stylesheet.
    // --------------------------------------------------------------------
    
constructor(dir)
    {
        
SlideView.addStylesheet(dir+'/SlideView.css');

        
window.addEventListener('DOMContentLoaded', () => {
            
this.init();
            
this.showFromQuery(); // Opens image given in the query if any
        
}, {passivetrue});
    }

    
// ====================================================================
    // Initialize elements.
    // --------------------------------------------------------------------
    
init()
    {
        
this.id// ID of slideshow (enabling multiple on same page)
        
this.imgs = []; // Array of images for current slideshow
        
this.slideIndex 0// Index of currently shown image (in imgs)

        // Get GUI elements
        
this.slideShow document.getElementById('slideShow');
        
this.slideInfo document.getElementById('slideInfo');
        
this.slideCount document.getElementById('slideCount');
        
this.slideArea document.getElementById('slideArea');
        
this.slides document.getElementById('slides');

        
// Slide listener: changing slide on touch and mouse swipes
        
new SlideListener(this);

        
// Key listener: changing slide on keyboard shortcuts
        
this.slideShow.addEventListener('keydown'this.keyChangeSlide.bind(this), {passivefalse});

        
// Resize listener: setting custom --vh property
        
window.addEventListener('resize'this.resizeListener.bind(this), {passivetrue});
        
        
// Orientation change causes stretched image, this seems to help in some cases
        
window.addEventListener('orientationchange', () => {
            try {
                
this.viewEngine(this.slideIndex);
            }
            catch (
err) {} // Ignore
        
}, {passivetrue});
    }

    
// ====================================================================
    // Get and store all images in slideshow given by id.
    // --------------------------------------------------------------------
    
initSlides(id)
    {
        
this.id id;
        
this.imgs.length 0// Empty array of image objects
        
this.slides.innerHTML ''// Empty slides container

        
const srcs document.getElementsByClassName('imgSrcs'+id);
        
let srcimg;

        
// Fill slides container with new images (though not loading them)
        
for (let i 0srcs.lengthi++) {
            
// Use big version of image if exists (filename, incl. path, must contain only one occurence of "_small")
            
src srcs[i].src;
            
src src.replace('_small''');
            
img = new SlideImage(src);
            
this.imgs.push(img);
            
this.slides.appendChild(img.element);
        }
    }

    
// ====================================================================
    // Open full screen slideshow with id and image given by index.
    // --------------------------------------------------------------------
    
openView(idindex)
    {
        
// Store the element in focus to restore when closing
        
this.wasInFocus document.activeElement;

        
// Prevent scrolling in the html element when in full screen view
        
document.documentElement.classList.add('slideViewOpen');
        
        
// Init specific slideshow
        
this.initSlides(id);

        
// Show full screen view and load specified image
        
this.slideShow.style.display 'block';
        
this.viewEngine(index-1); // Index starts at zero

        // Trigger transition for a little fanciness when opening
        
this.setTransition(false);
        
this.slides.classList.add('openTransition');
        
this.slides.offsetHeight// Trigger reflow
        
this.setTransition(true);
        
this.slides.classList.remove('openTransition');
    }

    
// ====================================================================
    // Close full screen view.
    // --------------------------------------------------------------------
    
closeView()
    {
        
// Hide full screen view
        
this.slideShow.style.display 'none';

        
// Restore scrolling
        
document.documentElement.classList.remove('slideViewOpen');

        
// Reset address bar
        
history.replaceState(null''location.href.split("?")[0]);

        
// Restore focus
        
this.wasInFocus.focus();
    }

    
// ====================================================================
    // Change slide with n increments, usually -1 or 1 for left and right.
    // --------------------------------------------------------------------
    
changeSlide(n)
    {
        
this.viewEngine(this.slideIndex += n);
    }

    
// ====================================================================
    // Shows the image given by index transitioning in from given direction.
    // Update info in the slideshow status bar and browser address bar.
    // --------------------------------------------------------------------
    
viewEngine(index)
    {
        
// Make the slideshow circular so it starts over in either end
        
this.slideIndex this.limitIndex(index);

        
// Load image and move to its position
        
this.imgs[this.slideIndex].load();
        
this.setLeft((-this.slideIndex 100)+'%');

        
// Set info
        
const filename this.imgs[this.slideIndex].getFilename();
        
this.slideInfo.innerHTML filename;
        
this.slideCount.innerHTML = (this.slideIndex 1)+' of '+this.imgs.length;

        
// Make address bar show current image so page can be reloaded and a direct link can be copied
        
SlideView.setURLQueryParam('slide'this.id);
        
SlideView.setURLQueryParam('img',  this.slideIndex+1);
        
this.slideShow.focus(); // Set focus for onkeydown to work

        // Preload previous and next images
        
this.imgs[this.limitIndex(this.slideIndex-1)].load();
        
this.imgs[this.limitIndex(this.slideIndex+1)].load();
    }

    
// ====================================================================
    // Set position of the slide element. For showing specific image and
    // animation when swiping.
    // --------------------------------------------------------------------
    
setLeft(val)
    {
        
this.slides.style.left val;
    }

    
// ====================================================================
    // Get current pixel position of the slide element.
    // --------------------------------------------------------------------
    
getLeft()
    {
        return 
this.slides.getBoundingClientRect().left;
    }

    
// ====================================================================
    // Enable or disable CSS tranistion for slide animation.
    // --------------------------------------------------------------------
    
setTransition(state)
    {
        if (
state) {
            
this.slides.classList.remove('noTransition');
        }
        else {
            
this.slides.classList.add('noTransition');
        }
    }

    
// ====================================================================
    // Make sure index is in range.
    // --------------------------------------------------------------------
    
limitIndex(index)
    {
        if (
index this.imgs.length 1) {
            return 
this.imgs.length 1;
        }

        if (
index 0) {
            return 
0;
        }

        return 
index;
    }

    
// ====================================================================
    // Change slide by arrow keys. Escape closes view and F5 reloads.
    // All other keys are disabled when SlideView is open.
    // --------------------------------------------------------------------
    
keyChangeSlide(event)
    {
        switch (
event.key) {
            case 
'ArrowLeft':
                
this.changeSlide(-1);
                break;
            case 
'Left'// IE is special
                
this.changeSlide(-1);
                break;
            case 
'ArrowRight':
                
this.changeSlide(1);
                break;
            case 
'Right'// IE is special
                
this.changeSlide(1);
                break;
            case 
'F5'// Reload from the server
                
location.reload(true);
                break;
            case 
'Escape':
                
this.closeView();
                break;
        }
        
event.preventDefault();
    }

    
// ====================================================================
    // Open view for link in focus when pressing space bar or Enter.
    // --------------------------------------------------------------------
    
openPressed(eventidindex)
    {
        if (
event.key == ' ' || event.key == 'Enter') {
            
this.openView(idindex);
            
event.preventDefault();
        }
    }

    
// ====================================================================
    // Catch direct link to a specific image from the query and show it.
    // --------------------------------------------------------------------
    
showFromQuery()
    {
        const 
id parseInt(SlideView.getURLQueryParam('slide'), 10);
        const 
index parseInt(SlideView.getURLQueryParam('img'), 10);

        if (
isNaN(id) || isNaN(index)) {
            return;
        }

        
this.openView(idindex);
    }

    
// ====================================================================
    // Set/replace query parameter without navigating.
    // --------------------------------------------------------------------
    
static setURLQueryParam(paramvalue)
    {
        const 
= new URLSearchParams(window.location.search);
        
q.set(paramvalue);
        const 
newPathQ window.location.pathname '?' q.toString();
        
history.replaceState(null''newPathQ);
    }

    
// ====================================================================
    // Get specified param from the query if it exists.
    // --------------------------------------------------------------------
    
static getURLQueryParam(param)
    {
        return 
decodeURIComponent((location.search.split(param+'=')[1]||'').split('&')[0])
    }

    
// ====================================================================
    // Adds the stylesheet located at url to the document's head.
    // --------------------------------------------------------------------
    
static addStylesheet(url)
    {
        const 
link document.createElement('link');
        
link.rel 'stylesheet';
        
link.media 'all';
        
link.href url;
        
document.getElementsByTagName('head')[0].appendChild(link);
    }

    
// ====================================================================
    // Resize listener: Set view port height custom property --vh.
    // Used to obtain correct viewport height in mobile browers where the
    // address bar height is unaccounted for.
    // --------------------------------------------------------------------
    
resizeListener()
    {
        const 
vh window.innerHeight 0.01;
        
document.documentElement.style.setProperty('--vh', `${vh}px`);
    }
}

// ========================================================================
// Image object representing one image. Will show spinning animation when
// loading an image and give an error message if couldn't load.
// ------------------------------------------------------------------------
class SlideImage
{
    
// ====================================================================
    // Constructor: Init all elements and default to showing spinner.
    // --------------------------------------------------------------------
    
constructor(src)
    {
        
// Save src but do not set before calling load
        
this.src src;

        
// Container for spinner, error message, or the actual image
        
this.element document.createElement('div');
        
this.element.className 'imgContainer';

        
this.img = new Image();
        
this.img.className 'slideImg';

        
this.loader document.createElement('div');
        
this.loader.className 'loader';

        
this.error document.createElement('div');
        
this.error.className 'imgError';
        
this.error.innerHTML 'Could not load image.';

        
// Show loader per default
        
this.element.appendChild(this.loader);

        
this.img.addEventListener('load'this.imgLoaded.bind(this), {passivetrue});
        
this.img.addEventListener('error'this.imgError.bind(this), {passivetrue});
    }

    
// ====================================================================
    // Get filename of the image.
    // --------------------------------------------------------------------
    
getFilename()
    {
        return 
this.src.substr(this.src.lastIndexOf('/')+1this.src.length-1);
    }

    
// ====================================================================
    // Load the image by setting src (transfer from server or read from cache).
    // --------------------------------------------------------------------
    
load()
    {
        
this.img.src this.src;
    }

    
// ====================================================================
    // When successfully loaded the spinner is removed and image is added.
    // --------------------------------------------------------------------
    
imgLoaded()
    {
        
this.element.innerHTML '';
        
this.element.appendChild(this.img);
    }

    
// ====================================================================
    // On error the spinner is removed and an error message added.
    // --------------------------------------------------------------------
    
imgError()
    {
        
this.element.innerHTML '';
        
this.element.appendChild(this.error);
    }
}

// ========================================================================
// Handling swipes, either by touch or mouse, to navigate the slideshow.
// ------------------------------------------------------------------------
class SlideListener
{
    
// ====================================================================
    // Constructor: Init and add EventListeners.
    // --------------------------------------------------------------------
    
constructor(slideShow)
    {
        
this.slideShow slideShow// SlideView class ref
        
this.slideArea slideShow.slideArea// Element where sliding happens
        
        
this.threshold 50// Min. x distance moved in px required to change slide
        
this.pinching false// Flag indicating if more than one finger is active for allowing pinch zoom
        
this.curPos 0// Position of active image when starting motion (in case of starting new motion before transitionend has occured)
        
this.startX 0// Position of finger when starting motion

        // Note: bind() results in new function ref.
        
this.mouseDownRef this.mouseDown.bind(this);
        
this.mouseMoveRef this.mouseMove.bind(this);
        
this.mouseUpRef this.mouseUp.bind(this);
        
this.touchStartRef this.touchStart.bind(this);
        
this.touchMoveRef this.touchMove.bind(this);
        
this.touchEndRef this.touchEnd.bind(this);
        
this.touchCancelRef this.resetPos.bind(this);

        
slideArea.addEventListener('mousedown'this.mouseDownRef, {passivefalse});
        
slideArea.addEventListener('touchstart'this.touchStartRef, {passivetrue});
        
slideArea.addEventListener('touchmove'this.touchMoveRef, {passivetrue});
        
slideArea.addEventListener('touchend'this.touchEndRef, {passivetrue});
        
slideArea.addEventListener('touchcancel'this.touchCancelRef, {passivetrue});
    }

    
// ====================================================================
    // Start motion: get current position and start coordinate.
    // --------------------------------------------------------------------
    
startMove(x)
    {
        
// Disable transition for smooth sliding
        
this.slideShow.setTransition(false);

        
// Record current position and start coordinate
        
this.curPos this.slideShow.getLeft();
        
this.startX x;
    }
  
    
// ====================================================================
    // Moving: calculate distance from start coordinate and move slideshow.
    // --------------------------------------------------------------------
    
moving(x)
    {
        
// Distance traveled, positive or negative, integer rounded
        
const = ~~(this.startX);

        
// Move the slideshow
        
this.slideShow.setLeft((this.curPos d)+'px');
    }
      
    
// ====================================================================
    // End movement: if    the motion was long enough (i.e. considered a
    // valid swipe) the slide is changed left or right with transition.
    // --------------------------------------------------------------------
    
endMove(x)
    {
        
// Final distance
        
const distX this.startX;

        
// Re-enable transition
        
this.slideShow.setTransition(true);

        
// A valid swipe?
        
if (Math.abs(distX) > this.threshold) {
            
this.slideShow.changeSlide((distX 0) ? : -1);
        }
        else {
            
this.resetPos();
        }
    }

    
// ====================================================================
    // Reset position if motion was not a valid swipe.
    // --------------------------------------------------------------------
    
resetPos()
    {
        
this.slideShow.setLeft(this.curPos+'px');
    }

    
// ====================================================================
    // Mouse button down can start a slide motion. Add mouse listeners.
    // --------------------------------------------------------------------
    
mouseDown(e)
    {
        
e.preventDefault();

        
this.slideArea.addEventListener('mousemove'this.mouseMoveRef, {passivetrue});
        
this.slideArea.addEventListener('mouseup'this.mouseUpRef, {passivetrue});
        
this.slideArea.addEventListener('mouseout'this.mouseUpRef, {passivetrue});

        
this.startMove(e.pageX);
    }

    
// ====================================================================
    // Mouse being dragged.
    // --------------------------------------------------------------------
    
mouseMove(e)
    {
        
this.moving(e.pageX);
    }

    
// ====================================================================
    // Mouse button released stops a slide motion. Remove mouse listeners.
    // --------------------------------------------------------------------
    
mouseUp(e)
    {
        
this.slideArea.removeEventListener('mousemove'this.mouseMoveRef, {passivetrue});
        
this.slideArea.removeEventListener('mouseup'this.mouseUpRef, {passivetrue});
        
this.slideArea.removeEventListener('mouseout'this.mouseUpRef, {passivetrue});

        
this.endMove(e.pageX);
    }

    
// ====================================================================
    // Touch down can start a slide movement.
    // --------------------------------------------------------------------
    
touchStart(e)
    {
        
this.pinching e.touches.length 1// Allow browser's native pinch zoom, stop sliding for multiple fingers

        
if (!this.pinching) {
            
this.startMove(e.touches[0].pageX);
            
//e.preventDefault();
        
}
    }

    
// ====================================================================
    // Finger being dragged.
    // --------------------------------------------------------------------
    
touchMove(e)
    {
        if (!
this.pinching) {
            
this.moving(e.touches[0].pageX);
        }
    }

    
// ====================================================================
    // Finger released stops a slide motion.
    // --------------------------------------------------------------------
    
touchEnd(e)
    {
        if (
this.pinching && e.touches.length == 1) {
            
this.pinching false;
            return;
        }

        
this.endMove(e.changedTouches[0].pageX);
    }
}