SlideView - Image Slideshow [PHP JavaScript CSS]

Download Source Code

SlideView.20200108-2037.zip [Downloads: 1122]
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($dir, string $css = '', int $forceID = 0): string
    {
        // $dir can be a path to a folder with images to read
        if (is_string($dir)) {
            if (!$files = scandir($dir, SCANDIR_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 > 0 ? $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) > 0 ? ' '.$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($path, ENT_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($filename, PATHINFO_FILENAME);
            $ext = pathinfo($filename, PATHINFO_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($ext, Self::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

tyle="color: #FF8000">// ======================================================================== // 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 }, {passive: true}); } // ==================================================================== // 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), {passive: false}); // Resize listener: setting custom --vh property window.addEventListener('resize', this.resizeListener.bind(this), {passive: true}); // Orientation change causes stretched image, this seems to help in some cases window.addEventListener('orientationchange', () => { try { this.viewEngine(this.slideIndex); } catch (err) {} // Ignore }, {passive: true}); } // ==================================================================== // 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 src, img; // Fill slides container with new images (though not loading them) for (let i = 0; i < srcs.length; i++) { // 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(id, index) { // 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(event, id, index) { if (event.key == ' ' || event.key == 'Enter') { this.openView(id, index); 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(id, index); } // ==================================================================== // Set/replace query parameter without navigating. // -------------------------------------------------------------------- static setURLQueryParam(param, value) { const q = new URLSearchParams(window.location.search); q.set(param, value); 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), {passive: true}); this.img.addEventListener('error', this.imgError.bind(this), {passive: true}); } // ==================================================================== // Get filename of the image. // -------------------------------------------------------------------- getFilename() { return this.src.substr(this.src.lastIndexOf('/')+1, this.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, {passive: false}); slideArea.addEventListener('touchstart', this.touchStartRef, {passive: true}); slideArea.addEventListener('touchmove', this.touchMoveRef, {passive: true}); slideArea.addEventListener('touchend', this.touchEndRef, {passive: true}); slideArea.addEventListener('touchcancel', this.touchCancelRef, {passive: true}); } // ==================================================================== // 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 d = ~~(x - 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 = x - this.startX; // Re-enable transition this.slideShow.setTransition(true); // A valid swipe? if (Math.abs(distX) > this.threshold) { this.slideShow.changeSlide((distX < 0) ? 1 : -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, {passive: true}); this.slideArea.addEventListener('mouseup', this.mouseUpRef, {passive: true}); this.slideArea.addEventListener('mouseout', this.mouseUpRef, {passive: true}); 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, {passive: true}); this.slideArea.removeEventListener('mouseup', this.mouseUpRef, {passive: true}); this.slideArea.removeEventListener('mouseout', this.mouseUpRef, {passive: true}); 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); } }