viewerjs
Version:
JavaScript image viewer.
1,287 lines (1,075 loc) • 30.9 kB
JavaScript
import {
CLASS_ACTIVE,
CLASS_FADE,
CLASS_FIXED,
CLASS_FULLSCREEN_EXIT,
CLASS_HIDE,
CLASS_IN,
CLASS_INVISIBLE,
CLASS_LOADING,
CLASS_SHOW,
CLASS_TRANSITION,
EVENT_CLICK,
EVENT_ERROR,
EVENT_HIDE,
EVENT_LOAD,
EVENT_MOVE,
EVENT_MOVED,
EVENT_PLAY,
EVENT_ROTATE,
EVENT_ROTATED,
EVENT_SCALE,
EVENT_SCALED,
EVENT_SHOW,
EVENT_STOP,
EVENT_TRANSITION_END,
EVENT_VIEW,
EVENT_VIEWED,
EVENT_ZOOM,
EVENT_ZOOMED,
NAMESPACE,
} from './constants';
import {
addClass,
addListener,
assign,
dispatchEvent,
escapeHTMLEntities,
forEach,
getData,
getOffset,
getPointersCenter,
hasClass,
isFunction,
isNumber,
isPlainObject,
isUndefined,
removeClass,
removeListener,
setStyle,
toggleClass,
} from './utilities';
export default {
/** Show the viewer (only available in modal mode)
* @param {boolean} [immediate=false] - Indicates if show the viewer immediately or not.
* @returns {Viewer} this
*/
show(immediate = false) {
const { element, options } = this;
if (options.inline || this.showing || this.isShown || this.showing) {
return this;
}
if (!this.ready) {
this.build();
if (this.ready) {
this.show(immediate);
}
return this;
}
if (isFunction(options.show)) {
addListener(element, EVENT_SHOW, options.show, {
once: true,
});
}
if (dispatchEvent(element, EVENT_SHOW) === false || !this.ready) {
return this;
}
if (this.hiding) {
this.transitioning.abort();
}
this.showing = true;
this.open();
const { viewer } = this;
removeClass(viewer, CLASS_HIDE);
viewer.setAttribute('role', 'dialog');
viewer.setAttribute('aria-labelledby', this.title.id);
viewer.setAttribute('aria-modal', true);
viewer.removeAttribute('aria-hidden');
if (options.transition && !immediate) {
const shown = this.shown.bind(this);
this.transitioning = {
abort: () => {
removeListener(viewer, EVENT_TRANSITION_END, shown);
removeClass(viewer, CLASS_IN);
},
};
addClass(viewer, CLASS_TRANSITION);
// Force reflow to enable CSS3 transition
viewer.initialOffsetWidth = viewer.offsetWidth;
addListener(viewer, EVENT_TRANSITION_END, shown, {
once: true,
});
addClass(viewer, CLASS_IN);
} else {
addClass(viewer, CLASS_IN);
this.shown();
}
return this;
},
/**
* Hide the viewer (only available in modal mode)
* @param {boolean} [immediate=false] - Indicates if hide the viewer immediately or not.
* @returns {Viewer} this
*/
hide(immediate = false) {
const { element, options } = this;
if (options.inline || this.hiding || !(this.isShown || this.showing)) {
return this;
}
if (isFunction(options.hide)) {
addListener(element, EVENT_HIDE, options.hide, {
once: true,
});
}
if (dispatchEvent(element, EVENT_HIDE) === false) {
return this;
}
if (this.showing) {
this.transitioning.abort();
}
this.hiding = true;
if (this.played) {
this.stop();
} else if (this.viewing) {
this.viewing.abort();
}
const { viewer, image } = this;
const hideImmediately = () => {
removeClass(viewer, CLASS_IN);
this.hidden();
};
if (options.transition && !immediate) {
const onViewerTransitionEnd = (event) => {
// Ignore all propagating `transitionend` events (#275).
if (event && event.target === viewer) {
removeListener(viewer, EVENT_TRANSITION_END, onViewerTransitionEnd);
this.hidden();
}
};
const onImageTransitionEnd = () => {
// In case of show the viewer by `viewer.show(true)` previously (#407).
if (hasClass(viewer, CLASS_TRANSITION)) {
addListener(viewer, EVENT_TRANSITION_END, onViewerTransitionEnd);
removeClass(viewer, CLASS_IN);
} else {
hideImmediately();
}
};
this.transitioning = {
abort: () => {
if (this.viewed && hasClass(image, CLASS_TRANSITION)) {
removeListener(image, EVENT_TRANSITION_END, onImageTransitionEnd);
} else if (hasClass(viewer, CLASS_TRANSITION)) {
removeListener(viewer, EVENT_TRANSITION_END, onViewerTransitionEnd);
}
},
};
// In case of hiding the viewer when holding on the image (#255),
// note that the `CLASS_TRANSITION` class will be removed on pointer down.
if (this.viewed && hasClass(image, CLASS_TRANSITION)) {
addListener(image, EVENT_TRANSITION_END, onImageTransitionEnd, {
once: true,
});
this.zoomTo(0, false, null, null, true);
} else {
onImageTransitionEnd();
}
} else {
hideImmediately();
}
return this;
},
/**
* View one of the images with image's index
* @param {number} index - The index of the image to view.
* @returns {Viewer} this
*/
view(index = this.options.initialViewIndex) {
index = Number(index) || 0;
if (this.hiding || this.played || index < 0 || index >= this.length
|| (this.viewed && index === this.index)) {
return this;
}
if (!this.isShown) {
this.index = index;
return this.show();
}
if (this.viewing) {
this.viewing.abort();
}
const {
element,
options,
title,
canvas,
} = this;
const item = this.items[index];
const img = item.querySelector('img');
const url = getData(img, 'originalUrl');
const alt = img.getAttribute('alt');
const image = document.createElement('img');
forEach(options.inheritedAttributes, (name) => {
const value = img.getAttribute(name);
if (value !== null) {
image.setAttribute(name, value);
}
});
image.src = url;
image.alt = alt;
if (isFunction(options.view)) {
addListener(element, EVENT_VIEW, options.view, {
once: true,
});
}
if (dispatchEvent(element, EVENT_VIEW, {
originalImage: this.images[index],
index,
image,
}) === false || !this.isShown || this.hiding || this.played) {
return this;
}
const activeItem = this.items[this.index];
if (activeItem) {
removeClass(activeItem, CLASS_ACTIVE);
activeItem.removeAttribute('aria-selected');
}
addClass(item, CLASS_ACTIVE);
item.setAttribute('aria-selected', true);
if (options.focus) {
item.focus();
}
this.image = image;
this.viewed = false;
this.index = index;
this.imageData = {};
addClass(image, CLASS_INVISIBLE);
if (options.loading) {
addClass(canvas, CLASS_LOADING);
}
canvas.innerHTML = '';
canvas.appendChild(image);
// Center current item
this.renderList();
// Clear title
title.innerHTML = '';
// Generate title after viewed
const onViewed = () => {
const { imageData } = this;
const render = Array.isArray(options.title) ? options.title[1] : options.title;
title.innerHTML = escapeHTMLEntities(isFunction(render)
? render.call(this, image, imageData)
: `${alt} (${imageData.naturalWidth} × ${imageData.naturalHeight})`);
};
let onLoad;
let onError;
addListener(element, EVENT_VIEWED, onViewed, {
once: true,
});
this.viewing = {
abort: () => {
removeListener(element, EVENT_VIEWED, onViewed);
if (image.complete) {
if (this.imageRendering) {
this.imageRendering.abort();
} else if (this.imageInitializing) {
this.imageInitializing.abort();
}
} else {
// Cancel download to save bandwidth.
image.src = '';
removeListener(image, EVENT_LOAD, onLoad);
if (this.timeout) {
clearTimeout(this.timeout);
}
}
},
};
if (image.complete) {
this.load();
} else {
addListener(image, EVENT_LOAD, onLoad = () => {
removeListener(image, EVENT_ERROR, onError);
this.load();
}, {
once: true,
});
addListener(image, EVENT_ERROR, onError = () => {
removeListener(image, EVENT_LOAD, onLoad);
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = false;
}
removeClass(image, CLASS_INVISIBLE);
if (options.loading) {
removeClass(this.canvas, CLASS_LOADING);
}
}, {
once: true,
});
if (this.timeout) {
clearTimeout(this.timeout);
}
// Make the image visible if it fails to load within 1s
this.timeout = setTimeout(() => {
removeClass(image, CLASS_INVISIBLE);
this.timeout = false;
}, 1000);
}
return this;
},
/**
* View the previous image
* @param {boolean} [loop=false] - Indicate if view the last one
* when it is the first one at present.
* @returns {Viewer} this
*/
prev(loop = false) {
let index = this.index - 1;
if (index < 0) {
index = loop ? this.length - 1 : 0;
}
this.view(index);
return this;
},
/**
* View the next image
* @param {boolean} [loop=false] - Indicate if view the first one
* when it is the last one at present.
* @returns {Viewer} this
*/
next(loop = false) {
const maxIndex = this.length - 1;
let index = this.index + 1;
if (index > maxIndex) {
index = loop ? 0 : maxIndex;
}
this.view(index);
return this;
},
/**
* Move the image with relative offsets.
* @param {number} x - The moving distance in the horizontal direction.
* @param {number} [y=x] The moving distance in the vertical direction.
* @returns {Viewer} this
*/
move(x, y = x) {
const { imageData } = this;
this.moveTo(
isUndefined(x) ? x : imageData.x + Number(x),
isUndefined(y) ? y : imageData.y + Number(y),
);
return this;
},
/**
* Move the image to an absolute point.
* @param {number} x - The new position in the horizontal direction.
* @param {number} [y=x] - The new position in the vertical direction.
* @param {Event} [_originalEvent=null] - The original event if any.
* @returns {Viewer} this
*/
moveTo(x, y = x, _originalEvent = null) {
const { element, options, imageData } = this;
x = Number(x);
y = Number(y);
if (this.viewed && !this.played && options.movable) {
const oldX = imageData.x;
const oldY = imageData.y;
let changed = false;
if (isNumber(x)) {
changed = true;
} else {
x = oldX;
}
if (isNumber(y)) {
changed = true;
} else {
y = oldY;
}
if (changed) {
if (isFunction(options.move)) {
addListener(element, EVENT_MOVE, options.move, {
once: true,
});
}
if (dispatchEvent(element, EVENT_MOVE, {
x,
y,
oldX,
oldY,
originalEvent: _originalEvent,
}) === false) {
return this;
}
imageData.x = x;
imageData.y = y;
imageData.left = x;
imageData.top = y;
this.moving = true;
this.renderImage(() => {
this.moving = false;
if (isFunction(options.moved)) {
addListener(element, EVENT_MOVED, options.moved, {
once: true,
});
}
dispatchEvent(element, EVENT_MOVED, {
x,
y,
oldX,
oldY,
originalEvent: _originalEvent,
}, {
cancelable: false,
});
});
}
}
return this;
},
/**
* Rotate the image with a relative degree.
* @param {number} degree - The rotate degree.
* @returns {Viewer} this
*/
rotate(degree) {
this.rotateTo((this.imageData.rotate || 0) + Number(degree));
return this;
},
/**
* Rotate the image to an absolute degree.
* @param {number} degree - The rotate degree.
* @returns {Viewer} this
*/
rotateTo(degree) {
const { element, options, imageData } = this;
degree = Number(degree);
if (isNumber(degree) && this.viewed && !this.played && options.rotatable) {
const oldDegree = imageData.rotate;
if (isFunction(options.rotate)) {
addListener(element, EVENT_ROTATE, options.rotate, {
once: true,
});
}
if (dispatchEvent(element, EVENT_ROTATE, {
degree,
oldDegree,
}) === false) {
return this;
}
imageData.rotate = degree;
this.rotating = true;
this.renderImage(() => {
this.rotating = false;
if (isFunction(options.rotated)) {
addListener(element, EVENT_ROTATED, options.rotated, {
once: true,
});
}
dispatchEvent(element, EVENT_ROTATED, {
degree,
oldDegree,
}, {
cancelable: false,
});
});
}
return this;
},
/**
* Scale the image on the x-axis.
* @param {number} scaleX - The scale ratio on the x-axis.
* @returns {Viewer} this
*/
scaleX(scaleX) {
this.scale(scaleX, this.imageData.scaleY);
return this;
},
/**
* Scale the image on the y-axis.
* @param {number} scaleY - The scale ratio on the y-axis.
* @returns {Viewer} this
*/
scaleY(scaleY) {
this.scale(this.imageData.scaleX, scaleY);
return this;
},
/**
* Scale the image.
* @param {number} scaleX - The scale ratio on the x-axis.
* @param {number} [scaleY=scaleX] - The scale ratio on the y-axis.
* @returns {Viewer} this
*/
scale(scaleX, scaleY = scaleX) {
const { element, options, imageData } = this;
scaleX = Number(scaleX);
scaleY = Number(scaleY);
if (this.viewed && !this.played && options.scalable) {
const oldScaleX = imageData.scaleX;
const oldScaleY = imageData.scaleY;
let changed = false;
if (isNumber(scaleX)) {
changed = true;
} else {
scaleX = oldScaleX;
}
if (isNumber(scaleY)) {
changed = true;
} else {
scaleY = oldScaleY;
}
if (changed) {
if (isFunction(options.scale)) {
addListener(element, EVENT_SCALE, options.scale, {
once: true,
});
}
if (dispatchEvent(element, EVENT_SCALE, {
scaleX,
scaleY,
oldScaleX,
oldScaleY,
}) === false) {
return this;
}
imageData.scaleX = scaleX;
imageData.scaleY = scaleY;
this.scaling = true;
this.renderImage(() => {
this.scaling = false;
if (isFunction(options.scaled)) {
addListener(element, EVENT_SCALED, options.scaled, {
once: true,
});
}
dispatchEvent(element, EVENT_SCALED, {
scaleX,
scaleY,
oldScaleX,
oldScaleY,
}, {
cancelable: false,
});
});
}
}
return this;
},
/**
* Zoom the image with a relative ratio.
* @param {number} ratio - The target ratio.
* @param {boolean} [showTooltip=false] - Indicates whether to show the tooltip.
* @param {Object} [pivot] - The pivot point coordinate for zooming.
* @param {Event} [_originalEvent=null] - The original event if any.
* @returns {Viewer} this
*/
zoom(ratio, showTooltip = false, pivot = null, _originalEvent = null) {
const { imageData } = this;
ratio = Number(ratio);
if (ratio < 0) {
ratio = 1 / (1 - ratio);
} else {
ratio = 1 + ratio;
}
this.zoomTo(
(imageData.width * ratio) / imageData.naturalWidth,
showTooltip,
pivot,
_originalEvent,
);
return this;
},
/**
* Zoom the image to an absolute ratio.
* @param {number} ratio - The target ratio.
* @param {boolean} [showTooltip] - Indicates whether to show the tooltip.
* @param {Object} [pivot] - The pivot point coordinate for zooming.
* @param {Event} [_originalEvent=null] - The original event if any.
* @param {Event} [_zoomable=false] - Indicates if the current zoom is available or not.
* @returns {Viewer} this
*/
zoomTo(ratio, showTooltip = false, pivot = null, _originalEvent = null, _zoomable = false) {
const {
element,
options,
pointers,
imageData,
} = this;
const {
x,
y,
width,
height,
naturalWidth,
naturalHeight,
} = imageData;
ratio = Math.max(0, ratio);
if (isNumber(ratio) && this.viewed && !this.played && (_zoomable || options.zoomable)) {
if (!_zoomable) {
const minZoomRatio = Math.max(0.01, options.minZoomRatio);
const maxZoomRatio = Math.min(100, options.maxZoomRatio);
ratio = Math.min(Math.max(ratio, minZoomRatio), maxZoomRatio);
}
if (_originalEvent) {
switch (_originalEvent.type) {
case 'wheel':
if (options.zoomRatio >= 0.055 && ratio > 0.95 && ratio < 1.05) {
ratio = 1;
}
break;
case 'pointermove':
case 'touchmove':
case 'mousemove':
if (ratio > 0.99 && ratio < 1.01) {
ratio = 1;
}
break;
default:
}
}
const newWidth = naturalWidth * ratio;
const newHeight = naturalHeight * ratio;
const offsetWidth = newWidth - width;
const offsetHeight = newHeight - height;
const oldRatio = imageData.ratio;
if (isFunction(options.zoom)) {
addListener(element, EVENT_ZOOM, options.zoom, {
once: true,
});
}
if (dispatchEvent(element, EVENT_ZOOM, {
ratio,
oldRatio,
originalEvent: _originalEvent,
}) === false) {
return this;
}
this.zooming = true;
if (_originalEvent) {
const offset = getOffset(this.viewer);
const center = pointers && Object.keys(pointers).length > 0
? getPointersCenter(pointers)
: {
pageX: _originalEvent.pageX,
pageY: _originalEvent.pageY,
};
// Zoom from the triggering point of the event
imageData.x -= offsetWidth * (((center.pageX - offset.left) - x) / width);
imageData.y -= offsetHeight * (((center.pageY - offset.top) - y) / height);
} else if (isPlainObject(pivot) && isNumber(pivot.x) && isNumber(pivot.y)) {
imageData.x -= offsetWidth * ((pivot.x - x) / width);
imageData.y -= offsetHeight * ((pivot.y - y) / height);
} else {
// Zoom from the center of the image
imageData.x -= offsetWidth / 2;
imageData.y -= offsetHeight / 2;
}
imageData.left = imageData.x;
imageData.top = imageData.y;
imageData.width = newWidth;
imageData.height = newHeight;
imageData.oldRatio = oldRatio;
imageData.ratio = ratio;
this.renderImage(() => {
this.zooming = false;
if (isFunction(options.zoomed)) {
addListener(element, EVENT_ZOOMED, options.zoomed, {
once: true,
});
}
dispatchEvent(element, EVENT_ZOOMED, {
ratio,
oldRatio,
originalEvent: _originalEvent,
}, {
cancelable: false,
});
});
if (showTooltip) {
this.tooltip();
}
}
return this;
},
/**
* Play the images
* @param {boolean|FullscreenOptions} [fullscreen=false] - Indicate if request fullscreen or not.
* @returns {Viewer} this
*/
play(fullscreen = false) {
if (!this.isShown || this.played) {
return this;
}
const { element, options } = this;
if (isFunction(options.play)) {
addListener(element, EVENT_PLAY, options.play, {
once: true,
});
}
if (dispatchEvent(element, EVENT_PLAY) === false) {
return this;
}
const { player } = this;
const onLoad = this.loadImage.bind(this);
const list = [];
let total = 0;
let index = 0;
this.played = true;
this.onLoadWhenPlay = onLoad;
if (fullscreen) {
this.requestFullscreen(fullscreen);
}
addClass(player, CLASS_SHOW);
forEach(this.items, (item, i) => {
const img = item.querySelector('img');
const image = document.createElement('img');
image.src = getData(img, 'originalUrl');
image.alt = img.getAttribute('alt');
image.referrerPolicy = img.referrerPolicy;
total += 1;
addClass(image, CLASS_FADE);
toggleClass(image, CLASS_TRANSITION, options.transition);
if (hasClass(item, CLASS_ACTIVE)) {
addClass(image, CLASS_IN);
index = i;
}
list.push(image);
addListener(image, EVENT_LOAD, onLoad, {
once: true,
});
player.appendChild(image);
});
if (isNumber(options.interval) && options.interval > 0) {
const prev = () => {
clearTimeout(this.playing.timeout);
removeClass(list[index], CLASS_IN);
index -= 1;
index = index >= 0 ? index : total - 1;
addClass(list[index], CLASS_IN);
this.playing.timeout = setTimeout(prev, options.interval);
};
const next = () => {
clearTimeout(this.playing.timeout);
removeClass(list[index], CLASS_IN);
index += 1;
index = index < total ? index : 0;
addClass(list[index], CLASS_IN);
this.playing.timeout = setTimeout(next, options.interval);
};
if (total > 1) {
this.playing = {
prev,
next,
timeout: setTimeout(next, options.interval),
};
}
}
return this;
},
// Stop play
stop() {
if (!this.played) {
return this;
}
const { element, options } = this;
if (isFunction(options.stop)) {
addListener(element, EVENT_STOP, options.stop, {
once: true,
});
}
if (dispatchEvent(element, EVENT_STOP) === false) {
return this;
}
const { player } = this;
clearTimeout(this.playing.timeout);
this.playing = false;
this.played = false;
forEach(player.getElementsByTagName('img'), (image) => {
removeListener(image, EVENT_LOAD, this.onLoadWhenPlay);
});
removeClass(player, CLASS_SHOW);
player.innerHTML = '';
this.exitFullscreen();
return this;
},
// Enter modal mode (only available in inline mode)
full() {
const {
options,
viewer,
image,
list,
} = this;
if (!this.isShown || this.played || this.fulled || !options.inline) {
return this;
}
this.fulled = true;
this.open();
addClass(this.button, CLASS_FULLSCREEN_EXIT);
if (options.transition) {
removeClass(list, CLASS_TRANSITION);
if (this.viewed) {
removeClass(image, CLASS_TRANSITION);
}
}
addClass(viewer, CLASS_FIXED);
viewer.setAttribute('role', 'dialog');
viewer.setAttribute('aria-labelledby', this.title.id);
viewer.setAttribute('aria-modal', true);
viewer.removeAttribute('style');
setStyle(viewer, {
zIndex: options.zIndex,
});
if (options.focus) {
this.enforceFocus();
}
this.initContainer();
this.viewerData = assign({}, this.containerData);
this.renderList();
if (this.viewed) {
this.initImage(() => {
this.renderImage(() => {
if (options.transition) {
setTimeout(() => {
addClass(image, CLASS_TRANSITION);
addClass(list, CLASS_TRANSITION);
}, 0);
}
});
});
}
return this;
},
// Exit modal mode (only available in inline mode)
exit() {
const {
options,
viewer,
image,
list,
} = this;
if (!this.isShown || this.played || !this.fulled || !options.inline) {
return this;
}
this.fulled = false;
this.close();
removeClass(this.button, CLASS_FULLSCREEN_EXIT);
if (options.transition) {
removeClass(list, CLASS_TRANSITION);
if (this.viewed) {
removeClass(image, CLASS_TRANSITION);
}
}
if (options.focus) {
this.clearEnforceFocus();
}
viewer.removeAttribute('role');
viewer.removeAttribute('aria-labelledby');
viewer.removeAttribute('aria-modal');
removeClass(viewer, CLASS_FIXED);
setStyle(viewer, {
zIndex: options.zIndexInline,
});
this.viewerData = assign({}, this.parentData);
this.renderViewer();
this.renderList();
if (this.viewed) {
this.initImage(() => {
this.renderImage(() => {
if (options.transition) {
setTimeout(() => {
addClass(image, CLASS_TRANSITION);
addClass(list, CLASS_TRANSITION);
}, 0);
}
});
});
}
return this;
},
// Show the current ratio of the image with percentage
tooltip() {
const { options, tooltipBox, imageData } = this;
if (!this.viewed || this.played || !options.tooltip) {
return this;
}
tooltipBox.textContent = `${Math.round(imageData.ratio * 100)}%`;
if (!this.tooltipping) {
if (options.transition) {
if (this.fading) {
dispatchEvent(tooltipBox, EVENT_TRANSITION_END);
}
addClass(tooltipBox, CLASS_SHOW);
addClass(tooltipBox, CLASS_FADE);
addClass(tooltipBox, CLASS_TRANSITION);
tooltipBox.removeAttribute('aria-hidden');
// Force reflow to enable CSS3 transition
tooltipBox.initialOffsetWidth = tooltipBox.offsetWidth;
addClass(tooltipBox, CLASS_IN);
} else {
addClass(tooltipBox, CLASS_SHOW);
tooltipBox.removeAttribute('aria-hidden');
}
} else {
clearTimeout(this.tooltipping);
}
this.tooltipping = setTimeout(() => {
if (options.transition) {
addListener(tooltipBox, EVENT_TRANSITION_END, () => {
removeClass(tooltipBox, CLASS_SHOW);
removeClass(tooltipBox, CLASS_FADE);
removeClass(tooltipBox, CLASS_TRANSITION);
tooltipBox.setAttribute('aria-hidden', true);
this.fading = false;
}, {
once: true,
});
removeClass(tooltipBox, CLASS_IN);
this.fading = true;
} else {
removeClass(tooltipBox, CLASS_SHOW);
tooltipBox.setAttribute('aria-hidden', true);
}
this.tooltipping = false;
}, 1000);
return this;
},
/**
* Toggle the image size between its current size and natural size
* @param {Event} [_originalEvent=null] - The original event if any.
* @returns {Viewer} this
*/
toggle(_originalEvent = null) {
if (this.imageData.ratio === 1) {
this.zoomTo(this.imageData.oldRatio, true, null, _originalEvent);
} else {
this.zoomTo(1, true, null, _originalEvent);
}
return this;
},
// Reset the image to its initial state
reset() {
if (this.viewed && !this.played) {
this.imageData = assign({}, this.initialImageData);
this.renderImage();
}
return this;
},
// Update viewer when images changed
update() {
const { element, options, isImg } = this;
// Destroy viewer if the target image was deleted
if (isImg && !element.parentNode) {
return this.destroy();
}
const images = [];
forEach(isImg ? [element] : element.querySelectorAll('img'), (image) => {
if (isFunction(options.filter)) {
if (options.filter.call(this, image)) {
images.push(image);
}
} else if (this.getImageURL(image)) {
images.push(image);
}
});
if (!images.length) {
return this;
}
this.images = images;
this.length = images.length;
if (this.ready) {
const changedIndexes = [];
forEach(this.items, (item, i) => {
const img = item.querySelector('img');
const image = images[i];
if (image && img) {
if (
image.src !== img.src
// Title changed (#408)
|| image.alt !== img.alt
) {
changedIndexes.push(i);
}
} else {
changedIndexes.push(i);
}
});
setStyle(this.list, {
width: 'auto',
});
this.initList();
if (this.isShown) {
if (this.length) {
if (this.viewed) {
const changedIndex = changedIndexes.indexOf(this.index);
if (changedIndex >= 0) {
this.viewed = false;
this.view(Math.max(Math.min(this.index - changedIndex, this.length - 1), 0));
} else {
const activeItem = this.items[this.index];
// Reactivate the current viewing item after reset the list.
addClass(activeItem, CLASS_ACTIVE);
activeItem.setAttribute('aria-selected', true);
}
}
} else {
this.image = null;
this.viewed = false;
this.index = 0;
this.imageData = {};
this.canvas.innerHTML = '';
this.title.innerHTML = '';
}
}
} else {
this.build();
}
return this;
},
// Destroy the viewer
destroy() {
const { element, options } = this;
if (!element[NAMESPACE]) {
return this;
}
this.destroyed = true;
if (this.ready) {
if (this.played) {
this.stop();
}
if (options.inline) {
if (this.fulled) {
this.exit();
}
this.unbind();
} else if (this.isShown) {
if (this.viewing) {
if (this.imageRendering) {
this.imageRendering.abort();
} else if (this.imageInitializing) {
this.imageInitializing.abort();
}
}
if (this.hiding) {
this.transitioning.abort();
}
this.hidden();
} else if (this.showing) {
this.transitioning.abort();
this.hidden();
}
this.ready = false;
this.viewer.parentNode.removeChild(this.viewer);
} else if (options.inline) {
if (this.delaying) {
this.delaying.abort();
} else if (this.initializing) {
this.initializing.abort();
}
}
if (!options.inline) {
removeListener(element, EVENT_CLICK, this.onStart);
}
element[NAMESPACE] = undefined;
return this;
},
};