vanilla-js-wheel-zoom

Image resizing using mouse wheel + drag scrollable image (as well as any HTML content)

Ce script ne devrait pas être installé directement. C'est une librairie créée pour d'autres scripts. Elle doit être inclus avec la commande // @require https://update.greasyfork.org/scripts/420842/927891/vanilla-js-wheel-zoom.js

(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined'
        ? (module.exports = factory())
        : typeof define === 'function' && define.amd
        ? define(factory)
        : ((global =
              typeof globalThis !== 'undefined' ? globalThis : global || self),
          (global.WZoom = factory()));
})(this, function () {
    'use strict';

    /**
     * Get element position (with support old browsers)
     * @param {Element} element
     * @returns {{top: number, left: number}}
     */
    function getElementPosition(element) {
        var box = element.getBoundingClientRect();
        var _document = document,
            body = _document.body,
            documentElement = _document.documentElement;
        var scrollTop =
            window.pageYOffset || documentElement.scrollTop || body.scrollTop;
        var scrollLeft =
            window.pageXOffset || documentElement.scrollLeft || body.scrollLeft;
        var clientTop = documentElement.clientTop || body.clientTop || 0;
        var clientLeft = documentElement.clientLeft || body.clientLeft || 0;
        var top = box.top + scrollTop - clientTop;
        var left = box.left + scrollLeft - clientLeft;
        return {
            top: top,
            left: left,
        };
    }
    /**
     * Universal alternative to Object.assign()
     * @param {Object} destination
     * @param {Object} source
     * @returns {Object}
     */

    function extendObject(destination, source) {
        if (destination && source) {
            for (var key in source) {
                if (source.hasOwnProperty(key)) {
                    destination[key] = source[key];
                }
            }
        }

        return destination;
    }
    /**
     * @param target
     * @param type
     * @param listener
     * @param options
     */

    function on(target, type, listener) {
        var options =
            arguments.length > 3 && arguments[3] !== undefined
                ? arguments[3]
                : false;
        target.addEventListener(type, listener, options);
    }
    /**
     * @param target
     * @param type
     * @param listener
     * @param options
     */

    function off(target, type, listener) {
        var options =
            arguments.length > 3 && arguments[3] !== undefined
                ? arguments[3]
                : false;
        target.removeEventListener(type, listener, options);
    }
    function isTouch() {
        return (
            'ontouchstart' in window ||
            navigator.MaxTouchPoints > 0 ||
            navigator.msMaxTouchPoints > 0
        );
    }
    function eventClientX(event) {
        return event.type === 'wheel' ||
            event.type === 'mousedown' ||
            event.type === 'mousemove' ||
            event.type === 'mouseup'
            ? event.clientX
            : event.changedTouches[0].clientX;
    }
    function eventClientY(event) {
        return event.type === 'wheel' ||
            event.type === 'mousedown' ||
            event.type === 'mousemove' ||
            event.type === 'mouseup'
            ? event.clientY
            : event.changedTouches[0].clientY;
    }

    /**
     * @class DragScrollable
     * @param {Object} windowObject
     * @param {Object} contentObject
     * @param {Object} options
     * @constructor
     */

    function DragScrollable(windowObject, contentObject) {
        var options =
            arguments.length > 2 && arguments[2] !== undefined
                ? arguments[2]
                : {};
        this._dropHandler = this._dropHandler.bind(this);
        this._grabHandler = this._grabHandler.bind(this);
        this._moveHandler = this._moveHandler.bind(this);
        this.options = extendObject(
            {
                // smooth extinction moving element after set loose
                smoothExtinction: false,
                // callback triggered when grabbing an element
                onGrab: null,
                // callback triggered when moving an element
                onMove: null,
                // callback triggered when dropping an element
                onDrop: null,
            },
            options
        ); // check if we're using a touch screen

        this.isTouch = isTouch(); // switch to touch events if using a touch screen

        this.events = this.isTouch
            ? {
                  grab: 'touchstart',
                  move: 'touchmove',
                  drop: 'touchend',
              }
            : {
                  grab: 'mousedown',
                  move: 'mousemove',
                  drop: 'mouseup',
              }; // for the touch screen we set the parameter forcibly

        this.events.options = this.isTouch
            ? {
                  passive: false,
              }
            : false;
        this.window = windowObject;
        this.content = contentObject;
        on(
            this.content.$element,
            this.events.grab,
            this._grabHandler,
            this.events.options
        );
    }

    DragScrollable.prototype = {
        constructor: DragScrollable,
        window: null,
        content: null,
        isTouch: false,
        isGrab: false,
        events: null,
        moveTimer: null,
        options: {},
        coordinates: null,
        speed: null,
        _grabHandler: function _grabHandler(event) {
            // if touch started (only one finger) or pressed left mouse button
            if (
                (this.isTouch && event.touches.length === 1) ||
                event.buttons === 1
            ) {
                event.preventDefault();
                this.isGrab = true;
                this.coordinates = {
                    left: eventClientX(event),
                    top: eventClientY(event),
                };
                this.speed = {
                    x: 0,
                    y: 0,
                };
                on(
                    document,
                    this.events.drop,
                    this._dropHandler,
                    this.events.options
                );
                on(
                    document,
                    this.events.move,
                    this._moveHandler,
                    this.events.options
                );

                if (typeof this.options.onGrab === 'function') {
                    this.options.onGrab();
                }
            }
        },
        _dropHandler: function _dropHandler(event) {
            event.preventDefault();
            this.isGrab = false; // if (this.options.smoothExtinction) {
            //     _moveExtinction.call(this, 'scrollLeft', numberExtinction(this.speed.x));
            //     _moveExtinction.call(this, 'scrollTop', numberExtinction(this.speed.y));
            // }

            off(document, this.events.drop, this._dropHandler);
            off(document, this.events.move, this._moveHandler);

            if (typeof this.options.onDrop === 'function') {
                this.options.onDrop();
            }
        },
        _moveHandler: function _moveHandler(event) {
            if (this.isTouch && event.touches.length > 1) return false;
            event.preventDefault();
            var window = this.window,
                content = this.content,
                speed = this.speed,
                coordinates = this.coordinates,
                options = this.options; // speed of change of the coordinate of the mouse cursor along the X/Y axis

            speed.x = eventClientX(event) - coordinates.left;
            speed.y = eventClientY(event) - coordinates.top;
            clearTimeout(this.moveTimer); // reset speed data if cursor stops

            this.moveTimer = setTimeout(function () {
                speed.x = 0;
                speed.y = 0;
            }, 50);
            var contentNewLeft = content.currentLeft + speed.x;
            var contentNewTop = content.currentTop + speed.y;
            var maxAvailableLeft =
                (content.currentWidth - window.originalWidth) / 2 +
                content.correctX;
            var maxAvailableTop =
                (content.currentHeight - window.originalHeight) / 2 +
                content.correctY; // if we do not go beyond the permissible boundaries of the window

            if (Math.abs(contentNewLeft) <= maxAvailableLeft)
                content.currentLeft = contentNewLeft; // if we do not go beyond the permissible boundaries of the window

            if (Math.abs(contentNewTop) <= maxAvailableTop)
                content.currentTop = contentNewTop;

            _transform(content.$element, {
                left: content.currentLeft,
                top: content.currentTop,
                scale: content.currentScale,
            });

            coordinates.left = eventClientX(event);
            coordinates.top = eventClientY(event);

            if (typeof options.onMove === 'function') {
                options.onMove();
            }
        },
        destroy: function destroy() {
            off(
                this.content.$element,
                this.events.grab,
                this._grabHandler,
                this.events.options
            );

            for (var key in this) {
                if (this.hasOwnProperty(key)) {
                    this[key] = null;
                }
            }
        },
    };

    function _transform($element, _ref) {
        var left = _ref.left,
            top = _ref.top,
            scale = _ref.scale;
        $element.style.transform = 'translate3d('
            .concat(left, 'px, ')
            .concat(top, 'px, 0px) scale(')
            .concat(scale, ')');
    } // function _moveExtinction(field, speedArray) {

    /**
     * @class WZoom
     * @param {string|HTMLElement} selectorOrHTMLElement
     * @param {Object} options
     * @constructor
     */

    function WZoom(selectorOrHTMLElement) {
        var options =
            arguments.length > 1 && arguments[1] !== undefined
                ? arguments[1]
                : {};
        this._init = this._init.bind(this);
        this._prepare = this._prepare.bind(this);
        this._computeNewScale = this._computeNewScale.bind(this);
        this._computeNewPosition = this._computeNewPosition.bind(this);
        this._transform = this._transform.bind(this);
        this._wheelHandler = _wheelHandler.bind(this);
        this._downHandler = _downHandler.bind(this);
        this._upHandler = _upHandler.bind(this);
        this._zoomTwoFingers_TouchmoveHandler = _zoomTwoFingers_TouchmoveHandler.bind(
            this
        );
        this._zoomTwoFingers_TouchendHandler = _zoomTwoFingers_TouchendHandler.bind(
            this
        );
        /********************/

        /********************/

        this.content = {};
        this.window = {};
        this.isTouch = false;
        this.events = null;
        this.direction = 1;
        this.options = null;
        this.dragScrollable = null; // processing of the event "max / min zoom" begin only if there was really just a click
        // so as not to interfere with the DragScrollable module

        this.clickExpired = true;
        /********************/

        /********************/

        var defaults = {
            // type content: `image` - only one image, `html` - any HTML content
            type: 'image',
            // for type `image` computed auto (if width set null), for type `html` need set real html content width, else computed auto
            width: null,
            // for type `image` computed auto (if height set null), for type `html` need set real html content height, else computed auto
            height: null,
            // drag scrollable content
            dragScrollable: true,
            // options for the DragScrollable module
            dragScrollableOptions: {},
            // minimum allowed proportion of scale
            minScale: null,
            // maximum allowed proportion of scale
            maxScale: 1,
            // content resizing speed
            speed: 50,
            // zoom to maximum (minimum) size on click
            zoomOnClick: true,
            // if is true, then when the source image changes, the plugin will automatically restart init function (used with type = image)
            // attention: if false, it will work correctly only if the images are of the same size
            watchImageChange: true,
        };

        if (typeof selectorOrHTMLElement === 'string') {
            this.content.$element = document.querySelector(
                selectorOrHTMLElement
            );
        } else if (selectorOrHTMLElement instanceof HTMLElement) {
            this.content.$element = selectorOrHTMLElement;
        } else {
            throw 'WZoom: `selectorOrHTMLElement` must be selector or HTMLElement, and not '.concat(
                {}.toString.call(selectorOrHTMLElement)
            );
        } // check if we're using a touch screen

        this.isTouch = isTouch(); // switch to touch events if using a touch screen

        this.events = this.isTouch
            ? {
                  down: 'touchstart',
                  up: 'touchend',
              }
            : {
                  down: 'mousedown',
                  up: 'mouseup',
              }; // if using touch screen tells the browser that the default action will not be undone

        this.events.options = this.isTouch
            ? {
                  passive: true,
              }
            : false;

        if (this.content.$element) {
            this.options = extendObject(defaults, options);

            if (
                this.options.minScale &&
                this.options.minScale >= this.options.maxScale
            ) {
                this.options.minScale = null;
            } // for window take just the parent

            this.window.$element = this.content.$element.parentNode;

            if (this.options.type === 'image') {
                var initAlreadyDone = false; // if the `image` has already been loaded

                if (this.content.$element.complete) {
                    this._init();

                    initAlreadyDone = true;
                }

                if (
                    !initAlreadyDone ||
                    this.options.watchImageChange === true
                ) {
                    // even if the `image` has already been loaded (for "hotswap" of src support)
                    on(
                        this.content.$element,
                        'load',
                        this._init, // if watchImageChange == false listen add only until the first call
                        this.options.watchImageChange
                            ? false
                            : {
                                  once: true,
                              }
                    );
                }
            } else {
                this._init();
            }
        }
    }

    WZoom.prototype = {
        constructor: WZoom,
        _init: function _init() {
            this._prepare(); // support for zoom and pinch on touch screen devices

            if (this.isTouch) {
                this.fingersHypot = null;
                this.zoomPinchWasDetected = false;
                on(
                    this.content.$element,
                    'touchmove',
                    this._zoomTwoFingers_TouchmoveHandler
                );
                on(
                    this.content.$element,
                    'touchend',
                    this._zoomTwoFingers_TouchendHandler
                );
            }

            if (this.options.dragScrollable === true) {
                // this can happen if the src of this.content.$element (when type = image) is changed and repeat event load at image
                if (this.dragScrollable) {
                    this.dragScrollable.destroy();
                }

                this.dragScrollable = new DragScrollable(
                    this.window,
                    this.content,
                    this.options.dragScrollableOptions
                );
            }

            on(this.content.$element, 'wheel', this._wheelHandler);

            if (this.options.zoomOnClick) {
                on(
                    this.content.$element,
                    this.events.down,
                    this._downHandler,
                    this.events.options
                );
                on(
                    this.content.$element,
                    this.events.up,
                    this._upHandler,
                    this.events.options
                );
            }
        },
        _prepare: function _prepare() {
            var windowPosition = getElementPosition(this.window.$element); // original window sizes and position

            this.window.originalWidth = this.window.$element.offsetWidth;
            this.window.originalHeight = this.window.$element.offsetHeight;
            this.window.positionLeft = windowPosition.left;
            this.window.positionTop = windowPosition.top; // original content sizes

            if (this.options.type === 'image') {
                this.content.originalWidth =
                    this.options.width || this.content.$element.naturalWidth;
                this.content.originalHeight =
                    this.options.height || this.content.$element.naturalHeight;
            } else {
                this.content.originalWidth =
                    this.options.width || this.content.$element.offsetWidth;
                this.content.originalHeight =
                    this.options.height || this.content.$element.offsetHeight;
            } // minScale && maxScale

            this.content.minScale =
                this.options.minScale ||
                Math.min(
                    this.window.originalWidth / this.content.originalWidth,
                    this.window.originalHeight / this.content.originalHeight
                );
            this.content.maxScale = this.options.maxScale; // current content sizes and transform data

            this.content.currentWidth =
                this.content.originalWidth * this.content.minScale;
            this.content.currentHeight =
                this.content.originalHeight * this.content.minScale;
            this.content.currentLeft = 0;
            this.content.currentTop = 0;
            this.content.currentScale = this.content.minScale; // calculate indent-left and indent-top to of content from window borders

            this.content.correctX = Math.max(
                0,
                (this.window.originalWidth - this.content.currentWidth) / 2
            );
            this.content.correctY = Math.max(
                0,
                (this.window.originalHeight - this.content.currentHeight) / 2
            );
            this.content.$element.style.transform = 'translate3d(0px, 0px, 0px) scale('.concat(
                this.content.minScale,
                ')'
            );

            if (typeof this.options.prepare === 'function') {
                this.options.prepare();
            }
        },
        _computeNewScale: function _computeNewScale(delta) {
            this.direction = delta < 0 ? 1 : -1;
            var _this$content = this.content,
                minScale = _this$content.minScale,
                maxScale = _this$content.maxScale,
                currentScale = _this$content.currentScale;
            var contentNewScale =
                currentScale + this.direction / this.options.speed;

            if (contentNewScale < minScale) {
                this.direction = 1;
            } else if (contentNewScale > maxScale) {
                this.direction = -1;
            }

            return contentNewScale < minScale
                ? minScale
                : contentNewScale > maxScale
                ? maxScale
                : contentNewScale;
        },
        _computeNewPosition: function _computeNewPosition(
            contentNewScale,
            _ref
        ) {
            var x = _ref.x,
                y = _ref.y;
            var window = this.window,
                content = this.content;
            var contentNewWidth = content.originalWidth * contentNewScale;
            var contentNewHeight = content.originalHeight * contentNewScale;
            var _document = document,
                body = _document.body,
                documentElement = _document.documentElement;
            var scrollLeft =
                window.pageXOffset ||
                documentElement.scrollLeft ||
                body.scrollLeft;
            var scrollTop =
                window.pageYOffset ||
                documentElement.scrollTop ||
                body.scrollTop; // calculate the parameters along the X axis

            var leftWindowShiftX = x + scrollLeft - window.positionLeft;
            var centerWindowShiftX =
                window.originalWidth / 2 - leftWindowShiftX;
            var centerContentShiftX = centerWindowShiftX + content.currentLeft;
            var contentNewLeft =
                centerContentShiftX * (contentNewWidth / content.currentWidth) -
                centerContentShiftX +
                content.currentLeft; // check that the content does not go beyond the X axis

            if (
                this.direction === -1 &&
                (contentNewWidth - window.originalWidth) / 2 +
                    content.correctX <
                    Math.abs(contentNewLeft)
            ) {
                var positive = contentNewLeft < 0 ? -1 : 1;
                contentNewLeft =
                    ((contentNewWidth - window.originalWidth) / 2 +
                        content.correctX) *
                    positive;
            } // calculate the parameters along the Y axis

            var topWindowShiftY = y + scrollTop - window.positionTop;
            var centerWindowShiftY =
                window.originalHeight / 2 - topWindowShiftY;
            var centerContentShiftY = centerWindowShiftY + content.currentTop;
            var contentNewTop =
                centerContentShiftY *
                    (contentNewHeight / content.currentHeight) -
                centerContentShiftY +
                content.currentTop; // check that the content does not go beyond the Y axis

            if (
                this.direction === -1 &&
                (contentNewHeight - window.originalHeight) / 2 +
                    content.correctY <
                    Math.abs(contentNewTop)
            ) {
                var _positive = contentNewTop < 0 ? -1 : 1;

                contentNewTop =
                    ((contentNewHeight - window.originalHeight) / 2 +
                        content.correctY) *
                    _positive;
            }

            if (contentNewScale === this.content.minScale) {
                contentNewLeft = contentNewTop = 0;
            }

            var response = {
                currentLeft: content.currentLeft,
                newLeft: contentNewLeft,
                currentTop: content.currentTop,
                newTop: contentNewTop,
                currentScale: content.currentScale,
                newScale: contentNewScale,
            };
            content.currentWidth = contentNewWidth;
            content.currentHeight = contentNewHeight;
            content.currentLeft = contentNewLeft;
            content.currentTop = contentNewTop;
            content.currentScale = contentNewScale;
            return response;
        },
        _transform: function _transform(_ref2) {
            _ref2.currentLeft;
            var newLeft = _ref2.newLeft;
            _ref2.currentTop;
            var newTop = _ref2.newTop;
            _ref2.currentScale;
            var newScale = _ref2.newScale;
            this.content.$element.style.transform = 'translate3d('
                .concat(newLeft, 'px, ')
                .concat(newTop, 'px, 0px) scale(')
                .concat(newScale, ')');

            if (typeof this.options.rescale === 'function') {
                this.options.rescale();
            }
        },
        _zoom: function _zoom(direction) {
            var windowPosition = getElementPosition(this.window.$element);
            var window = this.window;
            var _document2 = document,
                body = _document2.body,
                documentElement = _document2.documentElement;
            var scrollLeft =
                window.pageXOffset ||
                documentElement.scrollLeft ||
                body.scrollLeft;
            var scrollTop =
                window.pageYOffset ||
                documentElement.scrollTop ||
                body.scrollTop;

            this._transform(
                this._computeNewPosition(this._computeNewScale(direction), {
                    x:
                        windowPosition.left +
                        this.window.originalWidth / 2 -
                        scrollLeft,
                    y:
                        windowPosition.top +
                        this.window.originalHeight / 2 -
                        scrollTop,
                })
            );
        },
        prepare: function prepare() {
            this._prepare();
        },
        zoomUp: function zoomUp() {
            this._zoom(-1);
        },
        zoomDown: function zoomDown() {
            this._zoom(1);
        },
        destroy: function destroy() {
            this.content.$element.style.transform = '';

            if (this.options.type === 'image') {
                off(this.content.$element, 'load', this._init);
            }

            if (this.isTouch) {
                off(
                    this.content.$element,
                    'touchmove',
                    this._zoomTwoFingers_TouchmoveHandler
                );
                off(
                    this.content.$element,
                    'touchend',
                    this._zoomTwoFingers_TouchendHandler
                );
            }

            off(this.window.$element, 'wheel', this._wheelHandler);

            if (this.options.zoomOnClick) {
                off(
                    this.window.$element,
                    this.events.down,
                    this._downHandler,
                    this.events.options
                );
                off(
                    this.window.$element,
                    this.events.up,
                    this._upHandler,
                    this.events.options
                );
            }

            if (this.dragScrollable) {
                this.dragScrollable.destroy();
            }

            for (var key in this) {
                if (this.hasOwnProperty(key)) {
                    this[key] = null;
                }
            }
        },
    };

    function _wheelHandler(event) {
        event.preventDefault();

        this._transform(
            this._computeNewPosition(this._computeNewScale(event.deltaY), {
                x: eventClientX(event),
                y: eventClientY(event),
            })
        );
    }

    function _downHandler(event) {
        var _this = this;

        if (
            (this.isTouch && event.touches.length === 1) ||
            event.buttons === 1
        ) {
            this.clickExpired = false;
            setTimeout(function () {
                return (_this.clickExpired = true);
            }, 150);
        }
    }

    function _upHandler(event) {
        if (!this.clickExpired) {
            this._transform(
                this._computeNewPosition(
                    this.direction === 1
                        ? this.content.maxScale
                        : this.content.minScale,
                    {
                        x: eventClientX(event),
                        y: eventClientY(event),
                    }
                )
            );

            this.direction *= -1;
        }
    }

    function _zoomTwoFingers_TouchmoveHandler(event) {
        // detect two fingers
        if (event.targetTouches.length === 2) {
            var pageX1 = event.targetTouches[0].clientX;
            var pageY1 = event.targetTouches[0].clientY;
            var pageX2 = event.targetTouches[1].clientX;
            var pageY2 = event.targetTouches[1].clientY; // Math.hypot() analog

            var fingersHypotNew = Math.round(
                Math.sqrt(
                    Math.pow(Math.abs(pageX1 - pageX2), 2) +
                        Math.pow(Math.abs(pageY1 - pageY2), 2)
                )
            );
            var direction = 0;
            if (fingersHypotNew > this.fingersHypot + 5) direction = -1;
            if (fingersHypotNew < this.fingersHypot - 5) direction = 1;

            if (direction !== 0) {
                console.log(
                    'move',
                    direction,
                    this.fingersHypot,
                    fingersHypotNew
                );

                if (this.fingersHypot !== null || direction === 1) {
                    var eventEmulator = new Event('wheel'); // sized direction

                    eventEmulator.deltaY = direction; // middle position between fingers

                    eventEmulator.clientX =
                        Math.min(pageX1, pageX2) +
                        Math.abs(pageX1 - pageX2) / 2;
                    eventEmulator.clientY =
                        Math.min(pageY1, pageY2) +
                        Math.abs(pageY1 - pageY2) / 2;

                    this._wheelHandler(eventEmulator);
                }

                this.fingersHypot = fingersHypotNew;
                this.zoomPinchWasDetected = true;
            }
        }
    }

    function _zoomTwoFingers_TouchendHandler() {
        if (this.zoomPinchWasDetected) {
            this.fingersHypot = null;
            this.zoomPinchWasDetected = false;
            console.log('end', this.fingersHypot);
        }
    }
    /**
     * Create WZoom instance
     * @param {string|HTMLElement} selectorOrHTMLElement
     * @param {Object} [options]
     * @returns {WZoom}
     */

    WZoom.create = function (selectorOrHTMLElement, options) {
        return new WZoom(selectorOrHTMLElement, options);
    };

    return WZoom;
});