(function () { 'use strict'; let timerDebounce = undefined; function debounce(duration, callback) { clearTimeout(timerDebounce); timerDebounce = setTimeout(function () { callback(); }, duration); return timerDebounce; } function transitionAsPromise(triggeringFunc, el) { return new Promise(resolve => { const handleTransitionEnd = () => { el.removeEventListener('transitionend', handleTransitionEnd); resolve(); }; el.addEventListener('transitionend', handleTransitionEnd); const classesBefore = el.getAttribute('class'); const stylesBefore = el.getAttribute('style'); triggeringFunc(); if (classesBefore === el.getAttribute('class') && stylesBefore === el.getAttribute('style')) { handleTransitionEnd(); } if (parseFloat(getComputedStyle(el)['transitionDuration']) === 0) { handleTransitionEnd(); } }); } function loadImage({ src, srcset, sizes }) { const image = new Image(); image.src = src; if (srcset) { image.srcset = srcset; } if (sizes) { image.sizes = sizes; } if ('decode' in image) { return new Promise((resolve, reject) => { image.decode().then(() => { resolve(image); }).catch(() => { reject(image); }); }); } else { return new Promise((resolve, reject) => { image.onload = resolve(image); image.onerror = reject(image); }); } } function fit(options) { let height; let width; const { imgHeight, imgWidth, containerHeight, containerWidth, canvasWidth, canvasHeight, imageSize } = options; const canvasRatio = canvasHeight / canvasWidth; const containerRatio = containerHeight / containerWidth; const imgRatio = imgHeight / imgWidth; if (imageSize == 'cover') { if (imgRatio < containerRatio) { height = containerHeight; width = height / imgRatio; } else { width = containerWidth; height = width * imgRatio; } } else if (imageSize == 'native') { height = imgHeight; width = imgWidth; } else { if (imgRatio > canvasRatio) { height = canvasHeight; width = height / imgRatio; } else { width = canvasWidth; height = width * imgRatio; } if (imageSize === 'scale-down' && (width >= imgWidth || height >= imgHeight)) { width = imgWidth; height = imgHeight; } } return { height: height, width: width }; } function openFullScreen(wrapper) { if (wrapper.requestFullscreen) { wrapper.requestFullscreen(); return true; } else if (wrapper.webkitRequestFullscreen) { wrapper.webkitRequestFullscreen(); return true; } else if (wrapper.msRequestFullscreen) { wrapper.msRequestFullscreen(); return true; } else { return false; } } function exitFullScreen() { if (document.exitFullscreen) { document.exitFullscreen(); return false; } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); return false; } else if (document.msExitFullscreen) { document.msExitFullscreen(); return false; } else { return true; } } const defaults = { container: document.body, // window or element className: undefined, imageSize: 'scale-down', // 'scale-down', 'contain', 'cover' or 'native' fullScreen: false, loop: false, linkImages: true, setIndex: 0, firstImageIndex: 0, lastImageIndex: false, currentImageIndex: undefined, allowZoom: true, closeOnBackgroundClick: true, setTitle: function () { return ''; }, description: function () { return this.images[this.settings.currentImageIndex].title; }, pagination: function () { const last = this.settings.lastImageIndex + 1; const position = this.settings.currentImageIndex + 1; return position + '/' + last; }, afterInitialize() {}, afterMarkup() {}, afterImageLoad() {}, zoomedPaddingX: function (canvasWidth, imgWidth) { return 0; }, zoomedPaddingY: function (canvasHeight, imgHeight) { return 0; } }; class Chocolat { constructor(elements, settings) { this.settings = settings; this.elems = {}; this.images = []; this.events = []; this.state = { fullScreenOpen: false, initialZoomState: null, initialized: false, timer: false, visible: false }; this._cssClasses = ['chocolat-open', 'chocolat-in-container', 'chocolat-cover', 'chocolat-zoomable', 'chocolat-zoomed', 'chocolat-zooming-in', 'chocolat-zooming-out']; if (NodeList.prototype.isPrototypeOf(elements) || HTMLCollection.prototype.isPrototypeOf(elements)) { elements.forEach((el, i) => { this.images.push({ title: el.getAttribute('title'), src: el.getAttribute('href'), srcset: el.getAttribute('data-srcset'), sizes: el.getAttribute('data-sizes') }); this.off(el, 'click.chocolat'); this.on(el, 'click.chocolat', e => { this.init(i); e.preventDefault(); }); }); } else { this.images = elements; } if (this.settings.container instanceof Element || this.settings.container instanceof HTMLElement) { this.elems.container = this.settings.container; } else { this.elems.container = document.body; } this.api = { open: i => { i = parseInt(i) || 0; return this.init(i); }, close: () => { return this.close(); }, next: () => { return this.change(1); }, prev: () => { return this.change(-1); }, goto: i => { return this.open(i); }, current: () => { return this.settings.currentImageIndex; }, position: () => { return this.position(this.elems.img); }, destroy: () => { return this.destroy(); }, set: (property, value) => { this.settings[property] = value; return value; }, get: property => { return this.settings[property]; }, getElem: name => { return this.elems[name]; } }; } init(i) { if (!this.state.initialized) { this.markup(); this.attachListeners(); this.settings.lastImageIndex = this.images.length - 1; this.state.initialized = true; } this.settings.afterInitialize.call(this); return this.load(i); } load(index) { if (!this.state.visible) { this.state.visible = true; setTimeout(() => { this.elems.overlay.classList.add('chocolat-visible'); this.elems.wrapper.classList.add('chocolat-visible'); }, 0); this.elems.container.classList.add('chocolat-open'); } if (this.settings.fullScreen) { this.state.fullScreenOpen = openFullScreen(this.elems.wrapper); } if (this.settings.currentImageIndex === index) { return Promise.resolve(); } let loaderTimer = setTimeout(() => { this.elems.loader.classList.add('chocolat-visible'); }, 1000); let fadeOutPromise; let image; let fadeOutTimer = setTimeout(() => { fadeOutTimer = undefined; fadeOutPromise = transitionAsPromise(() => { this.elems.imageCanvas.classList.remove('chocolat-visible'); }, this.elems.imageCanvas); }, 80); return loadImage(this.images[index]).then(loadedImage => { image = loadedImage; if (fadeOutTimer) { clearTimeout(fadeOutTimer); return Promise.resolve(); } else { return fadeOutPromise; } }).then(() => { const nextIndex = index + 1; if (this.images[nextIndex] != undefined) { loadImage(this.images[nextIndex]); } this.settings.currentImageIndex = index; this.elems.description.textContent = this.settings.description.call(this); this.elems.pagination.textContent = this.settings.pagination.call(this); this.arrows(); return this.position(image).then(() => { this.elems.loader.classList.remove('chocolat-visible'); clearTimeout(loaderTimer); return this.appear(image); }); }).then(() => { this.elems.container.classList.toggle('chocolat-zoomable', this.zoomable(image, this.elems.wrapper)); this.settings.afterImageLoad.call(this); }); } position({ naturalHeight, naturalWidth }) { const fitOptions = { imgHeight: naturalHeight, imgWidth: naturalWidth, containerHeight: this.elems.container.clientHeight, containerWidth: this.elems.container.clientWidth, canvasWidth: this.elems.imageCanvas.clientWidth, canvasHeight: this.elems.imageCanvas.clientHeight, imageSize: this.settings.imageSize }; const { width, height } = fit(fitOptions); return transitionAsPromise(() => { Object.assign(this.elems.imageWrapper.style, { width: width + 'px', height: height + 'px' }); }, this.elems.imageWrapper); } appear(image) { this.elems.imageWrapper.removeChild(this.elems.img); this.elems.img = image; this.elems.img.setAttribute('class', 'chocolat-img'); this.elems.imageWrapper.appendChild(this.elems.img); const fadeInPromise = transitionAsPromise(() => { this.elems.imageCanvas.classList.add('chocolat-visible'); }, this.elems.imageCanvas); return fadeInPromise; } change(step) { if (!this.state.visible) { return; } if (!this.settings.linkImages) { return; } this.zoomOut(); const requestedImage = this.settings.currentImageIndex + parseInt(step); if (requestedImage > this.settings.lastImageIndex) { if (this.settings.loop) { return this.load(this.settings.firstImageIndex); } } else if (requestedImage < this.settings.firstImageIndex) { if (this.settings.loop) { return this.load(this.settings.lastImageIndex); } } else { return this.load(requestedImage); } } arrows() { if (this.settings.loop) { this.elems.left.classList.add('active'); this.elems.right.classList.add('active'); } else if (this.settings.linkImages) { this.elems.right.classList.toggle('active', this.settings.currentImageIndex !== this.settings.lastImageIndex); this.elems.left.classList.toggle('active', this.settings.currentImageIndex !== this.settings.firstImageIndex); } else { this.elems.left.classList.remove('active'); this.elems.right.classList.remove('active'); } } close() { if (this.state.fullScreenOpen) { this.state.fullScreenOpen = exitFullScreen(); return; } this.state.visible = false; const promiseOverlay = transitionAsPromise(() => { this.elems.overlay.classList.remove('chocolat-visible'); }, this.elems.overlay); const promiseWrapper = transitionAsPromise(() => { this.elems.wrapper.classList.remove('chocolat-visible'); }, this.elems.wrapper); return Promise.all([promiseOverlay, promiseWrapper]).then(() => { this.elems.container.classList.remove('chocolat-open'); }); } destroy() { for (let i = this.events.length - 1; i >= 0; i--) { const { element, eventName } = this.events[i]; this.off(element, eventName); } if (!this.state.initialized) { return; } if (this.state.fullScreenOpen) { this.state.fullScreenOpen = exitFullScreen(); } this.settings.currentImageIndex = undefined; this.state.visible = false; this.state.initialized = false; this.elems.container.classList.remove(...this._cssClasses); this.elems.wrapper.parentNode.removeChild(this.elems.wrapper); } markup() { this.elems.container.classList.add('chocolat-open', this.settings.className); if (this.settings.imageSize == 'cover') { this.elems.container.classList.add('chocolat-cover'); } if (this.elems.container !== document.body) { this.elems.container.classList.add('chocolat-in-container'); } this.elems.wrapper = document.createElement('div'); this.elems.wrapper.setAttribute('id', 'chocolat-content-' + this.settings.setIndex); this.elems.wrapper.setAttribute('class', 'chocolat-wrapper'); this.elems.container.appendChild(this.elems.wrapper); this.elems.overlay = document.createElement('div'); this.elems.overlay.setAttribute('class', 'chocolat-overlay'); this.elems.wrapper.appendChild(this.elems.overlay); this.elems.loader = document.createElement('div'); this.elems.loader.setAttribute('class', 'chocolat-loader'); this.elems.wrapper.appendChild(this.elems.loader); this.elems.layout = document.createElement('div'); this.elems.layout.setAttribute('class', 'chocolat-layout'); this.elems.wrapper.appendChild(this.elems.layout); this.elems.top = document.createElement('div'); this.elems.top.setAttribute('class', 'chocolat-top'); this.elems.layout.appendChild(this.elems.top); this.elems.center = document.createElement('div'); this.elems.center.setAttribute('class', 'chocolat-center'); this.elems.layout.appendChild(this.elems.center); this.elems.left = document.createElement('div'); this.elems.left.setAttribute('class', 'chocolat-left'); this.elems.center.appendChild(this.elems.left); this.elems.imageCanvas = document.createElement('div'); this.elems.imageCanvas.setAttribute('class', 'chocolat-image-canvas'); this.elems.center.appendChild(this.elems.imageCanvas); this.elems.imageWrapper = document.createElement('div'); this.elems.imageWrapper.setAttribute('class', 'chocolat-image-wrapper'); this.elems.imageCanvas.appendChild(this.elems.imageWrapper); this.elems.img = document.createElement('img'); this.elems.img.setAttribute('class', 'chocolat-img'); this.elems.imageWrapper.appendChild(this.elems.img); this.elems.right = document.createElement('div'); this.elems.right.setAttribute('class', 'chocolat-right'); this.elems.center.appendChild(this.elems.right); this.elems.bottom = document.createElement('div'); this.elems.bottom.setAttribute('class', 'chocolat-bottom'); this.elems.layout.appendChild(this.elems.bottom); this.elems.close = document.createElement('span'); this.elems.close.setAttribute('class', 'chocolat-close'); this.elems.top.appendChild(this.elems.close); this.elems.description = document.createElement('span'); this.elems.description.setAttribute('class', 'chocolat-description'); this.elems.bottom.appendChild(this.elems.description); this.elems.pagination = document.createElement('span'); this.elems.pagination.setAttribute('class', 'chocolat-pagination'); this.elems.bottom.appendChild(this.elems.pagination); this.elems.setTitle = document.createElement('span'); this.elems.setTitle.setAttribute('class', 'chocolat-set-title'); this.elems.setTitle.textContent = this.settings.setTitle(); this.elems.bottom.appendChild(this.elems.setTitle); this.elems.fullscreen = document.createElement('span'); this.elems.fullscreen.setAttribute('class', 'chocolat-fullscreen'); this.elems.bottom.appendChild(this.elems.fullscreen); this.settings.afterMarkup.call(this); } attachListeners() { this.off(document, 'keydown.chocolat'); this.on(document, 'keydown.chocolat', e => { if (this.state.initialized) { if (e.keyCode == 37) { this.change(-1); } else if (e.keyCode == 39) { this.change(1); } else if (e.keyCode == 27) { this.close(); } } }); const right = this.elems.wrapper.querySelector('.chocolat-right'); this.off(right, 'click.chocolat'); this.on(right, 'click.chocolat', () => { this.change(+1); }); const left = this.elems.wrapper.querySelector('.chocolat-left'); this.off(left, 'click.chocolat'); this.on(left, 'click.chocolat', () => { this.change(-1); }); this.off(this.elems.close, 'click.chocolat'); this.on(this.elems.close, 'click.chocolat', this.close.bind(this)); this.off(this.elems.fullscreen, 'click.chocolat'); this.on(this.elems.fullscreen, 'click.chocolat', () => { if (this.state.fullScreenOpen) { this.state.fullScreenOpen = exitFullScreen(); return; } this.state.fullScreenOpen = openFullScreen(this.elems.wrapper); }); if (this.settings.closeOnBackgroundClick) { this.off(this.elems.overlay, 'click.chocolat'); this.on(this.elems.overlay, 'click.chocolat', this.close.bind(this)); } this.off(this.elems.wrapper, 'click.chocolat'); this.on(this.elems.wrapper, 'click.chocolat', () => { if (this.state.initialZoomState === null || !this.state.visible) { return; } this.elems.container.classList.add('chocolat-zooming-out'); this.zoomOut().then(() => { this.elems.container.classList.remove('chocolat-zoomed'); this.elems.container.classList.remove('chocolat-zooming-out'); }); }); this.off(this.elems.imageWrapper, 'click.chocolat'); this.on(this.elems.imageWrapper, 'click.chocolat', e => { if (this.state.initialZoomState === null && this.elems.container.classList.contains('chocolat-zoomable')) { e.stopPropagation(); this.elems.container.classList.add('chocolat-zooming-in'); this.zoomIn(e).then(() => { this.elems.container.classList.add('chocolat-zoomed'); this.elems.container.classList.remove('chocolat-zooming-in'); }); } }); this.on(this.elems.wrapper, 'mousemove.chocolat', e => { if (this.state.initialZoomState === null || !this.state.visible) { return; } const rect = this.elems.wrapper.getBoundingClientRect(); const pos = { top: rect.top + window.scrollY, left: rect.left + window.scrollX }; const height = this.elems.wrapper.clientHeight; const width = this.elems.wrapper.clientWidth; const imgWidth = this.elems.img.width; const imgHeight = this.elems.img.height; const coord = [e.pageX - width / 2 - pos.left, e.pageY - height / 2 - pos.top]; let mvtX = 0; if (imgWidth > width) { const paddingX = this.settings.zoomedPaddingX(imgWidth, width); mvtX = coord[0] / (width / 2); mvtX = ((imgWidth - width) / 2 + paddingX) * mvtX; } let mvtY = 0; if (imgHeight > height) { const paddingY = this.settings.zoomedPaddingY(imgHeight, height); mvtY = coord[1] / (height / 2); mvtY = ((imgHeight - height) / 2 + paddingY) * mvtY; } this.elems.img.style.marginLeft = -mvtX + 'px'; this.elems.img.style.marginTop = -mvtY + 'px'; }); this.on(window, 'resize.chocolat', e => { if (!this.state.initialized || !this.state.visible) { return; } debounce(50, () => { const fitOptions = { imgHeight: this.elems.img.naturalHeight, imgWidth: this.elems.img.naturalWidth, containerHeight: this.elems.wrapper.clientHeight, containerWidth: this.elems.wrapper.clientWidth, canvasWidth: this.elems.imageCanvas.clientWidth, canvasHeight: this.elems.imageCanvas.clientHeight, imageSize: this.settings.imageSize }; const { width, height } = fit(fitOptions); this.position(this.elems.img).then(() => { this.elems.container.classList.toggle('chocolat-zoomable', this.zoomable(this.elems.img, this.elems.wrapper)); }); }); }); } zoomable(image, wrapper) { const wrapperWidth = wrapper.clientWidth; const wrapperHeight = wrapper.clientHeight; const isImageZoomable = this.settings.allowZoom && (image.naturalWidth > wrapperWidth || image.naturalHeight > wrapperHeight) ? true : false; const isImageStretched = image.clientWidth > image.naturalWidth || image.clientHeight > image.naturalHeight; return isImageZoomable && !isImageStretched; } zoomIn(e) { this.state.initialZoomState = this.settings.imageSize; this.settings.imageSize = 'native'; return this.position(this.elems.img); } zoomOut(e) { this.settings.imageSize = this.state.initialZoomState || this.settings.imageSize; this.state.initialZoomState = null; this.elems.img.style.margin = 0; return this.position(this.elems.img); } on(element, eventName, cb) { // const eventName = this.settings.setIndex + '-' + eventName const length = this.events.push({ element, eventName, cb }); element.addEventListener(eventName.split('.')[0], this.events[length - 1].cb); } off(element, eventName) { // const eventName = this.settings.setIndex + '-' + eventName const index = this.events.findIndex(event => { return event.element === element && event.eventName === eventName; }); if (this.events[index]) { element.removeEventListener(eventName.split('.')[0], this.events[index].cb); this.events.splice(index, 1); } } } const instances = []; window.Chocolat = function (elements, options) { const settings = Object.assign({}, defaults, { images: [] }, options, { setIndex: instances.length }); const instance = new Chocolat(elements, settings); instances.push(instance); return instance; }; }());