UNPKG

@mapaxe/ngx-pinch-zoom

Version:

Pinch zoom component for Angular 16.

1,217 lines (1,213 loc) 56.2 kB
import * as i0 from '@angular/core'; import { EventEmitter, Component, Input, Output, HostBinding, NgModule } from '@angular/core'; import * as i1 from '@angular/common'; import { CommonModule } from '@angular/common'; const defaultProperties = { transitionDuration: 200, doubleTap: true, doubleTapScale: 2, limitZoom: 'original image size', autoZoomOut: false, zoomControlScale: 1, minPanScale: 1.0001, minScale: 0, listeners: 'mouse and touch', wheel: true, wheelZoomFactor: 0.2, draggableImage: false, }; const backwardCompatibilityProperties = { 'transition-duration': 'transitionDuration', transitionDurationBackwardCompatibility: 'transitionDuration', 'double-tap': 'doubleTap', doubleTapBackwardCompatibility: 'doubleTap', 'double-tap-scale': 'doubleTapScale', doubleTapScaleBackwardCompatibility: 'doubleTapScale', 'auto-zoom-out': 'autoZoomOut', autoZoomOutBackwardCompatibility: 'autoZoomOut', 'limit-zoom': 'limitZoom', limitZoomBackwardCompatibility: 'limitZoom', }; class Touches { properties; element; elementPosition; eventType = undefined; handlers = {}; startX = 0; startY = 0; lastTap = 0; doubleTapTimeout; doubleTapMinTimeout = 300; tapMinTimeout = 200; touchstartTime = 0; i = 0; isMousedown = false; _touchListeners = { touchstart: 'handleTouchstart', touchmove: 'handleTouchmove', touchend: 'handleTouchend', }; _mouseListeners = { mousedown: 'handleMousedown', mousemove: 'handleMousemove', mouseup: 'handleMouseup', wheel: 'handleWheel', }; _otherListeners = { resize: 'handleResize', }; get touchListeners() { return this.properties.touchListeners ? this.properties.touchListeners : this._touchListeners; } get mouseListeners() { return this.properties.mouseListeners ? this.properties.mouseListeners : this._mouseListeners; } get otherListeners() { return this.properties.otherListeners ? this.properties.otherListeners : this._otherListeners; } constructor(properties) { this.properties = properties; this.element = this.properties.element; this.elementPosition = this.getElementPosition(); this.toggleEventListeners('addEventListener'); } destroy() { this.toggleEventListeners('removeEventListener'); } toggleEventListeners(action) { let listeners; if (this.properties.listeners === 'mouse and touch') { listeners = Object.assign(this.touchListeners, this.mouseListeners); } else { listeners = this.detectTouchScreen() ? this.touchListeners : this.mouseListeners; } if (this.properties.resize) { listeners = Object.assign(listeners, this.otherListeners); } for (var listener in listeners) { const handler = listeners[listener]; // Window if (listener === 'resize') { if (action === 'addEventListener') { window.addEventListener(listener, this[handler], false); } if (action === 'removeEventListener') { window.removeEventListener(listener, this[handler], false); } // Document } else if (listener === 'mouseup' || listener === 'mousemove') { if (action === 'addEventListener') { document.addEventListener(listener, this[handler], false); } if (action === 'removeEventListener') { document.removeEventListener(listener, this[handler], false); } // Element } else { if (action === 'addEventListener') { this.element.addEventListener(listener, this[handler], false); } if (action === 'removeEventListener') { this.element.removeEventListener(listener, this[handler], false); } } } } addEventListeners(listener) { const handler = this._mouseListeners[listener]; window.addEventListener(listener, this[handler], false); } removeEventListeners(listener) { const handler = this._mouseListeners[listener]; window.removeEventListener(listener, this[handler], false); } /* * Listeners */ /* Touchstart */ handleTouchstart = (event) => { this.elementPosition = this.getElementPosition(); this.touchstartTime = new Date().getTime(); if (this.eventType === undefined) { this.getTouchstartPosition(event); } this.runHandler('touchstart', event); }; /* Touchmove */ handleTouchmove = (event) => { const touches = event.touches; // Pan if (this.detectPan(touches)) { this.runHandler('pan', event); } // Pinch if (this.detectPinch(event)) { this.runHandler('pinch', event); } }; handleLinearSwipe(event) { //event.preventDefault(); this.i++; if (this.i > 3) { this.eventType = this.getLinearSwipeType(event); } if (this.eventType === 'horizontal-swipe') { this.runHandler('horizontal-swipe', event); } if (this.eventType === 'vertical-swipe') { this.runHandler('vertical-swipe', event); } } /* Touchend */ handleTouchend = (event) => { const touches = event.touches; // Double Tap if (this.detectDoubleTap()) { this.runHandler('double-tap', event); } // Tap this.detectTap(); this.runHandler('touchend', event); this.eventType = 'touchend'; if (touches && touches.length === 0) { this.eventType = undefined; this.i = 0; } }; /* Mousedown */ handleMousedown = (event) => { this.isMousedown = true; this.elementPosition = this.getElementPosition(); this.touchstartTime = new Date().getTime(); if (this.eventType === undefined) { this.getMousedownPosition(event); } this.runHandler('mousedown', event); }; /* Mousemove */ handleMousemove = (event) => { //event.preventDefault(); if (!this.isMousedown) { return; } // Pan this.runHandler('pan', event); // Linear swipe switch (this.detectLinearSwipe(event)) { case 'horizontal-swipe': event.swipeType = 'horizontal-swipe'; this.runHandler('horizontal-swipe', event); break; case 'vertical-swipe': event.swipeType = 'vertical-swipe'; this.runHandler('vertical-swipe', event); break; } // Linear swipe if (this.detectLinearSwipe(event) || this.eventType === 'horizontal-swipe' || this.eventType === 'vertical-swipe') { this.handleLinearSwipe(event); } }; /* Mouseup */ handleMouseup = (event) => { // Tap this.detectTap(); this.isMousedown = false; this.runHandler('mouseup', event); this.eventType = undefined; this.i = 0; }; /* Wheel */ handleWheel = (event) => { this.runHandler('wheel', event); }; /* Resize */ handleResize = (event) => { this.runHandler('resize', event); }; runHandler(eventName, response) { if (this.handlers[eventName]) { this.handlers[eventName](response); } } /* * Detection */ detectPan(touches) { return (touches.length === 1 && !this.eventType) || this.eventType === 'pan'; } detectDoubleTap() { if (this.eventType != undefined) { return; } const currentTime = new Date().getTime(); const tapLength = currentTime - this.lastTap; clearTimeout(this.doubleTapTimeout); if (tapLength < this.doubleTapMinTimeout && tapLength > 0) { return true; } else { this.doubleTapTimeout = setTimeout(() => { clearTimeout(this.doubleTapTimeout); }, this.doubleTapMinTimeout); } this.lastTap = currentTime; return undefined; } detectTap() { if (this.eventType != undefined) { return; } const currentTime = new Date().getTime(); const tapLength = currentTime - this.touchstartTime; if (tapLength > 0) { if (tapLength < this.tapMinTimeout) { this.runHandler('tap', {}); } else { this.runHandler('longtap', {}); } } } detectPinch(event) { const touches = event.touches; return (touches.length === 2 && this.eventType === undefined) || this.eventType === 'pinch'; } detectLinearSwipe(event) { const touches = event.touches; if (touches) { if ((touches.length === 1 && !this.eventType) || this.eventType === 'horizontal-swipe' || this.eventType === 'vertical-swipe') { return this.getLinearSwipeType(event); } } else { if (!this.eventType || this.eventType === 'horizontal-swipe' || this.eventType === 'vertical-swipe') { return this.getLinearSwipeType(event); } } return undefined; } getLinearSwipeType(event) { if (this.eventType !== 'horizontal-swipe' && this.eventType !== 'vertical-swipe') { const movementX = Math.abs(this.moveLeft(0, event) - this.startX); const movementY = Math.abs(this.moveTop(0, event) - this.startY); if (movementY * 3 > movementX) { return 'vertical-swipe'; } else { return 'horizontal-swipe'; } } else { return this.eventType; } } getElementPosition() { return this.element.getBoundingClientRect(); } getTouchstartPosition(event) { this.startX = event.touches[0].clientX - this.elementPosition.left; this.startY = event.touches[0].clientY - this.elementPosition.top; } getMousedownPosition(event) { this.startX = event.clientX - this.elementPosition.left; this.startY = event.clientY - this.elementPosition.top; } moveLeft(index, event) { const touches = event.touches; if (touches) { return touches[index].clientX - this.elementPosition.left; } else { return event.clientX - this.elementPosition.left; } } moveTop(index, event) { const touches = event.touches; if (touches) { return touches[index].clientY - this.elementPosition.top; } else { return event.clientY - this.elementPosition.top; } } detectTouchScreen() { var prefixes = ' -webkit- -moz- -o- -ms- '.split(' '); var mq = function (query) { return window.matchMedia(query).matches; }; if ('ontouchstart' in window) { return true; } // include the 'heartz' as a way to have a non matching MQ to help terminate the join // https://git.io/vznFH var query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join(''); return mq(query); } /* Public properties and methods */ on(event, handler) { if (event) { this.handlers[event] = handler; } } } class IvyPinch { properties = defaultProperties; onZoomChange; touches; element; elementTarget; parentElement; i = 0; scale = 1; initialScale = 1; elementPosition; eventType; startX = 0; startY = 0; moveX = 0; moveY = 0; initialMoveX = 0; initialMoveY = 0; moveXC = 0; moveYC = 0; lastTap = 0; draggingMode = false; distance = 0; doubleTapTimeout = 0; initialDistance = 0; events = {}; maxScale; defaultMaxScale = 3; // Minimum scale at which panning works get minPanScale() { return this.getPropertiesValue('minPanScale'); } get fullImage() { return this.properties.fullImage; } constructor(properties, onZoomChange) { this.element = properties.element; this.onZoomChange = onZoomChange; if (!this.element) { return; } this.elementTarget = this.element.querySelector('*').tagName; this.parentElement = this.element.parentElement; this.properties = Object.assign({}, defaultProperties, properties); this.detectLimitZoom(); this.touches = new Touches({ element: properties.element, listeners: properties.listeners, resize: properties.autoHeight, mouseListeners: { mousedown: 'handleMousedown', mouseup: 'handleMouseup', wheel: 'handleWheel', }, }); /* Init */ this.setBasicStyles(); /* * Listeners */ this.touches.on('touchstart', this.handleTouchstart); this.touches.on('touchend', this.handleTouchend); this.touches.on('mousedown', this.handleTouchstart); this.touches.on('mouseup', this.handleTouchend); this.touches.on('pan', this.handlePan); this.touches.on('mousemove', this.handlePan); this.touches.on('pinch', this.handlePinch); if (this.properties.wheel) { this.touches.on('wheel', this.handleWheel); } if (this.properties.doubleTap) { this.touches.on('double-tap', this.handleDoubleTap); } if (this.properties.autoHeight) { this.touches.on('resize', this.handleResize); } } /* Touchstart */ handleTouchstart = (event) => { this.touches.addEventListeners('mousemove', 'handleMousemove'); this.getElementPosition(); if (this.eventType === undefined) { this.getTouchstartPosition(event); } }; /* Touchend */ handleTouchend = (event) => { /* touchend */ if (event.type === 'touchend') { this.i = 0; this.draggingMode = false; const touches = event.touches; // Min scale if (this.scale < 1) { this.scale = 1; } // Auto Zoom Out if (this.properties.autoZoomOut && this.eventType === 'pinch') { this.scale = 1; } // Align image if (this.eventType === 'pinch' || (this.eventType === 'pan' && this.scale > this.minPanScale)) { this.alignImage(); } // Update initial values if (this.eventType === 'pinch' || this.eventType === 'pan' || this.eventType === 'horizontal-swipe' || this.eventType === 'vertical-swipe') { this.updateInitialValues(); } this.eventType = 'touchend'; if (touches && touches.length === 0) { this.eventType = undefined; } } /* mouseup */ if (event.type === 'mouseup') { this.draggingMode = false; this.updateInitialValues(); this.eventType = undefined; } this.touches.removeEventListeners('mousemove', 'handleMousemove'); }; /* * Handlers */ handlePan = (event) => { if (this.scale < this.minPanScale || this.properties.disablePan) { return; } event.preventDefault(); const { clientX, clientY } = this.getClientPosition(event); if (!this.eventType) { this.startX = clientX - this.elementPosition.left; this.startY = clientY - this.elementPosition.top; } this.eventType = 'pan'; this.moveX = this.initialMoveX + (this.moveLeft(event, 0) - this.startX); this.moveY = this.initialMoveY + (this.moveTop(event, 0) - this.startY); if (this.properties.limitPan) { this.limitPanY(); this.limitPanX(); } /* mousemove */ if (event.type === 'mousemove' && this.scale > this.minPanScale) { this.centeringImage(); } this.transformElement(0); }; handleDoubleTap = (event) => { this.toggleZoom(event); return; }; handlePinch = (event) => { event.preventDefault(); if (this.eventType === undefined || this.eventType === 'pinch') { const touches = event.touches; if (!this.eventType) { this.initialDistance = this.getDistance(touches); const moveLeft0 = this.moveLeft(event, 0); const moveLeft1 = this.moveLeft(event, 1); const moveTop0 = this.moveTop(event, 0); const moveTop1 = this.moveTop(event, 1); this.moveXC = (moveLeft0 + moveLeft1) / 2 - this.initialMoveX; this.moveYC = (moveTop0 + moveTop1) / 2 - this.initialMoveY; } this.eventType = 'pinch'; this.distance = this.getDistance(touches); this.scale = this.initialScale * (this.distance / this.initialDistance); this.moveX = this.initialMoveX - ((this.distance / this.initialDistance) * this.moveXC - this.moveXC); this.moveY = this.initialMoveY - ((this.distance / this.initialDistance) * this.moveYC - this.moveYC); this.handleLimitZoom(); if (this.properties.limitPan) { this.limitPanY(); this.limitPanX(); } this.transformElement(0); } }; handleWheel = (event) => { event.preventDefault(); let wheelZoomFactor = this.properties.wheelZoomFactor || 0; let zoomFactor = event.deltaY < 0 ? wheelZoomFactor : -wheelZoomFactor; let newScale = this.initialScale + zoomFactor; /* Round value */ if (newScale < 1 + wheelZoomFactor) { newScale = 1; } else if (newScale < this.maxScale && newScale > this.maxScale - wheelZoomFactor) { newScale = this.maxScale; } if (newScale < 1 || newScale > this.maxScale) { return; } if (newScale === this.scale) { return; } this.getElementPosition(); this.scale = newScale; /* Get cursor position over image */ let xCenter = event.clientX - this.elementPosition.left - this.initialMoveX; let yCenter = event.clientY - this.elementPosition.top - this.initialMoveY; this.setZoom({ scale: newScale, center: [xCenter, yCenter], }); }; handleResize = (_event) => { this.setAutoHeight(); }; handleLimitZoom() { const limitZoom = this.maxScale; const minScale = this.properties.minScale || 0; if (this.scale > limitZoom || this.scale <= minScale) { const imageWidth = this.getImageWidth(); const imageHeight = this.getImageHeight(); const enlargedImageWidth = imageWidth * this.scale; const enlargedImageHeight = imageHeight * this.scale; const moveXRatio = this.moveX / (enlargedImageWidth - imageWidth); const moveYRatio = this.moveY / (enlargedImageHeight - imageHeight); if (this.scale > limitZoom) { this.scale = limitZoom; } if (this.scale <= minScale) { this.scale = minScale; } const newImageWidth = imageWidth * this.scale; const newImageHeight = imageHeight * this.scale; this.moveX = -Math.abs(moveXRatio * (newImageWidth - imageWidth)); this.moveY = -Math.abs(-moveYRatio * (newImageHeight - imageHeight)); } } moveLeft(event, index = 0) { const clientX = this.getClientPosition(event, index).clientX; return clientX - this.elementPosition.left; } moveTop(event, index = 0) { const clientY = this.getClientPosition(event, index).clientY; return clientY - this.elementPosition.top; } /* * Detection */ centeringImage() { const img = this.element.getElementsByTagName(this.elementTarget)[0]; const initialMoveX = this.moveX; const initialMoveY = this.moveY; if (this.moveY > 0) { this.moveY = 0; } if (this.moveX > 0) { this.moveX = 0; } if (img) { this.limitPanY(); this.limitPanX(); } if (img && this.scale < 1) { if (this.moveX < this.element.offsetWidth * (1 - this.scale)) { this.moveX = this.element.offsetWidth * (1 - this.scale); } } return initialMoveX !== this.moveX || initialMoveY !== this.moveY; } limitPanY() { const imgHeight = this.getImageHeight(); const scaledImgHeight = imgHeight * this.scale; const parentHeight = this.parentElement.offsetHeight; const elementHeight = this.element.offsetHeight; if (scaledImgHeight < parentHeight) { this.moveY = (parentHeight - elementHeight * this.scale) / 2; } else { const imgOffsetTop = ((imgHeight - elementHeight) * this.scale) / 2; if (this.moveY > imgOffsetTop) { this.moveY = imgOffsetTop; } else if (scaledImgHeight + Math.abs(imgOffsetTop) - parentHeight + this.moveY < 0) { this.moveY = -(scaledImgHeight + Math.abs(imgOffsetTop) - parentHeight); } } } limitPanX() { const imgWidth = this.getImageWidth(); const scaledImgWidth = imgWidth * this.scale; const parentWidth = this.parentElement.offsetWidth; const elementWidth = this.element.offsetWidth; if (scaledImgWidth < parentWidth) { this.moveX = (parentWidth - elementWidth * this.scale) / 2; } else { const imgOffsetLeft = ((imgWidth - elementWidth) * this.scale) / 2; if (this.moveX > imgOffsetLeft) { this.moveX = imgOffsetLeft; } else if (scaledImgWidth + Math.abs(imgOffsetLeft) - parentWidth + this.moveX < 0) { this.moveX = -(imgWidth * this.scale + Math.abs(imgOffsetLeft) - parentWidth); } } } setBasicStyles() { this.element.style.display = 'flex'; this.element.style.alignItems = 'center'; this.element.style.justifyContent = 'center'; this.element.style.transformOrigin = '0 0'; this.setImageSize(); this.setDraggableImage(); } removeBasicStyles() { this.element.style.display = ''; this.element.style.alignItems = ''; this.element.style.justifyContent = ''; this.element.style.transformOrigin = ''; this.removeImageSize(); this.removeDraggableImage(); } setDraggableImage() { const imgElement = this.getImageElement(); if (imgElement) { imgElement.draggable = this.properties.draggableImage; } } removeDraggableImage() { const imgElement = this.getImageElement(); if (imgElement) { imgElement.draggable = true; } } setImageSize() { const imgElement = this.element.getElementsByTagName(this.elementTarget); if (imgElement.length) { imgElement[0].style.maxWidth = '100%'; imgElement[0].style.maxHeight = '100%'; this.setAutoHeight(); } } setAutoHeight() { const imgElement = this.element.getElementsByTagName(this.elementTarget); if (!this.properties.autoHeight || !imgElement.length) { return; } const imgNaturalWidth = imgElement[0].getAttribute('width'); const imgNaturalHeight = imgElement[0].getAttribute('height'); const sizeRatio = imgNaturalWidth / imgNaturalHeight; const parentWidth = this.parentElement.offsetWidth; imgElement[0].style.maxHeight = parentWidth / sizeRatio + 'px'; } removeImageSize() { const imgElement = this.element.getElementsByTagName(this.elementTarget); if (imgElement.length) { imgElement[0].style.maxWidth = ''; imgElement[0].style.maxHeight = ''; } } getElementPosition() { this.elementPosition = this.element.parentElement.getBoundingClientRect(); } getTouchstartPosition(event) { const { clientX, clientY } = this.getClientPosition(event); this.startX = clientX - this.elementPosition.left; this.startY = clientY - this.elementPosition.top; } getClientPosition(event, index = 0) { let clientX; let clientY; if (event.type === 'touchstart' || event.type === 'touchmove') { clientX = event.touches[index].clientX; clientY = event.touches[index].clientY; } if (event.type === 'mousedown' || event.type === 'mousemove') { clientX = event.clientX; clientY = event.clientY; } return { clientX, clientY, }; } resetScale() { this.scale = 1; this.moveX = 0; this.moveY = 0; this.updateInitialValues(); this.transformElement(this.properties.transitionDuration); } updateInitialValues() { this.initialScale = this.scale; this.initialMoveX = this.moveX; this.initialMoveY = this.moveY; let visibleAreaWidth = this.element.offsetWidth; let visibleAreaHeight = this.element.offsetHeight; const event = { scale: this.initialScale, moveX: this.initialMoveX, moveY: this.initialMoveY, visibleAreaWidth: visibleAreaWidth, visibleAreaHeight: visibleAreaHeight, }; this.onZoomChange.emit(event); } getDistance(touches) { return Math.sqrt(Math.pow(touches[0].pageX - touches[1].pageX, 2) + Math.pow(touches[0].pageY - touches[1].pageY, 2)); } getImageHeight() { const img = this.element.getElementsByTagName(this.elementTarget)[0]; return img.offsetHeight; } getImageWidth() { const img = this.element.getElementsByTagName(this.elementTarget)[0]; return img.offsetWidth; } transformElement(duration) { this.element.style.transition = 'all ' + duration + 'ms'; this.element.style.transform = 'matrix(' + Number(this.scale) + ', 0, 0, ' + Number(this.scale) + ', ' + Number(this.moveX) + ', ' + Number(this.moveY) + ')'; } isTouchScreen() { const prefixes = ' -webkit- -moz- -o- -ms- '.split(' '); if ('ontouchstart' in window) { return true; } // include the 'heartz' as a way to have a non matching MQ to help terminate the join // https://git.io/vznFH const query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join(''); return this.getMatchMedia(query); } getMatchMedia(query) { return window.matchMedia(query).matches; } isDragging() { if (this.properties.disablePan) { return false; } const imgHeight = this.getImageHeight(); const imgWidth = this.getImageWidth(); if (this.scale > 1) { return imgHeight * this.scale > this.parentElement.offsetHeight || imgWidth * this.scale > this.parentElement.offsetWidth; } if (this.scale === 1) { return imgHeight > this.parentElement.offsetHeight || imgWidth > this.parentElement.offsetWidth; } return undefined; } detectLimitZoom() { this.maxScale = this.defaultMaxScale; if (this.properties.limitZoom !== 'original image size' && +this.properties.limitZoom > 0) { this.maxScale = +this.properties.limitZoom; } if (this.properties.limitZoom === 'original image size' && this.elementTarget === 'IMG') { // We are waiting for the element with the image to be available this.pollLimitZoomForOriginalImage(); } } pollLimitZoomForOriginalImage() { let poll = setInterval(() => { let maxScaleForOriginalImage = this.getMaxScaleForOriginalImage(); if (typeof maxScaleForOriginalImage === 'number') { this.maxScale = maxScaleForOriginalImage; clearInterval(poll); } }, 10); } getMaxScaleForOriginalImage() { let maxScale; let img = this.element.getElementsByTagName('img')[0]; if (img.naturalWidth && img.offsetWidth) { maxScale = img.naturalWidth / img.offsetWidth; } return maxScale; } getImageElement() { const imgElement = this.element.getElementsByTagName(this.elementTarget); if (imgElement.length) { return imgElement[0]; } } toggleZoom(event = false) { if (this.initialScale === 1) { if (event && event.changedTouches) { if (this.properties.doubleTapScale === undefined) { return; } const changedTouches = event.changedTouches; this.scale = this.initialScale * this.properties.doubleTapScale; this.moveX = this.initialMoveX - (changedTouches[0].clientX - this.elementPosition.left) * (this.properties.doubleTapScale - 1); this.moveY = this.initialMoveY - (changedTouches[0].clientY - this.elementPosition.top) * (this.properties.doubleTapScale - 1); } else { let zoomControlScale = this.properties.zoomControlScale || 0; this.scale = this.initialScale * (zoomControlScale + 1); this.moveX = this.initialMoveX - (this.element.offsetWidth * (this.scale - 1)) / 2; this.moveY = this.initialMoveY - (this.element.offsetHeight * (this.scale - 1)) / 2; } this.centeringImage(); this.updateInitialValues(); this.transformElement(this.properties.transitionDuration); } else { this.resetScale(); } } zoomPoint(point, scale) { console.warn('zoomPoint', point, scale); let newScale = scale ?? (this.initialScale + this.properties.stepZoomFactor); if (newScale < 1 || newScale > this.maxScale) { return; } if (newScale === this.scale) { return; } /* Get cursor position over image */ let newCenter = undefined; if (point) { let xCenter = point.clientX - this.elementPosition.left - this.initialMoveX; let yCenter = point.clientY - this.elementPosition.top - this.initialMoveY; newCenter = [xCenter, yCenter]; } console.warn('newScale2', newScale); this.getElementPosition(); this.scale = newScale; this.setZoom({ scale: newScale, center: newCenter }); } stepZoom(mode) { let stepZoomFactor = this.properties.stepZoomFactor ?? 0; let zoomFactor = 0; if (mode === 'OUT') { zoomFactor = -stepZoomFactor; } if (mode === 'IN') { zoomFactor = stepZoomFactor; } let newScale = this.initialScale + zoomFactor; console.warn('newScale', newScale); /* Round value */ if (newScale < 1 + stepZoomFactor) { newScale = 1; } else if (newScale < this.maxScale && newScale > this.maxScale - stepZoomFactor) { newScale = this.maxScale; } if (newScale < 1 || newScale > this.maxScale) { return; } if (newScale === this.scale) { return; } console.warn('newScale2', newScale); this.getElementPosition(); this.scale = newScale; this.setZoom({ scale: newScale, center: undefined }); } setZoom(properties) { this.scale = properties.scale; let xCenter; let yCenter; let visibleAreaWidth = this.element.offsetWidth; let visibleAreaHeight = this.element.offsetHeight; let scalingPercent = (visibleAreaWidth * this.scale) / (visibleAreaWidth * this.initialScale); if (properties.center) { xCenter = properties.center[0]; yCenter = properties.center[1]; } else { xCenter = visibleAreaWidth / 2 - this.initialMoveX; yCenter = visibleAreaHeight / 2 - this.initialMoveY; } this.moveX = this.initialMoveX - (scalingPercent * xCenter - xCenter); this.moveY = this.initialMoveY - (scalingPercent * yCenter - yCenter); this.centeringImage(); this.updateInitialValues(); this.transformElement(this.properties.transitionDuration); } alignImage() { const isMoveChanged = this.centeringImage(); if (isMoveChanged) { this.updateInitialValues(); this.transformElement(this.properties.transitionDuration); } } destroy() { this.removeBasicStyles(); this.touches.destroy(); } getPropertiesValue(propertyName) { if (this.properties && this.properties[propertyName]) { return this.properties[propertyName]; } else { return defaultProperties[propertyName]; } } } const _defaultComponentProperties = { overflow: 'hidden', disableZoomControl: 'auto', backgroundColor: 'rgba(0,0,0,0.85)', }; class PinchZoomComponent { elementRef; defaultComponentProperties; pinchZoom; _properties; zoomControlPositionClass; _transitionDuration; _doubleTap; _doubleTapScale; _autoZoomOut; _limitZoom; set properties(value) { if (value) { this._properties = value; } } get properties() { return this._properties; } // transitionDuration set transitionDurationBackwardCompatibility(value) { if (value) { this._transitionDuration = value; } } set transitionDuration(value) { if (value) { this._transitionDuration = value; } } get transitionDuration() { return this._transitionDuration; } // doubleTap set doubleTapBackwardCompatibility(value) { if (value) { this._doubleTap = value; } } set doubleTap(value) { if (value) { this._doubleTap = value; } } get doubleTap() { return this._doubleTap; } // doubleTapScale set doubleTapScaleBackwardCompatibility(value) { if (value) { this._doubleTapScale = value; } } set doubleTapScale(value) { if (value) { this._doubleTapScale = value; } } get doubleTapScale() { return this._doubleTapScale; } // autoZoomOut set autoZoomOutBackwardCompatibility(value) { if (value) { this._autoZoomOut = value; } } set autoZoomOut(value) { if (value) { this._autoZoomOut = value; } } get autoZoomOut() { return this._autoZoomOut; } // limitZoom set limitZoomBackwardCompatibility(value) { if (value) { this._limitZoom = value; } } set limitZoom(value) { if (value) { this._limitZoom = value; } } get limitZoom() { return this._limitZoom; } disabled; disablePan; overflow; zoomControlScale; disableZoomControl; backgroundColor; limitPan; minPanScale; minScale; listeners; wheel; autoHeight; stepZoomFactor; wheelZoomFactor; draggableImage; onZoomChange = new EventEmitter(); get hostOverflow() { return this.properties['overflow']; } get hostBackgroundColor() { return this.properties['backgroundColor']; } get isTouchScreen() { var prefixes = ' -webkit- -moz- -o- -ms- '.split(' '); var mq = function (query) { return window.matchMedia(query).matches; }; if ('ontouchstart' in window) { return true; } // include the 'heartz' as a way to have a non matching MQ to help terminate the join // https://git.io/vznFH var query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join(''); return mq(query); } get isDragging() { return this.pinchZoom ? this.pinchZoom.isDragging() : undefined; } get isDisabled() { return this.properties['disabled']; } get scale() { return this.pinchZoom.scale; } get isZoomedIn() { return this.scale > 1; } get scaleLevel() { return Math.round(this.scale / this._zoomControlScale); } get maxScale() { return this.pinchZoom.maxScale; } get isZoomLimitReached() { return this.scale >= this.maxScale; } get _zoomControlScale() { return this.getPropertiesValue('zoomControlScale'); } constructor(elementRef) { this.elementRef = elementRef; this.defaultComponentProperties = this.getDefaultComponentProperties(); this.applyPropertiesDefault(this.defaultComponentProperties, {}); } ngOnInit() { this.initPinchZoom(); /* Calls the method until the image size is available */ this.detectLimitZoom(); } isControl() { if (this.isDisabled) { return false; } if (this.properties['disableZoomControl'] === 'disable') { return false; } if (this.isTouchScreen && this.properties['disableZoomControl'] === 'auto') { return false; } return true; } ngOnChanges(changes) { let changedProperties = this.getProperties(changes); changedProperties = this.renameProperties(changedProperties); this.applyPropertiesDefault(this.defaultComponentProperties, changedProperties); } ngOnDestroy() { this.destroy(); } toggleZoom() { this.pinchZoom.toggleZoom(); } zoomPoint(point, scale) { this.pinchZoom.zoomPoint(point, scale); } zoomIn() { this.pinchZoom.stepZoom('IN'); } zoomOut() { this.pinchZoom.stepZoom('OUT'); } resetZoom() { this.pinchZoom.resetScale(); } destroy() { if (this.pinchZoom) this.pinchZoom.destroy(); } initPinchZoom() { if (this.properties['disabled']) { return; } this.properties['element'] = this.elementRef.nativeElement.querySelector('.pinch-zoom-content'); this.pinchZoom = new IvyPinch(this.properties, this.onZoomChange); } getProperties(changes) { let properties = {}; for (var prop in changes) { if (prop !== 'properties') { properties[prop] = changes[prop].currentValue; } if (prop === 'properties') { properties = changes[prop].currentValue; } } return properties; } renameProperties(properties) { for (var prop in properties) { if (backwardCompatibilityProperties[prop]) { properties[backwardCompatibilityProperties[prop]] = properties[prop]; delete properties[prop]; } } return properties; } applyPropertiesDefault(defaultProperties, properties) { this.properties = Object.assign({}, defaultProperties, properties); } detectLimitZoom() { if (this.pinchZoom) { this.pinchZoom.detectLimitZoom(); } } getPropertiesValue(propertyName) { if (this.properties && this.properties[propertyName]) { return this.properties[propertyName]; } else { return this.defaultComponentProperties[propertyName]; } } getDefaultComponentProperties() { return { ...defaultProperties, ..._defaultComponentProperties }; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: PinchZoomComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.0.0", type: PinchZoomComponent, selector: "pinch-zoom, [pinch-zoom]", inputs: { properties: "properties", transitionDurationBackwardCompatibility: ["transition-duration", "transitionDurationBackwardCompatibility"], transitionDuration: "transitionDuration", doubleTapBackwardCompatibility: ["double-tap", "doubleTapBackwardCompatibility"], doubleTap: "doubleTap", doubleTapScaleBackwardCompatibility: ["double-tap-scale", "doubleTapScaleBackwardCompatibility"], doubleTapScale: "doubleTapScale", autoZoomOutBackwardCompatibility: ["auto-zoom-out", "autoZoomOutBackwardCompatibility"], autoZoomOut: "autoZoomOut", limitZoomBackwardCompatibility: ["limit-zoom", "limitZoomBackwardCompatibility"], limitZoom: "limitZoom", disabled: "disabled", disablePan: "disablePan", overflow: "overflow", zoomControlScale: "zoomControlScale", disableZoomControl: "disableZoomControl", backgroundColor: "backgroundColor", limitPan: "limitPan", minPanScale: "minPanScale", minScale: "minScale", listeners: "listeners", wheel: "wheel", autoHeight: "autoHeight", stepZoomFactor: "stepZoomFactor", wheelZoomFactor: "wheelZoomFactor", draggableImage: "draggableImage" }, outputs: { onZoomChange: "onZoomChange" }, host: { properties: { "style.overflow": "this.hostOverflow", "style.background-color": "this.hostBackgroundColor" } }, exportAs: ["pinchZoom"], usesOnChanges: true, ngImport: i0, template: "<div class=\"pinch-zoom-content\" [class.pz-dragging]=\"isDragging\">\r\n <ng-content></ng-content>\r\n</div>\r\n\r\n<!-- Control: one button -->\r\n<div\r\n class=\"pz-zoom-button pz-zoom-control-position-bottom\"\r\n [class.pz-zoom-button-out]=\"isZoomedIn\"\r\n *ngIf=\"isControl()\"\r\n (click)=\"toggleZoom()\"\r\n></div>\r\n", styles: [":host{position:relative;overflow:hidden;display:block}.pinch-zoom-content{height:inherit}.pz-dragging{cursor:all-scroll}.pz-zoom-button{position:absolute;z-index:1000;color:#fff;background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgc3R5bGU9IiI+PHJlY3QgaWQ9ImJhY2tncm91bmRyZWN0IiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4PSIwIiB5PSIwIiBmaWxsPSJub25lIiBzdHJva2U9Im5vbmUiLz48ZyBjbGFzcz0iY3VycmVudExheWVyIiBzdHlsZT0iIj48dGl0bGU+TGF5ZXIgMTwvdGl0bGU+PHBhdGggZD0iTTE1LjUgMTRoLS43OWwtLjI4LS4yN0MxNS40MSAxMi41OSAxNiAxMS4xMSAxNiA5LjUgMTYgNS45MSAxMy4wOSAzIDkuNSAzUzMgNS45MSAzIDkuNSA1LjkxIDE2IDkuNSAxNmMxLjYxIDAgMy4wOS0uNTkgNC4yMy0xLjU3bC4yNy4yOHYuNzlsNSA0Ljk5TDIwLjQ5IDE5bC00Ljk5LTV6bS02IDBDNy4wMSAxNCA1IDExLjk5IDUgOS41UzcuMDEgNSA5LjUgNSAxNCA3LjAxIDE0IDkuNSAxMS45OSAxNCA5LjUgMTR6IiBpZD0ic3ZnXzEiIGNsYXNzPSIiIGZpbGw9IiNmZmZmZmYiIGZpbGwtb3BhY2l0eT0iMSIvPjxwYXRoIGQ9Ik0xMiAxMGgtMnYySDl2LTJIN1Y5aDJWN2gxdjJoMnYxeiIgaWQ9InN2Z18zIiBjbGFzcz0iIiBmaWxsPSIjZmZmZmZmIiBmaWxsLW9wYWNpdHk9IjEiLz48L2c+PC9zdmc+),url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHJlY3QgaWQ9ImJhY2tncm91bmRyZWN0IiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4PSIwIiB5PSIwIiBmaWxsPSJub25lIiBzdHJva2U9Im5vbmUiLz48ZyBjbGFzcz0iY3VycmVudExheWVyIiBzdHlsZT0iIj48dGl0bGU+TGF5ZXIgMTwvdGl0bGU+PHBhdGggZD0iTTE1LjUgMTRoLS43OWwtLjI4LS4yN0MxNS40MSAxMi41OSAxNiAxMS4xMSAxNiA5LjUgMTYgNS45MSAxMy4wOSAzIDkuNSAzUzMgNS45MSAzIDkuNSA1LjkxIDE2IDkuNSAxNmMxLjYxIDAgMy4wOS0uNTkgNC4yMy0xLjU3bC4yNy4yOHYuNzlsNSA0Ljk5TDIwLjQ5IDE5bC00Ljk5LTV6bS02IDBDNy4wMSAxNCA1IDExLjk5IDUgOS41UzcuMDEgNSA5LjUgNSAxNCA3LjAxIDE0IDkuNSAxMS45OSAxNCA5LjUgMTR6TTcgOWg1djFIN3oiIGlkPSJzdmdfMiIgY2xhc3M9IiIgZmlsbD0iI2ZmZmZmZiIgZmlsbC1vcGFjaXR5PSIxIi8+PC9nPjwvc3ZnPg==);background-color:#000c;background-position:center,-1000px;background-repeat:no-repeat,no-repeat;background-size:40px;width:56px;height:56px;border-radius:4px;opacity:.5;cursor:pointer;transition:opacity .1s;-webkit-user-select:none;user-select:none}.pz-zoom-button-out{background-position:-1000px,center}.pz-zoom-button:hover{opacity:.7}.pz-zoom-button.pz-zoom-control-position-right{right:16px;top:50%;margin-top:-28px}.pz-zoom-button.pz-zoom-control-position-right-bottom{right:16px;bottom:32px}.pz-zoom-button.pz-zoom-control-position-bottom{bottom:16px;left:50%;margin-left:-28px}.pz-zoom-control{position:absolute;background-color:#000c;border-radius:4px;overflow:hidden}.pz-zoom-control.pz-zoom-control-position-right{right:16px;top:50%;margin-top:-48px}.pz-zoom-control.pz-zoom-control-position-right-bottom{right:16px;bottom:32px}.pz-zoom-control.pz-zoom-control-position-bottom{bottom:16px;left:50%;margin-left:-48px}.pz-zoom-in,.pz-zoom-out{width:48px;height:48px;background-position:center;background-repeat:no-repeat;opacity:1;cursor:pointer}.pz-zoom-in:hover,.pz-zoom-out:hover{background-color:#fff3}.pz-zoom-control-position-bottom .pz-zoom-in,.pz-zoom-control-position-bottom .pz-zoom-out{float:right}.pz-disabled{opacity:.5;cursor:default}.pz-disabled:hover{background-color:#fff0}.pz-zoom-in{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgc3R5bGU9IiI+PHJlY3QgaWQ9ImJhY2tncm91bmRyZWN0IiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4PSIwIiB5PSIwIiBmaWxsPSJub25lIiBzdHJva2U9Im5vbmUiLz48ZyBjbGFzcz0iY3VycmVudExheWVyIiBzdHlsZT0iIj48dGl0bGU+TGF5ZXIgMTwvdGl0bGU+PHBhdGggZD0iTTE5IDEzaC02djZoLTJ2LTZINXYtMmg2VjVoMnY2aDZ2MnoiIGlkPSJzdmdfMSIgY2xhc3M9IiIgc3Ryb2tlPSJub25lIiBmaWxsPSIjZmZmZmZmIiBmaWxsLW9wYWNpdHk9IjEiLz48cGF0aCBkPSJNLTE1LjgzNjczNDQyMDQ2MTY1Myw0NC41MzU0MDkzMDY3MTAxOCBoNTguMjA0MDgwODI3NTkzMDkgdi02LjU3NjIyNjcyMzM2OTIyMTUgSC0xNS44MzY3MzQ0MjA0NjE2NTMgeiIgZmlsbD0ibm9uZSIgaWQ9InN2Z18yIiBjbGFzcz0iIiBzdHJva2U9Im5vbmUiLz48L2c+PC9zdmc+)}.pz-zoom-out{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHJlY3QgaWQ9ImJhY2tncm91bmRyZWN0IiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4PSIwIiB5PSIwIiBmaWxsPSJub25lIiBzdHJva2U9Im5vbmUiLz48ZyBjbGFzcz0iY3VycmVudExheWVyIiBzdHlsZT0iIj48dGl0bGU+TGF5ZXIgMTwvdGl0bGU+PHBhdGggZD0iTTE5IDEzSDV2LTJoMTR2MnoiIGlkPSJzdmdfMSIgY2xhc3M9IiIgZmlsbD0iI2ZmZmZmZiIgZmlsbC1vcGFjaXR5PSIxIi8+PC9nPjwvc3ZnPg==)}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: PinchZoomComponent, decorators: [{ type: Component, args: [{ selector: 'pinch-zoom, [pinch-zoom]', exportAs: 'pinchZoom', template: "<div class=\"pinch-zoom-content\" [class.pz-dragging]=\"isDragging\">\r\n <ng-content></ng-content>\r\n</div>\r\n\r\n<!-- Control: one button -->\r\n<div\r\n class=\"pz-zoom-button pz-zoom-control-position-bottom\"\r\n [class.pz-zoom-button-out]=\"isZoomedIn\"\r\n *ngIf=\"isControl()\"\r\n (click)=\"toggleZoom()\"\r\n></div>\r\n", styles: [":host{position:relative;overflow:hidden;display:block}.pinch-zoom-content{height:inherit}.pz-dragging{cursor:all-scroll}.pz-zoom-button{position:absolute;z-index:1000;color:#fff;background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgc3R5bGU9IiI+PHJlY3QgaWQ9ImJhY2tncm91bmRyZWN0IiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4PSIwIiB5PSIwIiBmaWxsPSJub25lIiBzdHJva2U9Im5vbmUiLz48ZyBjbGFzcz0iY3VycmVudExheWVyIiBzdHlsZT0iIj48dGl0bGU+TGF5ZXIgMTwvdGl0bGU+PHBhdGggZD0iTTE1LjUgMTRoLS43OWwtLjI4LS4yN0MxNS40MSAxMi41OSAxNiAxMS4xMSAxNiA5LjUgMTYgNS45MSAxMy4wOSAzIDkuNSAzUzMgNS45MSAzIDkuNSA1LjkxIDE2IDkuNSAxNmMxLjYxIDAgMy4wOS0uNTkgNC4yMy0xLjU3bC4yNy4yOHYuNzlsNSA0Ljk5TDIwLjQ5IDE5bC00Ljk5LTV6bS02IDBDNy4wMSAxNCA1IDExLjk5IDUgOS41UzcuMDEgNSA5LjUgNSAxNCA3LjAxIDE0IDkuNSAxMS45OSAxNCA5LjUgMTR6IiBpZD0ic3ZnXzEiIGNsYXNzPSIiIGZpbGw9IiNmZmZmZmYiIGZpbGwtb3BhY2l0eT0iMSIvPjxwYXRoIGQ9Ik0xMiAxMGgtMnYySDl2LTJIN1Y5aDJWN2gxdjJoMnYxeiIgaWQ9InN2Z18zIiBjbGFzcz0iIiBmaWxsPSIjZmZmZmZmIiBmaWxsLW9wYWNpdHk9IjEiLz48L2c+PC9zdmc+),url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHJlY3QgaWQ9ImJhY2tncm91bmRyZWN0IiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4PSIwIiB5PSIwIiBmaWxsPSJub25lIiBzdHJva2U9Im5vbmUiLz48ZyBjbGFzcz0iY3VycmVudExheWVyIiBzdHlsZT0iIj48dGl0bGU+TGF5ZXIgMTwvdGl0bGU+PHBhdGggZD0iTTE1LjUgMTRoLS43OWwtLjI4LS4yN0MxNS40MSAxMi41OSAxNiAxMS4xMSAxNiA5LjUgMTYgNS45MSAxMy4wOSAzIDkuNSAzUzMgNS45MSAzIDkuNSA1LjkxIDE2IDkuNSAxNmMxLjYxIDAgMy4wOS0uNTkgNC4yMy0xLjU3bC4yNy4yOHYuNzlsNSA0Ljk5TDIwLjQ5IDE5bC00Ljk5LTV6bS02IDBDNy4wMSAxNCA1IDExLjk5IDUgOS41UzcuMDEgNSA5LjUgNSAxNCA3LjAxIDE0IDkuNSAxMS45OSAxNCA5LjUgMTR6TTcgOWg1djFIN3oiIGlkPSJzdmdfMiIgY2xhc3M9IiIgZmlsbD0iI2ZmZmZmZiIgZmlsbC1vcGFjaXR5PSIxIi8+PC9nPjwvc3ZnPg==);background-color:#000c;bac