%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /home/lightco1/www/media/sigplus/engines/slideplus/js/
Upload File :
Create Path :
Current File : /home/lightco1/www/media/sigplus/engines/slideplus/js/slideplus.js

/**@license slideplus image rotator
* @author  Levente Hunyadi
* @version 1.5.0
* @remarks Copyright (C) 2011-2017 Levente Hunyadi
* @see     http://hunyadi.info.hu/projects/slideplus
**/

/*
* slideplus image rotator
* Copyright 2011-2017 Levente Hunyadi
*
* slideplus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* slideplus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with slideplus.  If not, see <http://www.gnu.org/licenses/>.
*/

/*{"compilation_level":"ADVANCED_OPTIMIZATIONS"}*/
'use strict';

/**
* Unit the slider advances in response to navigation buttons Previous or Next.
* @enum {string}
*/
const SlidePlusStep = {
	Single: 'single',
	Page: 'page'
};

/**
* Orientation of the sliding image strip.
* @enum {string}
*/
const SlidePlusOrientation = {
	Horizontal: 'horizontal',
	Vertical: 'vertical'
};

/**
* Layout of the sliding image strip.
* @enum {string}
*/
const SlidePlusLayout = {
	Natural: 'natural',
	Row: 'row',
	Column: 'column'
};

/**
* Position of caption with respect to container slot.
* @enum {string}
*/
const SlidePlusPosition = {
	Hidden: 'hidden',
	Above: 'above',
	Top: 'top',
	Bottom: 'bottom',
	Below: 'below'
};

/**
* Text writing system.
* @enum {string}
*/
const SlidePlusWritingSystem = {
	LeftToRight: 'ltr',
	RightToLeft: 'rtl'
};

/**
* Options for the slideplus slider engine.
* The object has the following properties:
* + rows: The number of rows per slider page.
* + cols: The number of columns per slider page.
* + step: Unit the slider advances in response to navigation buttons Previous or Next ['single'|'page'].
* + loop: Whether the rotator loops around in a circular fashion [false|true].
* + random: Whether the rotator randomizes the order of images on startup [false|true].
* + orientation: Orientation of the sliding image strip ['horizontal'|'vertical'].
* + layout: Layout of the sliding image strip ['natural'|'row'|'column'].
* + links: Whether to show navigation links [true|false].
* + counter: Whether to show page counter [true|false].
* + protection: Whether the context menu appears when right-clicking an image [true|false].
* + delay: Time between successive automatic slide steps [ms], or 0 to disable automatic sliding.
* + dir: Text writing system, left-to-right or right-to-left ['ltr'|'rtl'].
*
* @typedef {{
*     rows: number,
*     cols: number,
*     step: SlidePlusStep,
*     loop: boolean,
*     random: boolean,
*     orientation: SlidePlusOrientation,
*     layout: SlidePlusLayout,
*     captions: SlidePlusPosition,
*     links: boolean,
*     counter: boolean,
*     protection: boolean,
*     delay: number,
*     dir: SlidePlusWritingSystem
* }}
*/
let SlidePlusOptions;

/** @type {SlidePlusOptions} */
const slidePlusDefaults = {
	'rows': 2,
	'cols': 3,
	'step': SlidePlusStep.Page,
	'loop': false,
	'random': false,
	'orientation': SlidePlusOrientation.Horizontal,
	'layout': SlidePlusLayout.Natural,
	'captions': SlidePlusPosition.Bottom,
	'links': true,
	'counter': true,
	'protection': false,
	'delay': 0,
	'dir': SlidePlusWritingSystem.LeftToRight,
	'lazyload': true
};

/**
* @param {!Element} elem
* @param {!Array<string>} attributes
*/
function moveAttributesToDataset(elem, attributes) {
	attributes.forEach(function (name) {
		if (elem.hasAttribute(name)) {
			const dataname = 'data-' + name;
			const value = elem.getAttribute(name);
			elem.setAttribute(dataname, value);
			elem.removeAttribute(name);
		}
	});
}

/**
* @param {!Element} elem
* @param {!Array<string>} attributes
*/
function moveAttributesFromDataset(elem, attributes) {
	attributes.forEach(function (name) {
		const dataname = 'data-' + name;
		if (elem.hasAttribute(dataname)) {
			const value = elem.getAttribute(dataname);
			elem.setAttribute(name, value);
			elem.removeAttribute(dataname);
		}
	});
}

/**
* Pre-loads one or more images in a container element.
* This function injects a user-friendly progress indicator, registers ready callbacks to be triggered when all images
* in the container have finished loading, and hides the pre-loader icon when all images are available.
*
* While the image is being loaded, the following HTML is injected into the container element:
* ```
* <div class="loadplus-progress"></div>
* ```
*
* @constructor
* @param {!HTMLElement} elem The container element in which image elements are to be pre-loaded.
*/
function Preloader(elem) {
	let self = this;

	/** @type {!HTMLElement} */
	this.host = elem;

	/** @type {!Array<!HTMLImageElement>} */
	let pending = [].filter.call(/** @type {!IArrayLike<!HTMLImageElement>} */ (elem.getElementsByTagName('img')), function (/** @type {!HTMLImageElement} */ image) {
		return !image.complete && image.src;
	});
	/** @type {!Array<!HTMLImageElement>} */
	this.pending = pending;

	if (pending.length < 1) {
		return;
	}

	// inject animated loader icon
	let icon = /** @type {!HTMLElement} */ (document.createElement('div'));
	icon.classList.add('loadplus-progress');
	elem.appendChild(icon);
	/** @type {!HTMLElement} */
	this.icon = icon;

	pending.forEach(function (image) {
		// hide image to prevent browser from displaying a partial image or broken image icon while data is being transferred
		image.classList.add('loadplus-hidden');

		// move native image attributes to data attributes to prevent the browser from fetching the image
		moveAttributesToDataset(image, ['src','srcset','sizes']);

		// add back-reference to pre-loader instance to allow other scripts to trigger the pre-loader mechanism
		image['preloader'] = self;
	});
}
Preloader.prototype.load = function () {
	let self = this;

	this.pending.forEach(function (image) {
		if (!image.src) {  // make sure the method is reentrant
			image.addEventListener('load', function () {
				image.classList.remove('loadplus-hidden');

				// remove image from list of images waiting to be loaded
				const index = self.pending.indexOf(image);
				if (index >= 0) {
					self.pending.splice(index, 1);
				}

				// check if there are further images in the container pending
				if (self.pending.length == 0) {
					// remove animated loader icon
					let parent = self.icon.parentNode;
					if (parent) {
						parent.removeChild(self.icon);
					}
				}
			});

			// re-apply previously cleared native image attributes by reading data attributes
			moveAttributesFromDataset(image, ['src','srcset','sizes']);

			// remove back-reference to pre-loader instance
			delete image['preloader'];
		}
	});
};
Preloader.prototype['load'] = Preloader.prototype.load;

/**
* Sets all undefined properties on an object using a reference object.
* @param {Object|null|undefined} obj
* @param {!Object} ref
* @return {!Object}
*/
function applyDefaults(obj, ref) {
	/** @type {!Object} */
	let extended = obj || {};
	for (const prop in /** @type {!Object} */ (JSON.parse(JSON.stringify(ref)))) {  // use JSON functions to clone object
		extended[prop] = /** @type {*} */ (extended[prop]) || /** @type {*} */ (ref[prop]);
	}
	return extended;
}

/**
* Fisher-Yates (aka Knuth) unbiased shuffle algorithm.
* @param {!Array<T>} array
* @return {!Array<T>}
* @template T
*/
function shuffle(array) {
	/** @type {number} */
	let currentIndex = array.length;

	while (0 !== currentIndex) {  // while there remain elements to shuffle
		// pick a remaining element
		/** @type {number} */
		let randomIndex = Math.floor(Math.random() * currentIndex);
		currentIndex -= 1;

		// swap element with the current element
		let temporaryValue = array[currentIndex];
		array[currentIndex] = array[randomIndex];
		array[randomIndex] = temporaryValue;
	}

	return array;
}

/**
* @param {!Element} element
*/
function resetPosition(element) {
	['left','right','top','bottom'].forEach(function (/** @type {string} */ dir) {
		element.style.removeProperty(dir);
	});
}

/**
* Removes all elements in a collection from the DOM tree.
* @param {Array<!Element>} items The items to remove.
*/
function removeElementsFromDOM(items) {
	if (items) {
		items.forEach(function (item) {
			let parent = item.parentNode;
			if (parent) {
				parent.removeChild(item);
			}
		});
	}
}

/**
* Clones all items in a collection.
* @param {!Array<!Element>} items An array of elements to duplicate.
* @return {!Array<!Element>} An array of duplicated elements.
*/
function cloneElements(items) {
	return items.map(function (item) {
		return item.cloneNode(true);
	});
}

function generateAnimationStyleDeclarations() {
	/**
	* @param {string} dir
	* @param {number} x
	* @param {number} y
	* @param {number} frac
	* @return {string}
	*/
	function generateKeyFrame(dir, x, y, frac) {
		return '@keyframes slideplus-push-' + dir + '-' + frac + '{' +
			'0%{transform:translate(0,0);}' +
			'100%{transform:translate(' + (100 * x / frac) + '%,' + (100 * y / frac) + '%);}' +
		'}';
	}

	/**
	* @param {number} frac
	* @return {!Array<string>}
	*/
	function generateKeyFrames(frac) {
		return [
			generateKeyFrame('left',  -1,  0, frac),
			generateKeyFrame('right',  1,  0, frac),
			generateKeyFrame('top',    0, -1, frac),
			generateKeyFrame('bottom', 0,  1, frac)
		];
	}

	/** @type {!Array<string>} */
	let keyframes = [];
	for (let k = 1; k < 10; ++k) {
		[].push.apply(keyframes, generateKeyFrames(k));
	}
	let rules = keyframes.join('');

	let stylesheet = document.createElement('link');
	stylesheet.rel = 'stylesheet';
	stylesheet.type = 'text/css';
	stylesheet.href = 'data:text/css;charset=' + (/** @type {?string} */ (document['characterSet']) || 'utf-8') + ',' + encodeURIComponent(rules);
	document.getElementsByTagName('head')[0].appendChild(stylesheet);
}
generateAnimationStyleDeclarations();

/**
* @constructor
* @param {!Element} holder
* @param {!Element} caption
*/
function SlidePlusSliderItem(holder, caption) {
	/**
	* Element holding an image or arbitrary HTML data.
	* @type {!Element}
	*/
	this.holder = holder;
	/**
	* Element with the caption text assigned to the image or HTML data.
	* @type {!Element}
	*/
	this.caption = caption;
}

/**
* @constructor
* @param {Element=} elem The placeholder element to replace with the slider.
* @param {SlidePlusOptions=} options Settings that customize the appearance and behavior of the slider.
* @param {function(!Element=):string=} titleFunc A function that extracts the caption HTML string corresponding to an element to be shown.
*/
function SlidePlusSlider(elem, options, titleFunc) {
	let self = this;

	// element existence test to ensure element is within DOM, some content management
	// systems may call the script even if the associated content is not on the page,
	// which is the case e.g. with Joomla category list layout or multi-page layout
	if (!elem) {
		return;
	}

	let titleFn = titleFunc || function () { return null; };

	// extracts the contents of a <noscript> element
	let host = elem;
	if (elem.tagName.toLowerCase() === 'noscript') {
		host = document.createElement('div');
		host.innerHTML = /** @type {string} */ (elem['innerText']);  // <noscript> elements are not parsed when javascript is enabled
	}

	/** @type {!SlidePlusOptions} */
	this.options = /** @type {!SlidePlusOptions} */ (applyDefaults(options, slidePlusDefaults));
	/** @type {number} */
	this.current = 0;

	/** @type {!Array<!HTMLElement>} */
	let listnodes = [].slice.call(/** @type {!IArrayLike<!HTMLElement>} */ (host.getElementsByTagName('li')));

	// randomize order of elements in the list
	if (this.options['random']) {
		listnodes = shuffle(listnodes);
	}

	// save captions for items
	/** @type {!Array<?string>} */
	this.captions = listnodes.map(titleFn);

	// unwrap items from <li> parent, keeping attached event handlers
	/**
	* List of DOM elements the sliding viewpane can be populated with.
	* @type {!Array<!HTMLElement>}
	*/
	let items = listnodes.map(function (/** @type {!HTMLElement} */ listitem) {
		let container = /** @type {!HTMLElement} */ (document.createElement('div'));
		for (let child = listitem.firstChild; child; child = listitem.firstChild) {
			container.appendChild(child);
		}
		return container;
	});
	/** @type {!Array<!HTMLElement>} */
	this.items = items;

	/** @type {number} */
	let rows = /** @type {number} */ (this.options['rows']);
	/** @type {number} */
	let cols = /** @type {number} */ (this.options['cols']);
	/** @type {boolean} */
	let loop = /** @type {boolean} */ (this.options['loop']);

	// ensure parameters are consistent with one another
	rows = Math.max(1, rows);
	cols = Math.max(1, cols);
	if (items.length <= rows * cols) {  // disable looping if not meaningful
		loop = false;
	}
	this.options['rows'] = rows;
	this.options['cols'] = cols;
	this.options['loop'] = loop;

	const orientation = /** @type {SlidePlusOrientation} */ (this.options['orientation']);
	const writingsystem = /** @type {SlidePlusWritingSystem} */ (self.options['dir']);

	// clone items to handle the case when the current and the successive viewport area display identical indices simultaneously
	/** @type {!Array<!Element>} */
	let clones = [];
	if (items.length < 2 * rows * cols) {
		// insufficient items to populate all slots in current and successive viewport
		while (clones.length < 2 * rows * cols) {
			clones = clones.concat(cloneElements(items));
		}
	} else if (loop) {
		// navigating to first/last page when some items from first/last page are currently visible
		clones = cloneElements(items.slice(0, rows*cols));  // append first rows*cols elements
		clones.length = items.length - rows*cols;  // skip all elements but last rows*cols
		clones = clones.concat(items.slice(-rows*cols));  // append last rows*cols elements
	}
	/** @type {!Array<!Element>} */
	this.clones = clones;

	// initiate pre-loading images after clones have been made
	if (options['lazyload']) {
		/** @type {!Array<!Preloader>} */
		this.preloaders = items.map(function (item) {
			return new Preloader(item);
		});
	}

	/**
	* @param {!Array<!SlidePlusSliderItem>} itemlist
	* @return {!HTMLElement}
	*/
	function createStripe(itemlist) {
		let grid = /** @type {!HTMLElement} */ (document.createElement('div'));
		grid.classList.add('slideplus-stripe');
		for (let i = 0; i < rows; ++i) {
			let gridrow = document.createElement('div');
			for (let j = 0; j < cols; ++j) {
				let griditem = document.createElement('div');
				griditem.classList.add('slideplus-slot');
				griditem.classList.add('slideplus-' + /** @type {SlidePlusPosition} */ (self.options['captions']));

				let aspect = document.createElement('div');
				aspect.classList.add('slideplus-aspect');
				griditem.appendChild(aspect);

				let contentholder = document.createElement('div');
				contentholder.classList.add('slideplus-content');
				griditem.appendChild(contentholder);

				let caption = document.createElement('div');
				caption.classList.add('slideplus-caption');

				gridrow.appendChild(griditem);

				itemlist.push(new SlidePlusSliderItem(contentholder, caption));
			}
			grid.appendChild(gridrow);
		}
		return grid;
	}

	/**
	* Tabular (semantically two-dimensional) array of items the sliding viewpane is currently populated with.
	* @type {!Array<!SlidePlusSliderItem>}
	*/
	let currentitems = [];
	/** @type {!Array<!SlidePlusSliderItem>} */
	this.currentitems = currentitems;
	let currentgrid = createStripe(currentitems);
	currentgrid.addEventListener('animationend', function () {
		self._layout();
	});
	/** @type {!HTMLElement} */
	this.currentgrid = currentgrid;

	/**
	* Tabular (semantically two-dimensional) array of items the upcoming sliding viewpane is to be populated with.
	* @type {!Array<!SlidePlusSliderItem>}
	*/
	let successoritems = [];
	/** @type {!Array<!SlidePlusSliderItem>} */
	this.successoritems = successoritems;
	let successorgrid = createStripe(successoritems);
	successorgrid.classList.add('slideplus-successor');
	successorgrid.classList.add('slideplus-hidden');
	/** @type {!HTMLElement} */
	this.successorgrid = successorgrid;

	let viewport = document.createElement('div');
	viewport.classList.add('slideplus-viewport');
	viewport.classList.add('slideplus-' + orientation);
	viewport.appendChild(currentgrid);
	viewport.appendChild(successorgrid);

	// navigation by swipe
	/** @type {number} */
	let touchStartX;
	/** @type {number} */
	let touchStartY;
	viewport.addEventListener('touchstart', function (/** @type {Event} */ event) {
		let touchEvent = /** @type {TouchEvent} */ (event);
		let touch = touchEvent.changedTouches[0];
		touchStartX = touch.pageX;
		touchStartY = touch.pageY;
	});
	viewport.addEventListener('touchend', function (/** @type {Event} */ event) {
		let touchEvent = /** @type {TouchEvent} */ (event);

		/**
		* Interpreting touch event locations, navigates to the previous or next set items.
		* @param {number} oldpos
		* @param {number} newpos
		*/
		function navigateOnSwipe(oldpos, newpos) {
			if (newpos - oldpos >= 50) {  // swipe to the right or bottom
				self.previous.call(self);
			} else if (oldpos - newpos >= 50) {  // swipe to the left or top
				self.next.call(self);
			}
		}

		let touch = touchEvent.changedTouches[0];
		if (SlidePlusOrientation.Horizontal === orientation) {  // moves from right to left, or left to right
			navigateOnSwipe(touchStartX, touch.pageX);
		} else if (SlidePlusOrientation.Vertical === orientation) {  // moves upwards or downwards
			navigateOnSwipe(touchStartY, touch.pageY);
		}
	});
	viewport.addEventListener('mouseenter', function (/** @type {Event} */ event) {
		self._stopTimer();
	});
	viewport.addEventListener('mouseleave', function (/** @type {Event} */ event) {
		self._startTimer();
	});

	// create navigation control buttons
	let previousbutton = document.createElement('div');
	previousbutton.classList.add('slideplus-previous');
	previousbutton.classList.add('slideplus-button');
	previousbutton.addEventListener('click', function (event) {
		event.preventDefault();
		self.previous();
	});
	viewport.appendChild(previousbutton);

	let nextbutton = document.createElement('div');
	nextbutton.classList.add('slideplus-next');
	nextbutton.classList.add('slideplus-button');
	nextbutton.addEventListener('click', function (/** @type {Event} */ event) {
		event.preventDefault();
		self.next();
	});
	viewport.appendChild(nextbutton);

	// suppress context menu and drag-and-drop
	if (this.options['protection']) {
		document.addEventListener('contextmenu', function (/** @type {Event} */ event) {  // prevent right-click on image
			let mouseEvent = /** @type {MouseEvent} */ (event);
			if (viewport.contains(/** @type {Node} */ (mouseEvent.target))) {
				mouseEvent.preventDefault();
			}
		});
		document.addEventListener('dragstart', function (/** @type {Event} */  event) {  // prevent drag-and-drop of image
			let dragEvent = /** @type {DragEvent} */ (event);
			if (viewport.contains(/** @type {Node} */ (dragEvent.target))) {
				dragEvent.preventDefault();
			}
		});
	}

	/**
	* @param {SlidePlusPosition} position
	* @return {Element}
	*/
	function createNavigationBar(position) {
		let navigationbar = document.createElement('div');
		navigationbar.classList.add('slideplus-navigation');
		navigationbar.dir = writingsystem;

		let paging = document.createElement('span');
		paging.classList.add('slideplus-paging');

		/**
		* @param {!Element} target
		* @param {string} cls
		* @param {string} content
		* @param {function():void} action
		*/
		function createLink(target, cls, content, action) {
			let link = document.createElement('a');
			link.href = '#';
			link.classList.add(cls);
			link.innerHTML = '<span>' + content + '</span>';
			link.addEventListener('click', function (event) {
				event.preventDefault();
				action();
			});
			target.appendChild(link);
			target.appendChild(document.createTextNode(' '));
		}

		/**
		* @param {string} type
		* @param {function():void} action
		*/
		function createNavigationLink(type, action) {
			createLink(navigationbar, 'slideplus-' + type, '', action);
		}

		/**
		* @param {number} index
		* @param {number} pagenumber
		*/
		function createPagingLink(index, pagenumber) {
			createLink(paging, 'slideplus-pager', '' + pagenumber, function () {
				if (index != self.current) {
					self._jump(index);
				}
			});
		}

		// populate navigation bar
		createNavigationLink('first', self.first.bind(self));
		createNavigationLink('previous', self.previous.bind(self));
		navigationbar.appendChild(paging);
		createNavigationLink('next', self.next.bind(self));
		createNavigationLink('last', self.last.bind(self));
		navigationbar.classList.add('slideplus-' + position);

		// populate paging area
		const len = self.items.length;
		const offset = self._getOffset();
		for (let i = 0, n = 1; i < len; i += offset, ++n) {
			createPagingLink(i, n);
		}

		return navigationbar;
	}

	let topnavigation = createNavigationBar(SlidePlusPosition.Top);
	let bottomnavigation = createNavigationBar(SlidePlusPosition.Bottom);

	/**
	* DOM Element that encapsules items, navigation controls, etc.
	* @type {!Element}
	*/
	let gallery = document.createElement('div');
	this.gallery = gallery;
	gallery.classList.add('slideplus-container');
	gallery.dir = writingsystem;

	const hasMultiplePages = items.length > rows * cols;
	if (hasMultiplePages) {
		gallery.appendChild(topnavigation);
	}
	gallery.appendChild(viewport);
	if (hasMultiplePages) {
		gallery.appendChild(bottomnavigation);
	}

	elem.parentNode.replaceChild(gallery, elem);

	this._updateNavigationControls();
	this._layout();

	const delay = /** @type {number} */ (this.options['delay']);
	if (delay > 0) {
		self._startTimer();
	}
}
/**
* @param {number} index A (possibly out-of-range) index.
* @return {boolean}
*/
SlidePlusSlider.prototype._inRange = function (index) {
	return index >= 0 && index < this.items.length;
}
/**
* The number of items the slider advances when navigating to the previous or next state.
* Returns (rows*cols) representing the entire viewport when the slide step is a full page.
* Returns rows or cols, whichever is appropriate, when the slider advances by a single column or row on each step.
* @return {number} An index offset.
*/
SlidePlusSlider.prototype._getOffset = function () {
	const rows = /** @type {number} */ (this.options['rows']);
	const cols = /** @type {number} */ (this.options['cols']);
	const orientation = /** @type {SlidePlusOrientation} */ (this.options['orientation']);
	const step = /** @type {SlidePlusStep} */ (this.options['step']);

	let offset = 0;
	if (SlidePlusOrientation.Horizontal === orientation) {  // moves from right to left, or left to right
		if (SlidePlusStep.Single === step) {  // advances by a single column
			offset = rows;
		} else {
			offset = rows * cols;
		}
	} else if (SlidePlusOrientation.Vertical === orientation) {  // moves upwards or downwards
		if (SlidePlusStep.Single === step) {  // advances by a single row
			offset = cols;
		} else {
			offset = rows * cols;
		}
	}
	return offset;
}
/**
* Applies an action to all control elements that match a selector.
* @param {string} selector
* @param {function(!HTMLElement):void} action
*/
SlidePlusSlider.prototype._applyAll = function (selector, action) {
	[].forEach.call(/** @type {!IArrayLike<!HTMLElement>} */ (this.gallery.querySelectorAll(selector)), function (/** @type {!HTMLElement} */ item) {
		action(item);
	});
};
/**
* Hides all control elements that match a selector.
* @param {string} selector
*/
SlidePlusSlider.prototype._hideAll = function (selector) {
	this._applyAll(selector, function (item) {
		item.classList.add('slideplus-hidden');
	});
};
/**
* Shows all control elements that match a selector.
* @param {string} selector
*/
SlidePlusSlider.prototype._showAll = function (selector) {
	this._applyAll(selector, function (item) {
		item.classList.remove('slideplus-hidden');
	});
};
/**
* Updates visibility for navigation control buttons.
*/
SlidePlusSlider.prototype._updateNavigationControls = function () {
	const len = this.items.length;
	const rows = /** @type {number} */ (this.options['rows']);
	const cols = /** @type {number} */ (this.options['cols']);
	const hasMultiplePages = len > rows * cols;
	const loop = /** @type {boolean} */ (this.options['loop']);
	const offset = this._getOffset();
	if (!loop || len < 2) {
		// cannot advance past first image or there are insufficient images
		if (hasMultiplePages && this.current > 0) {
			this._showAll('.slideplus-first');
			this._showAll('.slideplus-previous');
		} else {
			this._hideAll('.slideplus-first');
			this._hideAll('.slideplus-previous');
		}

		// cannot advance past last image or there are insufficient images
		if (hasMultiplePages && this._inRange(this.current + offset)) {
			this._showAll('.slideplus-next');
			this._showAll('.slideplus-last');
		} else {
			this._hideAll('.slideplus-next');
			this._hideAll('.slideplus-last');
		}
	}

	// highlight current item in pager control
	this._applyAll('.slideplus-pager', function (/** @type {!HTMLElement} */ pager) {
		pager.classList.remove('slideplus-current');
	});
	const pageindex = Math.floor(this.current / offset);
	[].forEach.call(/** @type {!IArrayLike<!HTMLElement>} */ (this.gallery.querySelectorAll('.slideplus-paging')), function (/** @type {!HTMLElement} */ item) {
		let pagers = item.querySelectorAll('.slideplus-pager');
		pagers[pageindex].classList.add('slideplus-current');
	});
};
/**
* Circular indexing.
* @param {number} index Index, possibly out of the range of indexable elements.
* @return {number} An index within the range of indexable elements.
*/
SlidePlusSlider.prototype._at = function (index) {
	const len = this.items.length;
	return (index % len + len) % len;
};
/**
* @param {!Array<!SlidePlusSliderItem>} target The container in which to arrange elements.
* @param {number} index An index with circular indexing semantics.
*/
SlidePlusSlider.prototype._arrange = function (target, index) {
	let layout = /** @type {SlidePlusLayout} */ (this.options['layout']);
	if (SlidePlusLayout.Natural === layout) {
		const orientation = /** @type {SlidePlusOrientation} */ (this.options['orientation']);
		if (SlidePlusOrientation.Horizontal === orientation) {
			layout = SlidePlusLayout.Column;
		} else if (SlidePlusOrientation.Vertical === orientation) {
			layout = SlidePlusLayout.Row;
		}
	}

	const rows = /** @type {number} */ (this.options['rows']);
	const cols = /** @type {number} */ (this.options['cols']);
	const loop = /** @type {boolean} */ (this.options['loop']);

	// set increments to advance with to support row-major and column-major traversal in same procedural loop
	let majorstride = 0, minorstride = 0;
	if (SlidePlusLayout.Row === layout) {
		majorstride = cols;
		minorstride = 1;
	} else if (SlidePlusLayout.Column === layout) {
		majorstride = 1;
		minorstride = rows;
	}

	// traverse items in row-major or column-major order
	// use i and j for checking limits, m and n for indexing
	for (let i = 0, m = index; i < rows; ++i, m += majorstride) {
		for (let j = 0, n = m; j < cols; ++j, n += minorstride) {
			const k = this._at(n);
			let item = this.items[k];

			// make sure we do not re-assign an item already in a designated slot
			/** @type {Element} */
			let assignable = null;
			if (item.parentNode) {  // already assigned to a slot
				// use a clone to fill slot if wrap-around is enabled (clone will be substituted with original when navigation ends)
				if (loop) {
					// iterate until we find an unassigned clone candidate
					for (let l = k; l < this.clones.length; l += this.items.length) {
						assignable = this.clones[l];

						// check if clone is already assigned (e.g. a 2x2 slider shows images #1, #2, #3 and #1)
						if (!assignable.parentNode) {
							break;  // a good candidate
						}
					}
				}
			} else {
				// leave slot empty unless wrap-around is enabled or slot in range of available item count
				if (loop || this._inRange(n)) {
					assignable = item;
				}
			}

			let targetitem = target[i*cols + j];

			// remove existing caption
			let captionholder = targetitem.caption;
			if (captionholder.parentNode) {
				captionholder.parentNode.removeChild(captionholder);
			}

			// assign new element to slot
			if (assignable) {
				let contentholder = targetitem.holder;
				contentholder.appendChild(assignable);

				// update caption
				let caption = this.captions[k];
				captionholder.innerHTML = caption || '';
				if (caption) {
					let position = /** @type {SlidePlusPosition} */ (this.options['captions']);
					let host = SlidePlusPosition.Top === position || SlidePlusPosition.Bottom === position ? contentholder.firstChild : contentholder.parentNode;
					host.appendChild(captionholder);
				}
			}
		}
	}
};
SlidePlusSlider.prototype._layout = function () {
	// stop any pending animations and reset animation state
	this.currentgrid.style.removeProperty('animation-name');
	this.successorgrid.style.removeProperty('animation-name');
	this.successorgrid.classList.add('slideplus-hidden');

	// remove all items from their current position
	removeElementsFromDOM(this.items);
	removeElementsFromDOM(this.clones);

	// arrange currently visible items in the viewport window (to be moved out on navigation)
	this._arrange(this.currentitems, this.current);

	const rows = /** @type {number} */ (this.options['rows']);
	const cols = /** @type {number} */ (this.options['cols']);
	const loop = /** @type {boolean} */ (this.options['loop']);
	const current = this.current;
	let start = current - rows*cols;
	let end = current + 2*rows*cols;
	if (!loop) {
		start = Math.max(0, start);  // cap at first image
		end = Math.min(this.items.length, end);  // cap at last image
	}
	if (this.preloaders) {
		for (let k = start; k < end; ++k) {
			this.preloaders[this._at(k)].load();
		}
	}

	this._restartTimer();
};
/**
* Moves the slider to the next set of elements.
* To be used when navigating to a neighboring set of elements.
* @param {number} direction 1 to more forward, -1 to move backward.
* @param {string} horzdir Assuming horizontal orientation, the direction towards which the slider moves.
* @param {string} vertdir Assuming vertical orientation, the direction towards which the slider moves.
*/
SlidePlusSlider.prototype._advance = function (direction, horzdir, vertdir) {
	if (!this.successorgrid.classList.contains('slideplus-hidden')) {
		return;  // animation is in progress, wait until finished
	}

	this._cancelTimer();

	const rows = /** @type {number} */ (this.options['rows']);
	const cols = /** @type {number} */ (this.options['cols']);
	const loop = /** @type {boolean} */ (this.options['loop']);

	const orientation = /** @type {SlidePlusOrientation} */ (this.options['orientation']);
	const step = /** @type {SlidePlusStep} */ (this.options['step']);

	let movedir = '', stationarydir = '';
	/**
	* The reciprocal of the fraction by which the slider advances (1 for the entire viewport).
	* @type {number}
	*/
	let fraction = 0;
	if (SlidePlusOrientation.Horizontal === orientation) {  // moves from right to left, or left to right
		if (SlidePlusStep.Single === step) {  // advances by a single column
			fraction = cols;
		} else {
			fraction = 1;
		}
		movedir = horzdir;
		stationarydir = vertdir;
	} else if (SlidePlusOrientation.Vertical === orientation) {  // moves upwards or downwards
		if (SlidePlusStep.Single === step) {  // advances by a single row
			fraction = rows;
		} else {
			fraction = 1;
		}
		movedir = vertdir;
		stationarydir = horzdir;
	}

	const targetindex = this.current + direction * this._getOffset();
	if (!loop && !this._inRange(targetindex)) {
		return;
	}

	// arrange items in the viewport that moves in
	this._arrange(this.successoritems, this.current + direction * rows * cols);

	// reset position
	resetPosition(this.successorgrid);

	// initiate animation sequence
	const animation = 'slideplus-push-' + movedir + '-' + fraction;
	this.currentgrid.style['animationName'] = animation;
	this.successorgrid.style['animationName'] = animation;
	this.successorgrid.style[movedir] = '100%';
	this.successorgrid.style[stationarydir] = '0';
	this.successorgrid.classList.remove('slideplus-hidden');

	// make sure arranging layout works with new starting offset
	this.current = this._at(targetindex);

	// update visibility for navigation buttons
	this._updateNavigationControls();
};
/**
* Switches the slider to a different set of elements.
* To be used when navigating to an arbitrary (not necessarily neighboring) set of elements.
* @param {number} index The zero-based element index to jump to.
*/
SlidePlusSlider.prototype._jump = function (index) {
	if (index == this.current) {
		return;  // nowhere to jump, already at the desired index
	}

	if (!this.successorgrid.classList.contains('slideplus-hidden')) {
		return;  // animation is in progress, wait until finished
	}

	this._cancelTimer();

	this._arrange(this.successoritems, index);

	// reset position
	resetPosition(this.successorgrid);
	this.successorgrid.style.top = '0';
	this.successorgrid.style.left = '0';

	this.currentgrid.style['animationName'] = 'slideplus-fade-out';
	this.successorgrid.style['animationName'] = 'slideplus-fade-in';
	this.successorgrid.classList.remove('slideplus-hidden');

	this.current = index;

	// update visibility for navigation buttons
	this._updateNavigationControls();
};
SlidePlusSlider.prototype.first = function () {
	this._jump(0);
};
SlidePlusSlider.prototype.previous = function () {
	const ltr = /** @type {SlidePlusWritingSystem} */ (this.options['dir']) == SlidePlusWritingSystem.LeftToRight;
	this._advance(-1, ltr ? 'right' : 'left', 'bottom');
};
SlidePlusSlider.prototype.next = function () {
	const ltr = /** @type {SlidePlusWritingSystem} */ (this.options['dir']) == SlidePlusWritingSystem.LeftToRight;
	this._advance(1, ltr ? 'left' : 'right', 'top');
};
SlidePlusSlider.prototype.last = function () {
	const offset = this._getOffset();
	const len = this.items.length;
	const mod = len % offset;
	const index = len - (mod != 0 ? mod : offset);  // check for remainder to avoid a blank page (index to position past end)
	this._jump(index);
};

SlidePlusSlider.prototype['first'] = SlidePlusSlider.prototype.first;
SlidePlusSlider.prototype['previous'] = SlidePlusSlider.prototype.previous;
SlidePlusSlider.prototype['next'] = SlidePlusSlider.prototype.next;
SlidePlusSlider.prototype['last'] = SlidePlusSlider.prototype.last;

SlidePlusSlider.prototype._startTimer = function () {
	if (!this.running) {
		/** @type {boolean} */
		this.running = true;
		this._restartTimer();
	}
}
SlidePlusSlider.prototype._stopTimer = function () {
	if (this.running) {
		this.running = false;
		this._cancelTimer();
	}
}
SlidePlusSlider.prototype._restartTimer = function () {
	let self = this;

	if (this.running && !this.timeout) {  // do not restart timer unless slideshow mode is active
		const delay = /** @type {number} */ (this.options['delay']);
		if (delay > 0) {
			/** @type {?number} */
			this.timeout = window.setInterval(function () {
				let rect = self.currentgrid.getBoundingClientRect();
				for (let i = 0; i <= 2; ++i) {  // check horizontally leftmost, center and rightmost point
					for (let j = 0; j <= 2; ++j) {  // check vertically topmost, center and bottommost point
						// check if container is visible and not covered by another element (e.g. a pop-up window)
						let elem = document.elementFromPoint(rect.left + i * rect.width / 2, rect.top + j * rect.height / 2);
						if (self.gallery.contains(elem)) {  // check self and all descendants
							self.next();
						}
					}
				}
			}, delay);
		}
	}
}
SlidePlusSlider.prototype._cancelTimer = function () {
	if (this.timeout) {
		window.clearInterval(this.timeout);
		this.timeout = null;
	}
}

/**
* Automatically discovers static image sliders wrapped in an HTML <noscript> element and transforms them into a rotating gallery.
*
* Example HTML source code:
* <noscript class="slideplus">
* <ul>
* <li><a href="images/example1.jpg"><img width="150" height="100" alt="First sample image" src="thumbs/example1.jpg" /></a></li>
* <li><img width="150" height="100" alt="Second sample image" src="thumbs/example2.jpg" /></li>
* </ul>
* </noscript>
*
* @param {SlidePlusOptions=} options
* @param {function(!Element=):string=} titleFn
*/
SlidePlusSlider['discover'] = function (options, titleFn) {
	// lists, possibly wrapped in <noscript>
	[].forEach.call(/** @type {!IArrayLike<!HTMLElement>} */ (document.querySelectorAll('ul.slideplus, noscript.slideplus')), function (/** @type {!HTMLElement} */ item) {
		new SlidePlusSlider(item, options, titleFn);
	});
};

window['SlidePlusSlider'] = SlidePlusSlider;
window['Preloader'] = Preloader;

Zerion Mini Shell 1.0