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_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()">✖</div>
<div class="button buttonLeft" onclick="slide.changeSlide(-1)">❮</div>
<div class="button buttonRight" onclick="slide.changeSlide(1)">❯</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
// 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);
}
}