viewerjs
Version:
JavaScript image viewer.
316 lines (259 loc) • 7.66 kB
JavaScript
import {
CLASS_LOADING,
CLASS_TRANSITION,
EVENT_ERROR,
EVENT_LOAD,
EVENT_TRANSITION_END,
EVENT_VIEWED,
} from './constants';
import {
addClass,
addListener,
assign,
forEach,
getImageNameFromURL,
getImageNaturalSizes,
getTransforms,
hasClass,
isNumber,
removeClass,
removeListener,
setData,
setStyle,
} from './utilities';
export default {
render() {
this.initContainer();
this.initViewer();
this.initList();
this.renderViewer();
},
initBody() {
const { ownerDocument } = this.element;
const body = ownerDocument.body || ownerDocument.documentElement;
this.body = body;
this.scrollbarWidth = window.innerWidth - ownerDocument.documentElement.clientWidth;
this.initialBodyPaddingRight = body.style.paddingRight;
this.initialBodyComputedPaddingRight = window.getComputedStyle(body).paddingRight;
},
initContainer() {
this.containerData = {
width: window.innerWidth,
height: window.innerHeight,
};
},
initViewer() {
const { options, parent } = this;
let viewerData;
if (options.inline) {
viewerData = {
width: Math.max(parent.offsetWidth, options.minWidth),
height: Math.max(parent.offsetHeight, options.minHeight),
};
this.parentData = viewerData;
}
if (this.fulled || !viewerData) {
viewerData = this.containerData;
}
this.viewerData = assign({}, viewerData);
},
renderViewer() {
if (this.options.inline && !this.fulled) {
setStyle(this.viewer, this.viewerData);
}
},
initList() {
const { element, options, list } = this;
const items = [];
// initList may be called in this.update, so should keep idempotent
list.innerHTML = '';
forEach(this.images, (image, index) => {
const { src } = image;
const alt = image.alt || getImageNameFromURL(src);
const url = this.getImageURL(image);
if (src || url) {
const item = document.createElement('li');
const img = document.createElement('img');
forEach(options.inheritedAttributes, (name) => {
const value = image.getAttribute(name);
if (value !== null) {
img.setAttribute(name, value);
}
});
if (options.navbar) {
img.src = src || url;
}
img.alt = alt;
img.setAttribute('data-original-url', url || src);
item.setAttribute('data-index', index);
item.setAttribute('data-viewer-action', 'view');
item.setAttribute('role', 'button');
if (options.keyboard) {
item.setAttribute('tabindex', 0);
}
item.appendChild(img);
list.appendChild(item);
items.push(item);
}
});
this.items = items;
forEach(items, (item) => {
const image = item.firstElementChild;
let onLoad;
let onError;
setData(image, 'filled', true);
if (options.loading) {
addClass(item, CLASS_LOADING);
}
addListener(image, EVENT_LOAD, onLoad = (event) => {
removeListener(image, EVENT_ERROR, onError);
if (options.loading) {
removeClass(item, CLASS_LOADING);
}
this.loadImage(event);
}, {
once: true,
});
addListener(image, EVENT_ERROR, onError = () => {
removeListener(image, EVENT_LOAD, onLoad);
if (options.loading) {
removeClass(item, CLASS_LOADING);
}
}, {
once: true,
});
});
if (options.transition) {
addListener(element, EVENT_VIEWED, () => {
addClass(list, CLASS_TRANSITION);
}, {
once: true,
});
}
},
renderList() {
const { index } = this;
const item = this.items[index];
if (!item) {
return;
}
const next = item.nextElementSibling;
const gutter = parseInt(window.getComputedStyle(next || item).marginLeft, 10);
const { offsetWidth } = item;
const outerWidth = offsetWidth + gutter;
// Place the active item in the center of the screen
setStyle(this.list, assign({
width: outerWidth * this.length - gutter,
}, getTransforms({
translateX: ((this.viewerData.width - offsetWidth) / 2) - outerWidth * index,
})));
},
resetList() {
const { list } = this;
list.innerHTML = '';
removeClass(list, CLASS_TRANSITION);
setStyle(list, getTransforms({
translateX: 0,
}));
},
initImage(done) {
const { options, image, viewerData } = this;
const footerHeight = this.footer.offsetHeight;
const viewerWidth = viewerData.width;
const viewerHeight = Math.max(viewerData.height - footerHeight, footerHeight);
const oldImageData = this.imageData || {};
let sizingImage;
this.imageInitializing = {
abort: () => {
sizingImage.onload = null;
},
};
sizingImage = getImageNaturalSizes(image, options, (naturalWidth, naturalHeight) => {
const aspectRatio = naturalWidth / naturalHeight;
let initialCoverage = Math.max(0, Math.min(1, options.initialCoverage));
let width = viewerWidth;
let height = viewerHeight;
this.imageInitializing = false;
if (viewerHeight * aspectRatio > viewerWidth) {
height = viewerWidth / aspectRatio;
} else {
width = viewerHeight * aspectRatio;
}
initialCoverage = isNumber(initialCoverage) ? initialCoverage : 0.9;
width = Math.min(width * initialCoverage, naturalWidth);
height = Math.min(height * initialCoverage, naturalHeight);
const left = (viewerWidth - width) / 2;
const top = (viewerHeight - height) / 2;
const imageData = {
left,
top,
x: left,
y: top,
width,
height,
oldRatio: 1,
ratio: width / naturalWidth,
aspectRatio,
naturalWidth,
naturalHeight,
};
const initialImageData = assign({}, imageData);
if (options.rotatable) {
imageData.rotate = oldImageData.rotate || 0;
initialImageData.rotate = 0;
}
if (options.scalable) {
imageData.scaleX = oldImageData.scaleX || 1;
imageData.scaleY = oldImageData.scaleY || 1;
initialImageData.scaleX = 1;
initialImageData.scaleY = 1;
}
this.imageData = imageData;
this.initialImageData = initialImageData;
if (done) {
done();
}
});
},
renderImage(done) {
const { image, imageData } = this;
setStyle(image, assign({
width: imageData.width,
height: imageData.height,
// XXX: Not to use translateX/Y to avoid image shaking when zooming
marginLeft: imageData.x,
marginTop: imageData.y,
}, getTransforms(imageData)));
if (done) {
if ((this.viewing || this.moving || this.rotating || this.scaling || this.zooming)
&& this.options.transition
&& hasClass(image, CLASS_TRANSITION)) {
const onTransitionEnd = () => {
this.imageRendering = false;
done();
};
this.imageRendering = {
abort: () => {
removeListener(image, EVENT_TRANSITION_END, onTransitionEnd);
},
};
addListener(image, EVENT_TRANSITION_END, onTransitionEnd, {
once: true,
});
} else {
done();
}
}
},
resetImage() {
const { image } = this;
if (image) {
if (this.viewing) {
this.viewing.abort();
}
image.parentNode.removeChild(image);
this.image = null;
this.title.innerHTML = '';
}
},
};