@epickris/bootstrap-kit
Version:
User interface and behaviour framework based on Bootstrap.
340 lines (256 loc) • 9.96 kB
JavaScript
import EventHandler from 'bootstrap/js/src/dom/event-handler';
import BaseComponent from 'bootstrap/js/src/base-component';
import { defineJQueryPlugin, executeAfterTransition } from 'bootstrap/js/src/util';
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'zoom';
const DATA_KEY = 'bs.zoom';
const EVENT_KEY = `.${DATA_KEY}`;
const DATA_API_KEY = '.data-api';
const ZOOM_OFFSET = 80;
const EVENT_CLICK = `click${EVENT_KEY}`;
const EVENT_SCROLL = `scroll${EVENT_KEY}`;
const EVENT_KEYUP = `keyup${EVENT_KEY}`;
const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`;
const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`;
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`;
const CLASS_NAME_ZOOM_OVERLAY_OPEN = 'zoom-overlay-open';
const CLASS_NAME_ZOOM_OVERLAY_TRANSITIONING = 'zoom-overlay-transitioning';
const CLASS_NAME_ZOOM_OVERLAY = 'zoom-overlay';
const CLASS_NAME_ZOOM_IMG_WRAP = 'zoom-img-wrap';
const CLASS_NAME_ZOOM_IMG = 'zoom-img';
const SELECTOR_ACTION = '[data-action="zoom"]';
const DATA_ZOOM = 'zoom';
const DATA_ZOOM_OUT = 'zoom-out';
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
*/
class ZoomService extends BaseComponent {
constructor(element) {
super(element);
this._activeZoom = null;
this._initialScrollPosition = null;
this._initialTouchPosition = null;
this._touchMoveListener = null;
this._document = document;
this._window = window;
this._body = document.body;
this._boundClick = this._clickHandler.bind(this);
}
// Getters
static get DATA_KEY() {
return DATA_KEY;
}
// Private
_zoom(event) {
const { target, ctrlKey, metaKey, bubbles } = event;
if (!target || target.tagName !== 'IMG') {
return;
}
if (this._body.classList.contains(CLASS_NAME_ZOOM_OVERLAY_OPEN)) {
return;
}
if (metaKey || ctrlKey) {
return window.open((target.getAttribute('data-original') || target.src), '_blank');
}
if (target.width >= (window.innerWidth - ZOOM_OFFSET)) {
return;
}
this._activeZoomClose(true);
this._activeZoom = new Zoom(target);
this._activeZoom.zoomImage();
// Todo: Probably worth throttling this
EventHandler.on(this._window, EVENT_SCROLL, this._scrollHandler.bind(this));
EventHandler.on(this._document, EVENT_KEYUP, this._keyHandler.bind(this));
EventHandler.on(this._document, EVENT_TOUCHSTART, this._touchStart.bind(this));
// We use a capturing phase here to prevent unintended js events
if (document.addEventListener) {
document.addEventListener('click', this._boundClick, true);
} else {
document.attachEvent('onclick', this._boundClick, true);
}
if ('bubbles' in event) {
if (bubbles) {
event.stopPropagation();
}
} else {
// Internet Explorer before version 9
event.cancelBubble = true;
}
}
_activeZoomClose(forceDispose) {
if (!this._activeZoom) {
return;
}
if (forceDispose) {
this._activeZoom.dispose();
} else {
this._activeZoom.close();
}
EventHandler.off(this._window, EVENT_KEY);
EventHandler.off(this._window, EVENT_KEY);
document.removeEventListener('click', this._boundClick, true);
this._activeZoom = null;
}
_scrollHandler() {
if (this._initialScrollPosition === null) {
this._initialScrollPosition = window.scrollTop;
}
const deltaY = this._initialScrollPosition - window.scrollTop;
if (Math.abs(deltaY) >= 40) {
this._activeZoomClose();
}
}
_keyHandler(event) {
if (event.keyCode === 27) {
this._activeZoomClose();
}
}
_clickHandler(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
if ('bubbles' in event) {
if (event.bubbles) {
event.stopPropagation();
}
} else {
// Internet Explorer before version 9
event.cancelBubble = true;
}
this._activeZoomClose();
}
_touchStart(event) {
this._initialTouchPosition = event.touches[0].pageY;
EventHandler.on(event.target, EVENT_TOUCHMOVE, this._touchMove.bind(this));
}
_touchMove(event) {
if (Math.abs(event.touches[0].pageY - this._initialTouchPosition) > 10) {
this._activeZoomClose();
EventHandler.off(event.target, EVENT_TOUCHMOVE);
}
}
listen() {
EventHandler.on(this._body, EVENT_CLICK, SELECTOR_ACTION, this._zoom.bind(this));
}
}
class Zoom {
constructor(element) {
this._fullHeight = null;
this._fullWidth = null;
this._overlay = null;
this._targetImageWrap = null;
this._targetImage = element;
this._targetTransform = element.style.transform;
this._body = document.body;
}
// Public
zoomImage() {
const img = document.createElement('img');
img.addEventListener('load', () => {
this._fullHeight = Number(img.height);
this._fullWidth = Number(img.width);
this._zoomOriginal();
});
img.src = this._targetImage.src;
}
_zoomOriginal() {
this._targetImageWrap = document.createElement('div');
this._targetImageWrap.className = CLASS_NAME_ZOOM_IMG_WRAP;
this._targetImage.parentNode.insertBefore(this._targetImageWrap, this._targetImage);
this._targetImageWrap.append(this._targetImage);
this._targetImage.classList.add(CLASS_NAME_ZOOM_IMG);
this._targetImage.dataset.Action = DATA_ZOOM_OUT;
this._overlay = document.createElement('div');
this._overlay.className = CLASS_NAME_ZOOM_OVERLAY;
document.body.append(this._overlay);
this._calculateZoom();
this._triggerAnimation();
}
_calculateZoom() {
// eslint-disable-next-line no-unused-expressions
this._targetImage.offsetWidth; // Repaint before animating
const originalFullImageWidth = this._fullWidth;
const originalFullImageHeight = this._fullHeight;
const maxScaleFactor = originalFullImageWidth / this._targetImage.width;
const viewportHeight = (window.innerHeight - ZOOM_OFFSET);
const viewportWidth = (window.innerWidth - ZOOM_OFFSET);
const imageAspectRatio = originalFullImageWidth / originalFullImageHeight;
const viewportAspectRatio = viewportWidth / viewportHeight;
if (originalFullImageWidth < viewportWidth && originalFullImageHeight < viewportHeight) {
this._imgScaleFactor = maxScaleFactor;
} else if (imageAspectRatio < viewportAspectRatio) {
this._imgScaleFactor = (viewportHeight / originalFullImageHeight) * maxScaleFactor;
} else {
this._imgScaleFactor = (viewportWidth / originalFullImageWidth) * maxScaleFactor;
}
}
_triggerAnimation() {
// eslint-disable-next-line no-unused-expressions
this._targetImage.offsetWidth; // Repaint before animating
const rect = this._targetImage.getBoundingClientRect();
const imageOffset = {
top: rect.top + window.pageYOffset,
left: rect.left + window.pageXOffset
};
const scrollTop = window.pageYOffset;
const viewportY = scrollTop + (window.innerHeight / 2);
const viewportX = (window.innerWidth / 2);
const imageCenterY = imageOffset.top + (this._targetImage.offsetHeight / 2);
const imageCenterX = imageOffset.left + (this._targetImage.offsetWidth / 2);
this._translateY = viewportY - imageCenterY;
this._translateX = viewportX - imageCenterX;
const targetTransform = `scale(${this._imgScaleFactor})`;
const imageWrapTransform = `translate3d(${this._translateX}px, ${this._translateY}px, 1px)`;
this._targetImage.style.transform = targetTransform;
this._targetImage.style['-webkit-transform'] = this._targetImage.style.transform;
this._targetImage.style['-ms-transform'] = this._targetImage.style.transform;
this._targetImageWrap.style.transform = imageWrapTransform;
this._targetImageWrap.style['-webkit-transform'] = this._targetImageWrap.style.transform;
this._targetImageWrap.style['-ms-transform'] = this._targetImageWrap.style.transform;
this._body.classList.add(CLASS_NAME_ZOOM_OVERLAY_OPEN);
}
close() {
this._body.classList.remove(CLASS_NAME_ZOOM_OVERLAY_OPEN);
this._body.classList.add(CLASS_NAME_ZOOM_OVERLAY_TRANSITIONING);
this._targetImage.style.transform = this._targetTransform;
this._targetImage.style['-webkit-transform'] = this._targetImage.transform;
this._targetImage.style['-ms-transform'] = this._targetImage.transform;
this._targetImageWrap.style.transform = '';
this._targetImageWrap.style['-webkit-transform'] = this._targetImageWrap.transform;
this._targetImageWrap.style['-ms-transform'] = this._targetImageWrap.transform;
executeAfterTransition(this.dispose.bind(this), this._targetImage);
}
dispose() {
if (this._targetImageWrap && this._targetImageWrap.parentNode) {
this._targetImage.classList.remove(CLASS_NAME_ZOOM_IMG);
this._targetImage.dataset.action = DATA_ZOOM;
this._targetImageWrap.parentNode.replaceChild(this._targetImage, this._targetImageWrap);
this._overlay.remove();
this._body.classList.remove(CLASS_NAME_ZOOM_OVERLAY_TRANSITIONING);
}
}
}
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
*/
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
new ZoomService().listen();
});
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
* add .Zoom to jQuery only if jQuery is present
*/
defineJQueryPlugin(NAME, Zoom);
export default Zoom;