Image resizing using mouse wheel + drag scrollable image (as well as any HTML content)
สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @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; });