Source: document-library-preview-image/src/main/resources/META-INF/resources/preview/js/ImagePreviewer.es.js

/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library 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 Lesser General Public License for more
 * details.
 */

import {Config} from 'metal-state';
import {debounce} from 'frontend-js-web';
import Component from 'metal-component';
import Soy from 'metal-soy';
import 'clay-button';

import templates from './ImagePreviewer.soy';

/**
 * Zoom ratio limit that fire the autocenter
 * @type {number}
 */
const MIN_ZOOM_RATIO_AUTOCENTER = 3;

/**
 * Available zoom sizes
 * @type {Array<number>}
 */
const ZOOM_LEVELS = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];

/**
 * Available reversed zoom sizes
 * @type {Array<number>}
 */
const ZOOM_LEVELS_REVERSED = ZOOM_LEVELS.slice().reverse();

/**
 * Component that create an image preview to allow zoom
 * @review
 */
class ImagePreviewer extends Component {
	/**
	 * @inheritDoc
	 */
	attached() {
		this._imageNaturalHeight = this.refs.image.naturalHeight;
		this._imageNaturalWidth = this.refs.image.naturalWidth;
		this._isPreviewFit = true;

		this._updateDimensions();

		this._updateDimensionsDebounced = debounce(
			this._updateDimensions.bind(this),
			250
		);
		window.addEventListener('resize', this._updateDimensionsDebounced);
	}

	/**
	 * @inheritDoc
	 */
	detached() {
		window.removeEventListener('resize', this._updateDimensionsDebounced);
	}

	/**
	 * @inheritDoc
	 */
	rendered() {
		if (this._zoomRatio) {
			this._setScrollContainer();
		}

		if (this._reCalculateCurrentZoom) {
			this._reCalculateCurrentZoom = false;

			this._calculateCurrentZoom();
		}
	}

	/**
	 * @inheritDoc
	 */
	syncCurrentZoom() {
		this.zoomInDisabled = ZOOM_LEVELS_REVERSED[0] === this.currentZoom;
		this.zoomOutDisabled = ZOOM_LEVELS[0] >= this.currentZoom;
	}

	/**
	 * Set the zoom based in multiplier
	 * @param {number} zoomNumber
	 * @private
	 * @review
	 */
	_applyZoom(zoomNumber) {
		this.imageHeight = this._imageNaturalHeight * zoomNumber;
		this.imageWidth = this._imageNaturalWidth * zoomNumber;
		this._zoomRatio = zoomNumber / this.currentZoom;
		this._isPreviewFit = false;
		this.currentZoom = zoomNumber;
		this._updateDimensions();
	}

	/**
	 * Calculate actual zoom based in image rendered
	 * @private
	 * @review
	 */
	_calculateCurrentZoom() {
		this.currentZoom = this.refs.image.width / this._imageNaturalWidth;
	}

	/**
	 * Clear zoom and allow the image fit the container in natural way
	 * @private
	 * @review
	 */
	_clearZoom() {
		this.imageHeight = null;
		this.imageMargin = null;
		this.imageWidth = null;
		this._isPreviewFit = true;
		this._reCalculateCurrentZoom = true;
	}

	/**
	 * Event handler executed when zoom changed
	 * @param {!Event} event
	 * @private
	 * @review
	 */
	_handleToolbarClick(event) {
		const value = event.currentTarget.value;

		let zoomValue;

		if (value === 'in') {
			zoomValue = ZOOM_LEVELS.find(zoom => zoom > this.currentZoom);
		} else if (value === 'out') {
			zoomValue = ZOOM_LEVELS_REVERSED.find(
				zoom => zoom < this.currentZoom
			);
		} else if (value === 'real') {
			zoomValue = 1;
		} else if (value === 'fit') {
			this._clearZoom();
		}

		if (zoomValue) {
			this._applyZoom(zoomValue);
		}
	}

	/**
	 * Move the scroll of the cointainer based in the actual position or center
	 * @private
	 * @review
	 */
	_setScrollContainer() {
		const imageContainer = this.refs.imageContainer;
		let scrollLeft;
		let scrollTop;

		if (this._zoomRatio < MIN_ZOOM_RATIO_AUTOCENTER) {
			scrollLeft =
				(imageContainer.clientWidth * (this._zoomRatio - 1)) / 2 +
				imageContainer.scrollLeft * this._zoomRatio;
			scrollTop =
				(imageContainer.clientHeight * (this._zoomRatio - 1)) / 2 +
				imageContainer.scrollTop * this._zoomRatio;
		} else {
			scrollTop = (this.imageHeight - imageContainer.clientHeight) / 2;
			scrollLeft = (this.imageWidth - imageContainer.clientWidth) / 2;
		}

		imageContainer.scrollLeft = scrollLeft;
		imageContainer.scrollTop = scrollTop;

		this._zoomRatio = null;
	}

	/**
	 * Calculate actual dimensions based in container rendered
	 * @private
	 * @review
	 */
	_updateDimensions() {
		this.imageMargin = `${
			this.imageHeight > this.refs.imageContainer.clientHeight
				? 0
				: 'auto'
		} ${
			this.imageWidth > this.refs.imageContainer.clientWidth ? 0 : 'auto'
		}`;

		if (this._isPreviewFit) {
			this._calculateCurrentZoom();
		}
	}
}

/**
 * State definition.
 * @review
 * @static
 * @type {!Object}
 */
ImagePreviewer.STATE = {
	/**
	 * The current zoom value that is shown in the toolbar.
	 * @type {Number}
	 */
	currentZoom: Config.number().internal(),

	/**
	 * The height of the <img> element.
	 * @type {Number}
	 */
	imageHeight: Config.number().internal(),

	/**
	 * The margin of the <img> element
	 * @type {String}
	 */
	imageMargin: Config.string().internal(),

	/**
	 * The "src" attribute of the <img> element
	 * @type {String}
	 */
	imageURL: Config.string().required(),

	/**
	 * The width of the <img> element.
	 * @type {Number}
	 */
	imageWidth: Config.number().internal(),

	/**
	 * Path to icon images.
	 * @type {String}
	 */
	spritemap: Config.string().required(),

	/**
	 * Flag that indicate if 'zoom in' is disabled.
	 * @type {Boolean}
	 */
	zoomInDisabled: Config.bool().internal(),

	/**
	 * Flag that indicate if 'zoom out' is disabled.
	 * @type {Boolean}
	 */
	zoomOutDisabled: Config.bool().internal()
};

Soy.register(ImagePreviewer, templates);

export {ImagePreviewer};
export default ImagePreviewer;