%PDF- %PDF-
Direktori : /home/lightco1/www/media/sigplus/engines/slideplus/js/ |
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;