├── src ├── assets │ └── .gitkeep ├── favicon.ico ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── styles.sass ├── app │ ├── app-routing.module.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── app.component.spec.ts │ ├── app.component.sass │ └── app.component.html ├── index.html ├── main.ts ├── test.ts └── polyfills.ts ├── projects └── ngx-pinch-zoom │ ├── src │ ├── public-api.ts │ ├── lib │ │ ├── pinch-zoom.component.html │ │ ├── interfaces.ts │ │ ├── properties.ts │ │ ├── pinch-zoom.component.sass │ │ ├── pinch-zoom.component.ts │ │ ├── touches.ts │ │ └── ivypinch.ts │ └── test.ts │ ├── cypress │ ├── plugins │ │ └── index.ts │ ├── tsconfig.json │ ├── integration │ │ └── spec.ts │ └── support │ │ ├── index.ts │ │ └── commands.ts │ ├── ng-package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── cypress.json │ ├── tsconfig.lib.json │ ├── package.json │ ├── karma.conf.js │ ├── .eslintrc.js │ └── README.md ├── cypress ├── fixtures │ └── example.json ├── plugins │ └── index.ts ├── tsconfig.json ├── integration │ └── spec.ts └── support │ ├── index.ts │ └── commands.ts ├── .prettierrc ├── .prettierignore ├── tsconfig.app.json ├── .editorconfig ├── cypress.json ├── tsconfig.spec.json ├── .gitignore ├── tsconfig.json ├── LICENSE ├── karma.conf.js ├── package.json ├── angular.json ├── .eslintrc.js └── README.md /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medDV-GmbH/ngx-pinch-zoom/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /src/styles.sass: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-pinch-zoom 3 | */ 4 | 5 | export * from './lib/interfaces'; 6 | export * from './lib/pinch-zoom.component'; 7 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/plugins/index.ts: -------------------------------------------------------------------------------- 1 | // Plugins enable you to tap into, modify, or extend the internal behavior of Cypress 2 | // For more info, visit https://on.cypress.io/plugins-api 3 | module.exports = (on, config) => {}; 4 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "sourceMap": false, 6 | "types": ["cypress", "node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "tabWidth": 4, 4 | "singleQuote": true, 5 | "semi": true, 6 | "useTabs": false, 7 | "trailingComma": "es5", 8 | "printWidth": 140 9 | } 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | yarn.lock 4 | dist 5 | node_modules 6 | build 7 | res 8 | coverage 9 | libs/svg-management/src/lib/svg-canvas-editor/svg-canvas-editor.component.stories.ts 10 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/cypress/plugins/index.ts: -------------------------------------------------------------------------------- 1 | // Plugins enable you to tap into, modify, or extend the internal behavior of Cypress 2 | // For more info, visit https://on.cypress.io/plugins-api 3 | module.exports = (on, config) => {}; 4 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-pinch-zoom", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "sourceMap": false, 6 | "types": ["cypress", "node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/cypress/integration/spec.ts: -------------------------------------------------------------------------------- 1 | describe('My First Test', () => { 2 | it('Visits the initial project page', () => { 3 | cy.visit('/'); 4 | cy.contains('Welcome'); 5 | cy.contains('sandbox app is running!'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = []; 5 | 6 | @NgModule({ 7 | imports: [RouterModule.forRoot(routes)], 8 | exports: [RouterModule], 9 | }) 10 | export class AppRoutingModule {} 11 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "integrationFolder": "cypress/integration", 3 | "supportFile": "cypress/support/index.ts", 4 | "videosFolder": "cypress/videos", 5 | "screenshotsFolder": "cypress/screenshots", 6 | "pluginsFile": "cypress/plugins/index.ts", 7 | "fixturesFolder": "cypress/fixtures", 8 | "baseUrl": "http://localhost:4200" 9 | } 10 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": ["jasmine"] 7 | }, 8 | "files": ["src/test.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": ["jasmine"] 7 | }, 8 | "files": ["src/test.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/src/lib/pinch-zoom.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | @if (isControl()) { 7 |
12 | } 13 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IvypinchApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.sass'], 7 | standalone: false 8 | }) 9 | export class AppComponent { 10 | title = 'ivypinchApp'; 11 | public zoomstate = 1; 12 | 13 | onZoomChanged(zoom: number) { 14 | this.zoomstate = zoom; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch((err) => console.error(err)); 14 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 6 | 7 | // First, initialize the Angular testing environment. 8 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 9 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "integrationFolder": "projects/ngx-pinch-zoom/cypress/integration", 3 | "supportFile": "projects/ngx-pinch-zoom/cypress/support/index.ts", 4 | "videosFolder": "projects/ngx-pinch-zoom/cypress/videos", 5 | "screenshotsFolder": "projects/ngx-pinch-zoom/cypress/screenshots", 6 | "pluginsFile": "projects/ngx-pinch-zoom/cypress/plugins/index.ts", 7 | "fixturesFolder": "projects/ngx-pinch-zoom/cypress/fixtures", 8 | "baseUrl": "http://localhost:4200" 9 | } 10 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | import {PinchZoomComponent} from "ngx-pinch-zoom"; 7 | 8 | @NgModule({ 9 | declarations: [AppComponent], 10 | imports: [BrowserModule, AppRoutingModule, PinchZoomComponent], 11 | providers: [], 12 | bootstrap: [AppComponent], 13 | }) 14 | export class AppModule {} 15 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone'; 4 | import 'zone.js/dist/zone-testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 7 | 8 | // First, initialize the Angular testing environment. 9 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 10 | -------------------------------------------------------------------------------- /cypress/integration/spec.ts: -------------------------------------------------------------------------------- 1 | Cypress.on('window:before:load', (win) => { 2 | cy.spy(win.console, 'error'); 3 | cy.spy(win.console, 'warn'); 4 | }); 5 | 6 | describe('workspace-project App', () => { 7 | afterEach(() => { 8 | cy.window().then((win) => { 9 | expect(win.console.error).to.have.callCount(0); 10 | expect(win.console.warn).to.have.callCount(0); 11 | }); 12 | }); 13 | 14 | it('should display welcome message', () => { 15 | cy.visit('/'); 16 | cy.get('title').should('contain.text', 'IvypinchApp'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "angularCompilerOptions": { 13 | "skipTemplateCodegen": true, 14 | "strictMetadataEmit": true, 15 | "enableResourceInlining": true 16 | }, 17 | "exclude": ["src/test.ts", "**/*.spec.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /cypress/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // When a command from ./commands is ready to use, import with `import './commands'` syntax 17 | // import './commands'; 18 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/src/lib/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface Properties { 2 | element?: HTMLElement; 3 | doubleTap?: boolean; 4 | doubleTapScale?: number; 5 | zoomControlScale?: number; 6 | transitionDuration?: number; 7 | autoZoomOut?: boolean; 8 | limitZoom?: number | string | 'original image size'; 9 | disablePan?: boolean; 10 | limitPan?: boolean; 11 | minPanScale?: number; 12 | minScale?: number; 13 | listeners?: 'auto' | 'mouse and touch'; 14 | wheel?: boolean; 15 | fullImage?: { 16 | path: string; 17 | minScale?: number; 18 | }; 19 | autoHeight?: boolean; 20 | wheelZoomFactor?: number; 21 | draggableImage?: boolean; 22 | draggableOnPinch?: boolean; 23 | } 24 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/cypress/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // When a command from ./commands is ready to use, import with `import './commands'` syntax 17 | // import './commands'; 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.angular/cache 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "esModuleInterop": true, 9 | "declaration": false, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "bundler", 12 | "importHelpers": true, 13 | "paths": { 14 | "ngx-pinch-zoom": [ 15 | "dist/ngx-pinch-zoom/ngx-pinch-zoom", 16 | "dist/ngx-pinch-zoom" 17 | ] 18 | }, 19 | "target": "ES2022", 20 | "module": "es2020", 21 | "lib": [ 22 | "es2018", 23 | "dom" 24 | ], 25 | "types": [ 26 | "node" 27 | ], 28 | "useDefineForClassFields": false 29 | }, 30 | "angularCompilerOptions": { 31 | "enableI18nLegacyMessageIdFormat": false 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | import { PinchZoomModule } from '../../projects/ngx-pinch-zoom/src/lib/pinch-zoom.module'; 5 | 6 | describe('AppComponent', () => { 7 | beforeEach(async () => { 8 | await TestBed.configureTestingModule({ 9 | imports: [RouterTestingModule, PinchZoomModule], 10 | declarations: [AppComponent], 11 | }).compileComponents(); 12 | }); 13 | 14 | it('should create the app', () => { 15 | const fixture = TestBed.createComponent(AppComponent); 16 | const app = fixture.componentInstance; 17 | expect(app).toBeTruthy(); 18 | }); 19 | 20 | it(`should have as title 'ivypinchApp'`, () => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.componentInstance; 23 | expect(app.title).toEqual('ivypinchApp'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/src/lib/properties.ts: -------------------------------------------------------------------------------- 1 | import { Properties } from './interfaces'; 2 | 3 | export const defaultProperties: Properties = { 4 | transitionDuration: 200, 5 | doubleTap: true, 6 | doubleTapScale: 2, 7 | limitZoom: 'original image size', 8 | autoZoomOut: false, 9 | zoomControlScale: 1, 10 | minPanScale: 1.0001, 11 | minScale: 0, 12 | listeners: 'mouse and touch', 13 | wheel: true, 14 | wheelZoomFactor: 0.2, 15 | draggableImage: false, 16 | draggableOnPinch: false, 17 | }; 18 | 19 | export const backwardCompatibilityProperties = { 20 | 'transition-duration': 'transitionDuration', 21 | transitionDurationBackwardCompatibility: 'transitionDuration', 22 | 'double-tap': 'doubleTap', 23 | doubleTapBackwardCompatibility: 'doubleTap', 24 | 'double-tap-scale': 'doubleTapScale', 25 | doubleTapScaleBackwardCompatibility: 'doubleTapScale', 26 | 'auto-zoom-out': 'autoZoomOut', 27 | autoZoomOutBackwardCompatibility: 'autoZoomOut', 28 | 'limit-zoom': 'limitZoom', 29 | limitZoomBackwardCompatibility: 'limitZoom', 30 | }; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nikita Drozhzhin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@meddv/ngx-pinch-zoom", 3 | "version": "20.0.1", 4 | "description": "Pinch zoom component for Angular 20.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/medDV-GmbH/ngx-pinch-zoom" 8 | }, 9 | "author": { 10 | "name": "Nikita Drozhzhin", 11 | "email": "drozhzhin.n.e@gmail.com" 12 | }, 13 | "contributors": [ 14 | { 15 | "name": "Konstantin Schütte", 16 | "email": "team-web@meddv.de", 17 | "url": "https://www.meddv.de" 18 | }, 19 | { 20 | "name": "Björn Schmidt", 21 | "email": "team-web@meddv.de", 22 | "url": "https://www.meddv.de" 23 | } 24 | ], 25 | "homepage": "http://www.meddv.de", 26 | "keywords": [ 27 | "Angular", 28 | "Angular 20", 29 | "ngx", 30 | "Pinch zoom", 31 | "Image zoom", 32 | "Touch image zoom" 33 | ], 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/medDV-GmbH/ngx-pinch-zoom/issues" 37 | }, 38 | "peerDependencies": { 39 | "@angular/common": ">=20.0.0", 40 | "@angular/core": ">=20.0.0" 41 | }, 42 | "dependencies": { 43 | "tslib": "^2.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example namespace declaration will help 3 | // with Intellisense and code completion in your 4 | // IDE or Text Editor. 5 | // *********************************************** 6 | // declare namespace Cypress { 7 | // interface Chainable { 8 | // customCommand(param: any): typeof customCommand; 9 | // } 10 | // } 11 | // 12 | // function customCommand(param: any): void { 13 | // console.warn(param); 14 | // } 15 | // 16 | // NOTE: You can use it like so: 17 | // Cypress.Commands.add('customCommand', customCommand); 18 | // 19 | // *********************************************** 20 | // This example commands.js shows you how to 21 | // create various custom commands and overwrite 22 | // existing commands. 23 | // 24 | // For more comprehensive examples of custom 25 | // commands please read more here: 26 | // https://on.cypress.io/custom-commands 27 | // *********************************************** 28 | // 29 | // 30 | // -- This is a parent command -- 31 | // Cypress.Commands.add("login", (email, password) => { ... }) 32 | // 33 | // 34 | // -- This is a child command -- 35 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 36 | // 37 | // 38 | // -- This is a dual command -- 39 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 40 | // 41 | // 42 | // -- This will overwrite an existing command -- 43 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 44 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example namespace declaration will help 3 | // with Intellisense and code completion in your 4 | // IDE or Text Editor. 5 | // *********************************************** 6 | // declare namespace Cypress { 7 | // interface Chainable { 8 | // customCommand(param: any): typeof customCommand; 9 | // } 10 | // } 11 | // 12 | // function customCommand(param: any): void { 13 | // console.warn(param); 14 | // } 15 | // 16 | // NOTE: You can use it like so: 17 | // Cypress.Commands.add('customCommand', customCommand); 18 | // 19 | // *********************************************** 20 | // This example commands.js shows you how to 21 | // create various custom commands and overwrite 22 | // existing commands. 23 | // 24 | // For more comprehensive examples of custom 25 | // commands please read more here: 26 | // https://on.cypress.io/custom-commands 27 | // *********************************************** 28 | // 29 | // 30 | // -- This is a parent command -- 31 | // Cypress.Commands.add("login", (email, password) => { ... }) 32 | // 33 | // 34 | // -- This is a child command -- 35 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 36 | // 37 | // 38 | // -- This is a dual command -- 39 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 40 | // 41 | // 42 | // -- This will overwrite an existing command -- 43 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 44 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true, // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/ivypinchApp'), 29 | subdir: '.', 30 | reporters: [{ type: 'html' }, { type: 'text-summary' }], 31 | }, 32 | reporters: ['progress', 'kjhtml'], 33 | port: 9876, 34 | colors: true, 35 | logLevel: config.LOG_INFO, 36 | autoWatch: true, 37 | browsers: ['Chrome'], 38 | singleRun: false, 39 | restartOnFileChange: true, 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true, // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, '../../coverage/ngx-pinch-zoom'), 29 | subdir: '.', 30 | reporters: [{ type: 'html' }, { type: 'text-summary' }], 31 | }, 32 | reporters: ['progress', 'kjhtml'], 33 | port: 9876, 34 | colors: true, 35 | logLevel: config.LOG_INFO, 36 | autoWatch: true, 37 | browsers: ['Chrome'], 38 | singleRun: false, 39 | restartOnFileChange: true, 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-pinch-zoom", 3 | "version": "20.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "build-lib": "npx ng-packagr -p projects/ngx-pinch-zoom/ng-package.json", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e", 12 | "cypress:open": "cypress open", 13 | "cypress:run": "cypress run" 14 | }, 15 | "dependencies": { 16 | "@angular/animations": "^20.3.15", 17 | "@angular/common": "^20.3.15", 18 | "@angular/compiler": "^20.3.15", 19 | "@angular/core": "^20.3.15", 20 | "@angular/forms": "^20.3.15", 21 | "@angular/platform-browser": "^20.3.15", 22 | "@angular/platform-browser-dynamic": "^20.3.15", 23 | "@angular/router": "^20.3.15", 24 | "rxjs": "~7.5.2", 25 | "tslib": "^2.3.1", 26 | "zone.js": "0.15.0" 27 | }, 28 | "devDependencies": { 29 | "@angular-devkit/build-angular": "^20.3.12", 30 | "@angular-eslint/eslint-plugin": "^20.7.0", 31 | "@angular-eslint/eslint-plugin-template": "^20.7.0", 32 | "@angular/cli": "^20.3.12", 33 | "@angular/compiler-cli": "^20.3.15", 34 | "@cypress/schematic": "^1.6.0", 35 | "@types/jasmine": "~3.10.3", 36 | "@types/node": "^16.18.24", 37 | "@typescript-eslint/eslint-plugin": "^5.10.2", 38 | "@typescript-eslint/parser": "^5.10.2", 39 | "cypress": "latest", 40 | "eslint": "^8.8.0", 41 | "eslint-plugin-import": "^2.25.4", 42 | "eslint-plugin-jsdoc": "^37.7.1", 43 | "eslint-plugin-prefer-arrow": "^1.2.3", 44 | "jasmine-core": "~4.6.1", 45 | "jasmine-spec-reporter": "~7.0.0", 46 | "karma": "~6.4.4", 47 | "karma-chrome-launcher": "~3.2.0", 48 | "karma-coverage": "~2.2.1", 49 | "karma-jasmine": "~5.1.0", 50 | "karma-jasmine-html-reporter": "^2.1.0", 51 | "ng-packagr": "^20.3.2", 52 | "prettier": "^2.7.1", 53 | "ts-node": "~10.4.0", 54 | "typescript": "5.9.3" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * IE11 requires the following for NgClass support on SVG elements 23 | */ 24 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 25 | 26 | /** 27 | * Web Animations `@angular/platform-browser/animations` 28 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 29 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 30 | */ 31 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 32 | 33 | /** 34 | * By default, zone.js will patch all possible macroTask and DomEvents 35 | * user can disable parts of macroTask/DomEvents patch by setting following flags 36 | * because those flags need to be set before `zone.js` being loaded, and webpack 37 | * will put import in the top of bundle, so user need to create a separate file 38 | * in this directory (for example: zone-flags.ts), and put the following flags 39 | * into that file, and then add the following code before importing zone.js. 40 | * import './zone-flags'; 41 | * 42 | * The flags allowed in zone-flags.ts are listed here. 43 | * 44 | * The following flags will work for all browsers. 45 | * 46 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 47 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 48 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 49 | * 50 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 51 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 52 | * 53 | * (window as any).__Zone_enable_cross_context_check = true; 54 | * 55 | */ 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js'; // Included with Angular CLI. 61 | 62 | /*************************************************************************************************** 63 | * APPLICATION IMPORTS 64 | */ 65 | -------------------------------------------------------------------------------- /src/app/app.component.sass: -------------------------------------------------------------------------------- 1 | .wrapper 2 | max-width: 500px 3 | margin: 0 auto 4 | font-family: system-ui 5 | 6 | .demo 7 | margin-bottom: 60px 8 | 9 | .actions 10 | padding-top: 20px 11 | 12 | h1 13 | font-size: 30px 14 | h2 15 | font-size: 20px 16 | 17 | /* Colors */ 18 | 19 | $black: #00000a 20 | $white: #fff 21 | $dark-white: rgba(0, 0, 0, 0.75) 22 | $blue: #0366d6 23 | $dark-blue: #035cbf 24 | $green: #7cae7a 25 | $dark-green: #709a6e 26 | $gray: rgba(0, 0, 0, 0.5) 27 | $light-shadow: rgba(0, 0, 0, 0.15) 28 | 29 | /* Buttons */ 30 | 31 | .btn 32 | display: inline-block 33 | min-width: 190px 34 | line-height: 56px 35 | font-weight: 500 36 | border-radius: 6px 37 | text-align: center 38 | margin: 0 15px 15px 0 39 | background: none 40 | transition: all 200ms 41 | color: $black 42 | outline: none 43 | font-size: 15px 44 | letter-spacing: 1px 45 | text-transform: uppercase 46 | font-weight: 600 47 | background: $white 48 | .btn:hover 49 | color: $dark-white 50 | text-decoration: none 51 | cursor: pointer 52 | box-shadow: 0px 3px 10px $light-shadow 53 | .btn:active 54 | position: relative 55 | top: 2px 56 | 57 | .btn-small 58 | line-height: 32px 59 | font-weight: normal 60 | padding: 0 8px 61 | font-size: 14px 62 | letter-spacing: 0px 63 | min-width: auto 64 | text-transform: none 65 | 66 | .btn-outline 67 | border: 1px solid #999 68 | .btn-outline:hover 69 | border-color: $dark-white 70 | .btn-rounded 71 | border-radius: 100px 72 | .btn[disabled] 73 | cursor: default 74 | box-shadow: none 75 | 76 | .btn-light 77 | color: $white 78 | .btn-light:hover 79 | color: rgba(255, 255, 255, 0.75) 80 | .btn-light.btn-outline 81 | border: 1px solid $white 82 | .btn-light.btn-outline:hover 83 | border-color: rgba(255, 255, 255, 0.75) 84 | 85 | .btn-green 86 | background: $green 87 | color: $white 88 | border: none 89 | .btn-green.btn-outline 90 | border: 1px solid $green 91 | color: $green 92 | background: none 93 | .btn-green.btn-clear 94 | color: $green 95 | .btn-green:hover 96 | background: $dark-green 97 | color: $white 98 | .btn-green.btn-outline:hover 99 | background: none 100 | border-color: $dark-green 101 | color: $dark-green 102 | .btn-green.btn-clear:hover 103 | color: $dark-green 104 | .btn-green[disabled] 105 | background: #aaa 106 | .btn-green.btn-outline[disabled] 107 | background: none 108 | border-color: #aaa 109 | color: #aaa 110 | 111 | .btn-blue 112 | background: $blue 113 | color: $white 114 | border: none 115 | .btn-blue.btn-outline 116 | border: 1px solid $blue 117 | color: $blue 118 | background: none 119 | .btn-blue.btn-clear 120 | color: $blue 121 | background: none 122 | .btn-blue:hover 123 | background: $dark-blue 124 | color: $white 125 | .btn-blue.btn-outline:hover 126 | background: none 127 | border-color: $dark-blue 128 | color: $dark-blue 129 | .btn-blue.btn-clear:hover 130 | color: $dark-blue 131 | 132 | .btn-clear 133 | background: none 134 | border: none 135 | .btn-clear:hover 136 | background: none -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/src/lib/pinch-zoom.component.sass: -------------------------------------------------------------------------------- 1 | :host 2 | position: relative 3 | overflow: hidden 4 | display: block 5 | 6 | .pinch-zoom-content 7 | height: inherit 8 | 9 | .pz-dragging 10 | cursor: all-scroll 11 | 12 | 13 | /* Zoom button */ 14 | .pz-zoom-button 15 | position: absolute 16 | z-index: 1000 17 | color: #fff 18 | background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgc3R5bGU9IiI+PHJlY3QgaWQ9ImJhY2tncm91bmRyZWN0IiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4PSIwIiB5PSIwIiBmaWxsPSJub25lIiBzdHJva2U9Im5vbmUiLz48ZyBjbGFzcz0iY3VycmVudExheWVyIiBzdHlsZT0iIj48dGl0bGU+TGF5ZXIgMTwvdGl0bGU+PHBhdGggZD0iTTE1LjUgMTRoLS43OWwtLjI4LS4yN0MxNS40MSAxMi41OSAxNiAxMS4xMSAxNiA5LjUgMTYgNS45MSAxMy4wOSAzIDkuNSAzUzMgNS45MSAzIDkuNSA1LjkxIDE2IDkuNSAxNmMxLjYxIDAgMy4wOS0uNTkgNC4yMy0xLjU3bC4yNy4yOHYuNzlsNSA0Ljk5TDIwLjQ5IDE5bC00Ljk5LTV6bS02IDBDNy4wMSAxNCA1IDExLjk5IDUgOS41UzcuMDEgNSA5LjUgNSAxNCA3LjAxIDE0IDkuNSAxMS45OSAxNCA5LjUgMTR6IiBpZD0ic3ZnXzEiIGNsYXNzPSIiIGZpbGw9IiNmZmZmZmYiIGZpbGwtb3BhY2l0eT0iMSIvPjxwYXRoIGQ9Ik0xMiAxMGgtMnYySDl2LTJIN1Y5aDJWN2gxdjJoMnYxeiIgaWQ9InN2Z18zIiBjbGFzcz0iIiBmaWxsPSIjZmZmZmZmIiBmaWxsLW9wYWNpdHk9IjEiLz48L2c+PC9zdmc+), url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHJlY3QgaWQ9ImJhY2tncm91bmRyZWN0IiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4PSIwIiB5PSIwIiBmaWxsPSJub25lIiBzdHJva2U9Im5vbmUiLz48ZyBjbGFzcz0iY3VycmVudExheWVyIiBzdHlsZT0iIj48dGl0bGU+TGF5ZXIgMTwvdGl0bGU+PHBhdGggZD0iTTE1LjUgMTRoLS43OWwtLjI4LS4yN0MxNS40MSAxMi41OSAxNiAxMS4xMSAxNiA5LjUgMTYgNS45MSAxMy4wOSAzIDkuNSAzUzMgNS45MSAzIDkuNSA1LjkxIDE2IDkuNSAxNmMxLjYxIDAgMy4wOS0uNTkgNC4yMy0xLjU3bC4yNy4yOHYuNzlsNSA0Ljk5TDIwLjQ5IDE5bC00Ljk5LTV6bS02IDBDNy4wMSAxNCA1IDExLjk5IDUgOS41UzcuMDEgNSA5LjUgNSAxNCA3LjAxIDE0IDkuNSAxMS45OSAxNCA5LjUgMTR6TTcgOWg1djFIN3oiIGlkPSJzdmdfMiIgY2xhc3M9IiIgZmlsbD0iI2ZmZmZmZiIgZmlsbC1vcGFjaXR5PSIxIi8+PC9nPjwvc3ZnPg==) 19 | background-color: rgba(0, 0, 0, .8) 20 | background-position: center, -1000px 21 | background-repeat: no-repeat, no-repeat 22 | background-size: 40px 23 | width: 56px 24 | height: 56px 25 | border-radius: 4px 26 | opacity: 0.5 27 | cursor: pointer 28 | transition: opacity .1s 29 | user-select: none 30 | 31 | .pz-zoom-button-out 32 | background-position: -1000px, center 33 | 34 | .pz-zoom-button:hover 35 | opacity: 0.7 36 | 37 | .pz-zoom-button.pz-zoom-control-position-right 38 | right: 16px 39 | top: 50% 40 | margin-top: -28px 41 | 42 | .pz-zoom-button.pz-zoom-control-position-right-bottom 43 | right: 16px 44 | bottom: 32px 45 | 46 | .pz-zoom-button.pz-zoom-control-position-bottom 47 | bottom: 16px 48 | left: 50% 49 | margin-left: -28px 50 | 51 | 52 | /* Zoom control */ 53 | .pz-zoom-control 54 | position: absolute 55 | background-color: rgba(0, 0, 0, .8) 56 | border-radius: 4px 57 | overflow: hidden 58 | 59 | .pz-zoom-control.pz-zoom-control-position-right 60 | right: 16px 61 | top: 50% 62 | margin-top: -48px 63 | 64 | .pz-zoom-control.pz-zoom-control-position-right-bottom 65 | right: 16px 66 | bottom: 32px 67 | 68 | .pz-zoom-control.pz-zoom-control-position-bottom 69 | bottom: 16px 70 | left: 50% 71 | margin-left: -48px 72 | 73 | .pz-zoom-in, 74 | .pz-zoom-out 75 | width: 48px 76 | height: 48px 77 | background-position: center 78 | background-repeat: no-repeat 79 | opacity: 1 80 | cursor: pointer 81 | 82 | .pz-zoom-in:hover, 83 | .pz-zoom-out:hover 84 | background-color: rgba(255, 255, 255, 0.2) 85 | 86 | .pz-zoom-control-position-bottom .pz-zoom-in, 87 | .pz-zoom-control-position-bottom .pz-zoom-out 88 | float: right 89 | 90 | .pz-disabled 91 | opacity: 0.5 92 | cursor: default 93 | 94 | .pz-disabled:hover 95 | background-color: rgba(255, 255, 255, 0) 96 | 97 | .pz-zoom-in 98 | background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgc3R5bGU9IiI+PHJlY3QgaWQ9ImJhY2tncm91bmRyZWN0IiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4PSIwIiB5PSIwIiBmaWxsPSJub25lIiBzdHJva2U9Im5vbmUiLz48ZyBjbGFzcz0iY3VycmVudExheWVyIiBzdHlsZT0iIj48dGl0bGU+TGF5ZXIgMTwvdGl0bGU+PHBhdGggZD0iTTE5IDEzaC02djZoLTJ2LTZINXYtMmg2VjVoMnY2aDZ2MnoiIGlkPSJzdmdfMSIgY2xhc3M9IiIgc3Ryb2tlPSJub25lIiBmaWxsPSIjZmZmZmZmIiBmaWxsLW9wYWNpdHk9IjEiLz48cGF0aCBkPSJNLTE1LjgzNjczNDQyMDQ2MTY1Myw0NC41MzU0MDkzMDY3MTAxOCBoNTguMjA0MDgwODI3NTkzMDkgdi02LjU3NjIyNjcyMzM2OTIyMTUgSC0xNS44MzY3MzQ0MjA0NjE2NTMgeiIgZmlsbD0ibm9uZSIgaWQ9InN2Z18yIiBjbGFzcz0iIiBzdHJva2U9Im5vbmUiLz48L2c+PC9zdmc+) 99 | 100 | .pz-zoom-out 101 | background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHJlY3QgaWQ9ImJhY2tncm91bmRyZWN0IiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4PSIwIiB5PSIwIiBmaWxsPSJub25lIiBzdHJva2U9Im5vbmUiLz48ZyBjbGFzcz0iY3VycmVudExheWVyIiBzdHlsZT0iIj48dGl0bGU+TGF5ZXIgMTwvdGl0bGU+PHBhdGggZD0iTTE5IDEzSDV2LTJoMTR2MnoiIGlkPSJzdmdfMSIgY2xhc3M9IiIgZmlsbD0iI2ZmZmZmZiIgZmlsbC1vcGFjaXR5PSIxIi8+PC9nPjwvc3ZnPg==) 102 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": false 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "ivypinchApp": { 10 | "projectType": "application", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "sass" 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:application", 22 | "options": { 23 | "outputPath": { 24 | "base": "dist/ivypinchApp" 25 | }, 26 | "index": "src/index.html", 27 | "polyfills": [ 28 | "src/polyfills.ts" 29 | ], 30 | "tsConfig": "tsconfig.app.json", 31 | "aot": true, 32 | "assets": ["src/favicon.ico", "src/assets"], 33 | "styles": ["src/styles.sass"], 34 | "scripts": [], 35 | "browser": "src/main.ts" 36 | }, 37 | "configurations": { 38 | "production": { 39 | "fileReplacements": [ 40 | { 41 | "replace": "src/environments/environment.ts", 42 | "with": "src/environments/environment.prod.ts" 43 | } 44 | ], 45 | "optimization": true, 46 | "outputHashing": "all", 47 | "sourceMap": false, 48 | "namedChunks": false, 49 | "extractLicenses": true, 50 | "budgets": [ 51 | { 52 | "type": "initial", 53 | "maximumWarning": "2mb", 54 | "maximumError": "5mb" 55 | }, 56 | { 57 | "type": "anyComponentStyle", 58 | "maximumWarning": "6kb", 59 | "maximumError": "10kb" 60 | } 61 | ] 62 | } 63 | } 64 | }, 65 | "serve": { 66 | "builder": "@angular-devkit/build-angular:dev-server", 67 | "options": { 68 | "buildTarget": "ivypinchApp:build" 69 | }, 70 | "configurations": { 71 | "production": { 72 | "buildTarget": "ivypinchApp:build:production" 73 | } 74 | } 75 | }, 76 | "extract-i18n": { 77 | "builder": "@angular-devkit/build-angular:extract-i18n", 78 | "options": { 79 | "buildTarget": "ivypinchApp:build" 80 | } 81 | }, 82 | "test": { 83 | "builder": "@angular-devkit/build-angular:karma", 84 | "options": { 85 | "main": "src/test.ts", 86 | "polyfills": "src/polyfills.ts", 87 | "tsConfig": "tsconfig.spec.json", 88 | "karmaConfig": "karma.conf.js", 89 | "assets": ["src/favicon.ico", "src/assets"], 90 | "styles": ["src/styles.sass"], 91 | "scripts": [] 92 | } 93 | }, 94 | "lint": { 95 | "builder": "@angular-devkit/build-angular:tslint", 96 | "options": { 97 | "tsConfig": ["tsconfig.app.json", "tsconfig.spec.json", "cypress/tsconfig.json"], 98 | "exclude": ["**/node_modules/**"] 99 | } 100 | }, 101 | "e2e": { 102 | "builder": "@cypress/schematic:cypress", 103 | "options": { 104 | "devServerTarget": "ivypinchApp:serve", 105 | "watch": true, 106 | "headless": false 107 | }, 108 | "configurations": { 109 | "production": { 110 | "devServerTarget": "ivypinchApp:serve:production" 111 | } 112 | } 113 | }, 114 | "cypress-run": { 115 | "builder": "@cypress/schematic:cypress", 116 | "options": { 117 | "devServerTarget": "ivypinchApp:serve" 118 | }, 119 | "configurations": { 120 | "production": { 121 | "devServerTarget": "ivypinchApp:serve:production" 122 | } 123 | } 124 | }, 125 | "cypress-open": { 126 | "builder": "@cypress/schematic:cypress", 127 | "options": { 128 | "watch": true, 129 | "headless": false 130 | } 131 | } 132 | } 133 | }, 134 | "ngx-pinch-zoom": { 135 | "projectType": "library", 136 | "root": "projects/ngx-pinch-zoom", 137 | "sourceRoot": "projects/ngx-pinch-zoom/src", 138 | "prefix": "lib", 139 | "architect": { 140 | "build": { 141 | "builder": "@angular-devkit/build-angular:ng-packagr", 142 | "options": { 143 | "tsConfig": "projects/ngx-pinch-zoom/tsconfig.lib.json", 144 | "project": "projects/ngx-pinch-zoom/ng-package.json" 145 | }, 146 | "configurations": { 147 | "production": { 148 | "tsConfig": "projects/ngx-pinch-zoom/tsconfig.lib.prod.json" 149 | } 150 | } 151 | }, 152 | "test": { 153 | "builder": "@angular-devkit/build-angular:karma", 154 | "options": { 155 | "main": "projects/ngx-pinch-zoom/src/test.ts", 156 | "tsConfig": "projects/ngx-pinch-zoom/tsconfig.spec.json", 157 | "karmaConfig": "projects/ngx-pinch-zoom/karma.conf.js" 158 | } 159 | }, 160 | "lint": { 161 | "builder": "@angular-devkit/build-angular:tslint", 162 | "options": { 163 | "tsConfig": [ 164 | "projects/ngx-pinch-zoom/tsconfig.lib.json", 165 | "projects/ngx-pinch-zoom/tsconfig.spec.json", 166 | "projects/ngx-pinch-zoom/cypress/tsconfig.json" 167 | ], 168 | "exclude": ["**/node_modules/**"] 169 | } 170 | }, 171 | "cypress-run": { 172 | "builder": "@cypress/schematic:cypress", 173 | "options": { 174 | "devServerTarget": "ngx-pinch-zoom:serve", 175 | "configFile": "projects/ngx-pinch-zoom/cypress.json" 176 | }, 177 | "configurations": { 178 | "production": { 179 | "devServerTarget": "ngx-pinch-zoom:serve:production" 180 | } 181 | } 182 | }, 183 | "cypress-open": { 184 | "builder": "@cypress/schematic:cypress", 185 | "options": { 186 | "watch": true, 187 | "headless": false, 188 | "configFile": "projects/ngx-pinch-zoom/cypress.json" 189 | } 190 | }, 191 | "e2e": { 192 | "builder": "@cypress/schematic:cypress", 193 | "options": { 194 | "devServerTarget": "ngx-pinch-zoom:serve", 195 | "watch": true, 196 | "headless": false 197 | }, 198 | "configurations": { 199 | "production": { 200 | "devServerTarget": "ngx-pinch-zoom:serve:production" 201 | } 202 | } 203 | } 204 | } 205 | } 206 | }, 207 | "schematics": { 208 | "@schematics/angular:component": { 209 | "type": "component" 210 | }, 211 | "@schematics/angular:directive": { 212 | "type": "directive" 213 | }, 214 | "@schematics/angular:service": { 215 | "type": "service" 216 | }, 217 | "@schematics/angular:guard": { 218 | "typeSeparator": "." 219 | }, 220 | "@schematics/angular:interceptor": { 221 | "typeSeparator": "." 222 | }, 223 | "@schematics/angular:module": { 224 | "typeSeparator": "." 225 | }, 226 | "@schematics/angular:pipe": { 227 | "typeSeparator": "." 228 | }, 229 | "@schematics/angular:resolver": { 230 | "typeSeparator": "." 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | 👋 Hi! This file was autogenerated by tslint-to-eslint-config. 3 | https://github.com/typescript-eslint/tslint-to-eslint-config 4 | 5 | It represents the closest reasonable ESLint configuration to this 6 | project's original TSLint configuration. 7 | 8 | We recommend eventually switching this configuration to extend from 9 | the recommended rulesets in typescript-eslint. 10 | https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md 11 | 12 | Happy linting! 💖 13 | */ 14 | module.exports = { 15 | env: { 16 | browser: true, 17 | es6: true, 18 | node: true, 19 | }, 20 | extends: ['plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking'], 21 | parser: '@typescript-eslint/parser', 22 | parserOptions: { 23 | project: 'tsconfig.json', 24 | sourceType: 'module', 25 | }, 26 | plugins: [ 27 | 'eslint-plugin-import', 28 | 'eslint-plugin-jsdoc', 29 | '@angular-eslint/eslint-plugin', 30 | '@angular-eslint/eslint-plugin-template', 31 | 'eslint-plugin-prefer-arrow', 32 | '@typescript-eslint', 33 | '@typescript-eslint/tslint', 34 | ], 35 | rules: { 36 | '@angular-eslint/component-class-suffix': 'error', 37 | '@angular-eslint/component-selector': [ 38 | 'error', 39 | { 40 | type: 'element', 41 | prefix: 'app', 42 | style: 'kebab-case', 43 | }, 44 | ], 45 | '@angular-eslint/contextual-lifecycle': 'error', 46 | '@angular-eslint/directive-class-suffix': 'error', 47 | '@angular-eslint/directive-selector': [ 48 | 'error', 49 | { 50 | type: 'attribute', 51 | prefix: 'app', 52 | style: 'camelCase', 53 | }, 54 | ], 55 | '@angular-eslint/no-conflicting-lifecycle': 'error', 56 | '@angular-eslint/no-host-metadata-property': 'error', 57 | '@angular-eslint/no-input-rename': 'error', 58 | '@angular-eslint/no-inputs-metadata-property': 'error', 59 | '@angular-eslint/no-output-native': 'error', 60 | '@angular-eslint/no-output-on-prefix': 'error', 61 | '@angular-eslint/no-output-rename': 'error', 62 | '@angular-eslint/no-outputs-metadata-property': 'error', 63 | '@angular-eslint/template/banana-in-box': 'error', 64 | '@angular-eslint/template/eqeqeq': 'error', 65 | '@angular-eslint/template/no-negated-async': 'error', 66 | '@angular-eslint/use-lifecycle-interface': 'error', 67 | '@angular-eslint/use-pipe-transform-interface': 'error', 68 | '@typescript-eslint/adjacent-overload-signatures': 'error', 69 | '@typescript-eslint/array-type': 'off', 70 | '@typescript-eslint/ban-types': [ 71 | 'error', 72 | { 73 | types: { 74 | Object: { 75 | message: 'Avoid using the `Object` type. Did you mean `object`?', 76 | }, 77 | Function: { 78 | message: 'Avoid using the `Function` type. Prefer a specific function type, like `() => void`.', 79 | }, 80 | Boolean: { 81 | message: 'Avoid using the `Boolean` type. Did you mean `boolean`?', 82 | }, 83 | Number: { 84 | message: 'Avoid using the `Number` type. Did you mean `number`?', 85 | }, 86 | String: { 87 | message: 'Avoid using the `String` type. Did you mean `string`?', 88 | }, 89 | Symbol: { 90 | message: 'Avoid using the `Symbol` type. Did you mean `symbol`?', 91 | }, 92 | }, 93 | }, 94 | ], 95 | '@typescript-eslint/consistent-type-assertions': 'error', 96 | '@typescript-eslint/dot-notation': 'error', 97 | '@typescript-eslint/indent': [ 98 | 'error', 99 | 4, 100 | { 101 | FunctionDeclaration: { 102 | parameters: 'first', 103 | }, 104 | FunctionExpression: { 105 | parameters: 'first', 106 | }, 107 | }, 108 | ], 109 | '@typescript-eslint/member-delimiter-style': [ 110 | 'error', 111 | { 112 | multiline: { 113 | delimiter: 'semi', 114 | requireLast: true, 115 | }, 116 | singleline: { 117 | delimiter: 'semi', 118 | requireLast: false, 119 | }, 120 | }, 121 | ], 122 | '@typescript-eslint/member-ordering': 'error', 123 | '@typescript-eslint/naming-convention': 'error', 124 | '@typescript-eslint/no-empty-function': 'off', 125 | '@typescript-eslint/no-empty-interface': 'error', 126 | '@typescript-eslint/no-explicit-any': 'off', 127 | '@typescript-eslint/no-inferrable-types': [ 128 | 'error', 129 | { 130 | ignoreParameters: true, 131 | }, 132 | ], 133 | '@typescript-eslint/no-misused-new': 'error', 134 | '@typescript-eslint/no-namespace': 'error', 135 | '@typescript-eslint/no-non-null-assertion': 'error', 136 | '@typescript-eslint/no-parameter-properties': 'off', 137 | '@typescript-eslint/no-shadow': [ 138 | 'error', 139 | { 140 | hoist: 'all', 141 | }, 142 | ], 143 | '@typescript-eslint/no-unused-expressions': 'error', 144 | '@typescript-eslint/no-use-before-define': 'off', 145 | '@typescript-eslint/no-var-requires': 'off', 146 | '@typescript-eslint/prefer-for-of': 'error', 147 | '@typescript-eslint/prefer-function-type': 'error', 148 | '@typescript-eslint/prefer-namespace-keyword': 'error', 149 | '@typescript-eslint/quotes': ['error', 'single'], 150 | '@typescript-eslint/semi': ['error', 'always'], 151 | '@typescript-eslint/triple-slash-reference': [ 152 | 'error', 153 | { 154 | path: 'always', 155 | types: 'prefer-import', 156 | lib: 'always', 157 | }, 158 | ], 159 | '@typescript-eslint/type-annotation-spacing': 'error', 160 | '@typescript-eslint/unified-signatures': 'error', 161 | 'arrow-body-style': 'error', 162 | complexity: 'off', 163 | 'constructor-super': 'error', 164 | curly: 'error', 165 | 'dot-notation': 'error', 166 | 'eol-last': 'error', 167 | eqeqeq: ['error', 'smart'], 168 | 'guard-for-in': 'error', 169 | 'id-denylist': ['error', 'any', 'Number', 'number', 'String', 'string', 'Boolean', 'boolean', 'Undefined', 'undefined'], 170 | 'id-match': 'error', 171 | 'import/no-deprecated': 'warn', 172 | indent: 'error', 173 | 'jsdoc/check-alignment': 'error', 174 | 'jsdoc/check-indentation': 'error', 175 | 'jsdoc/newline-after-description': 'error', 176 | 'jsdoc/no-types': 'error', 177 | 'max-classes-per-file': 'off', 178 | 'max-len': [ 179 | 'error', 180 | { 181 | code: 140, 182 | }, 183 | ], 184 | 'new-parens': 'error', 185 | 'no-bitwise': 'error', 186 | 'no-caller': 'error', 187 | 'no-cond-assign': 'error', 188 | 'no-console': [ 189 | 'error', 190 | { 191 | allow: [ 192 | 'log', 193 | 'warn', 194 | 'dir', 195 | 'timeLog', 196 | 'assert', 197 | 'clear', 198 | 'count', 199 | 'countReset', 200 | 'group', 201 | 'groupEnd', 202 | 'table', 203 | 'dirxml', 204 | 'error', 205 | 'groupCollapsed', 206 | 'Console', 207 | 'profile', 208 | 'profileEnd', 209 | 'timeStamp', 210 | 'context', 211 | ], 212 | }, 213 | ], 214 | 'no-debugger': 'error', 215 | 'no-empty': 'off', 216 | 'no-empty-function': 'off', 217 | 'no-eval': 'error', 218 | 'no-fallthrough': 'error', 219 | 'no-invalid-this': 'off', 220 | 'no-new-wrappers': 'error', 221 | 'no-restricted-imports': ['error', 'rxjs/Rx'], 222 | 'no-shadow': 'error', 223 | 'no-throw-literal': 'error', 224 | 'no-trailing-spaces': 'error', 225 | 'no-undef-init': 'error', 226 | 'no-underscore-dangle': 'error', 227 | 'no-unsafe-finally': 'error', 228 | 'no-unused-expressions': 'error', 229 | 'no-unused-labels': 'error', 230 | 'no-use-before-define': 'off', 231 | 'no-var': 'error', 232 | 'object-shorthand': 'error', 233 | 'one-var': ['error', 'never'], 234 | 'prefer-arrow/prefer-arrow-functions': 'error', 235 | 'prefer-const': 'error', 236 | 'quote-props': ['error', 'as-needed'], 237 | quotes: 'error', 238 | radix: 'error', 239 | semi: 'error', 240 | 'space-before-function-paren': [ 241 | 'error', 242 | { 243 | anonymous: 'never', 244 | asyncArrow: 'always', 245 | named: 'never', 246 | }, 247 | ], 248 | 'spaced-comment': [ 249 | 'error', 250 | 'always', 251 | { 252 | markers: ['/'], 253 | }, 254 | ], 255 | 'use-isnan': 'error', 256 | 'valid-typeof': 'off', 257 | '@typescript-eslint/tslint/config': [ 258 | 'error', 259 | { 260 | rules: { 261 | 'import-spacing': true, 262 | typedef: [true, 'call-signature'], 263 | whitespace: [true, 'check-branch', 'check-decl', 'check-operator', 'check-separator', 'check-type', 'check-typecast'], 264 | }, 265 | }, 266 | ], 267 | }, 268 | }; 269 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | 👋 Hi! This file was autogenerated by tslint-to-eslint-config. 3 | https://github.com/typescript-eslint/tslint-to-eslint-config 4 | 5 | It represents the closest reasonable ESLint configuration to this 6 | project's original TSLint configuration. 7 | 8 | We recommend eventually switching this configuration to extend from 9 | the recommended rulesets in typescript-eslint. 10 | https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md 11 | 12 | Happy linting! 💖 13 | */ 14 | module.exports = { 15 | env: { 16 | browser: true, 17 | es6: true, 18 | node: true, 19 | }, 20 | extends: ['plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking'], 21 | parser: '@typescript-eslint/parser', 22 | parserOptions: { 23 | project: 'tsconfig.json', 24 | sourceType: 'module', 25 | }, 26 | plugins: [ 27 | 'eslint-plugin-import', 28 | 'eslint-plugin-jsdoc', 29 | '@angular-eslint/eslint-plugin', 30 | '@angular-eslint/eslint-plugin-template', 31 | 'eslint-plugin-prefer-arrow', 32 | '@typescript-eslint', 33 | '@typescript-eslint/tslint', 34 | ], 35 | rules: { 36 | '@angular-eslint/component-class-suffix': 'error', 37 | '@angular-eslint/component-selector': [ 38 | 'error', 39 | { 40 | type: 'element', 41 | prefix: 'app', 42 | style: 'kebab-case', 43 | }, 44 | ], 45 | '@angular-eslint/contextual-lifecycle': 'error', 46 | '@angular-eslint/directive-class-suffix': 'error', 47 | '@angular-eslint/directive-selector': [ 48 | 'error', 49 | { 50 | type: 'attribute', 51 | prefix: 'app', 52 | style: 'camelCase', 53 | }, 54 | ], 55 | '@angular-eslint/no-conflicting-lifecycle': 'error', 56 | '@angular-eslint/no-host-metadata-property': 'error', 57 | '@angular-eslint/no-input-rename': 'error', 58 | '@angular-eslint/no-inputs-metadata-property': 'error', 59 | '@angular-eslint/no-output-native': 'error', 60 | '@angular-eslint/no-output-on-prefix': 'error', 61 | '@angular-eslint/no-output-rename': 'error', 62 | '@angular-eslint/no-outputs-metadata-property': 'error', 63 | '@angular-eslint/template/banana-in-box': 'error', 64 | '@angular-eslint/template/eqeqeq': 'error', 65 | '@angular-eslint/template/no-negated-async': 'error', 66 | '@angular-eslint/use-lifecycle-interface': 'error', 67 | '@angular-eslint/use-pipe-transform-interface': 'error', 68 | '@typescript-eslint/adjacent-overload-signatures': 'error', 69 | '@typescript-eslint/array-type': 'off', 70 | '@typescript-eslint/ban-types': [ 71 | 'error', 72 | { 73 | types: { 74 | Object: { 75 | message: 'Avoid using the `Object` type. Did you mean `object`?', 76 | }, 77 | Function: { 78 | message: 'Avoid using the `Function` type. Prefer a specific function type, like `() => void`.', 79 | }, 80 | Boolean: { 81 | message: 'Avoid using the `Boolean` type. Did you mean `boolean`?', 82 | }, 83 | Number: { 84 | message: 'Avoid using the `Number` type. Did you mean `number`?', 85 | }, 86 | String: { 87 | message: 'Avoid using the `String` type. Did you mean `string`?', 88 | }, 89 | Symbol: { 90 | message: 'Avoid using the `Symbol` type. Did you mean `symbol`?', 91 | }, 92 | }, 93 | }, 94 | ], 95 | '@typescript-eslint/consistent-type-assertions': 'error', 96 | '@typescript-eslint/dot-notation': 'error', 97 | '@typescript-eslint/indent': [ 98 | 'error', 99 | 4, 100 | { 101 | FunctionDeclaration: { 102 | parameters: 'first', 103 | }, 104 | FunctionExpression: { 105 | parameters: 'first', 106 | }, 107 | }, 108 | ], 109 | '@typescript-eslint/member-delimiter-style': [ 110 | 'error', 111 | { 112 | multiline: { 113 | delimiter: 'semi', 114 | requireLast: true, 115 | }, 116 | singleline: { 117 | delimiter: 'semi', 118 | requireLast: false, 119 | }, 120 | }, 121 | ], 122 | '@typescript-eslint/member-ordering': 'error', 123 | '@typescript-eslint/naming-convention': 'error', 124 | '@typescript-eslint/no-empty-function': 'off', 125 | '@typescript-eslint/no-empty-interface': 'error', 126 | '@typescript-eslint/no-explicit-any': 'off', 127 | '@typescript-eslint/no-inferrable-types': [ 128 | 'error', 129 | { 130 | ignoreParameters: true, 131 | }, 132 | ], 133 | '@typescript-eslint/no-misused-new': 'error', 134 | '@typescript-eslint/no-namespace': 'error', 135 | '@typescript-eslint/no-non-null-assertion': 'error', 136 | '@typescript-eslint/no-parameter-properties': 'off', 137 | '@typescript-eslint/no-shadow': [ 138 | 'error', 139 | { 140 | hoist: 'all', 141 | }, 142 | ], 143 | '@typescript-eslint/no-unused-expressions': 'error', 144 | '@typescript-eslint/no-use-before-define': 'off', 145 | '@typescript-eslint/no-var-requires': 'off', 146 | '@typescript-eslint/prefer-for-of': 'error', 147 | '@typescript-eslint/prefer-function-type': 'error', 148 | '@typescript-eslint/prefer-namespace-keyword': 'error', 149 | '@typescript-eslint/quotes': ['error', 'single'], 150 | '@typescript-eslint/semi': ['error', 'always'], 151 | '@typescript-eslint/triple-slash-reference': [ 152 | 'error', 153 | { 154 | path: 'always', 155 | types: 'prefer-import', 156 | lib: 'always', 157 | }, 158 | ], 159 | '@typescript-eslint/type-annotation-spacing': 'error', 160 | '@typescript-eslint/unified-signatures': 'error', 161 | 'arrow-body-style': 'error', 162 | complexity: 'off', 163 | 'constructor-super': 'error', 164 | curly: 'error', 165 | 'dot-notation': 'error', 166 | 'eol-last': 'error', 167 | eqeqeq: ['error', 'smart'], 168 | 'guard-for-in': 'error', 169 | 'id-denylist': ['error', 'any', 'Number', 'number', 'String', 'string', 'Boolean', 'boolean', 'Undefined', 'undefined'], 170 | 'id-match': 'error', 171 | 'import/no-deprecated': 'warn', 172 | indent: 'error', 173 | 'jsdoc/check-alignment': 'error', 174 | 'jsdoc/check-indentation': 'error', 175 | 'jsdoc/newline-after-description': 'error', 176 | 'jsdoc/no-types': 'error', 177 | 'max-classes-per-file': 'off', 178 | 'max-len': [ 179 | 'error', 180 | { 181 | code: 140, 182 | }, 183 | ], 184 | 'new-parens': 'error', 185 | 'no-bitwise': 'error', 186 | 'no-caller': 'error', 187 | 'no-cond-assign': 'error', 188 | 'no-console': [ 189 | 'error', 190 | { 191 | allow: [ 192 | 'log', 193 | 'warn', 194 | 'dir', 195 | 'timeLog', 196 | 'assert', 197 | 'clear', 198 | 'count', 199 | 'countReset', 200 | 'group', 201 | 'groupEnd', 202 | 'table', 203 | 'dirxml', 204 | 'error', 205 | 'groupCollapsed', 206 | 'Console', 207 | 'profile', 208 | 'profileEnd', 209 | 'timeStamp', 210 | 'context', 211 | ], 212 | }, 213 | ], 214 | 'no-debugger': 'error', 215 | 'no-empty': 'off', 216 | 'no-empty-function': 'off', 217 | 'no-eval': 'error', 218 | 'no-fallthrough': 'error', 219 | 'no-invalid-this': 'off', 220 | 'no-new-wrappers': 'error', 221 | 'no-restricted-imports': ['error', 'rxjs/Rx'], 222 | 'no-shadow': 'error', 223 | 'no-throw-literal': 'error', 224 | 'no-trailing-spaces': 'error', 225 | 'no-undef-init': 'error', 226 | 'no-underscore-dangle': 'error', 227 | 'no-unsafe-finally': 'error', 228 | 'no-unused-expressions': 'error', 229 | 'no-unused-labels': 'error', 230 | 'no-use-before-define': 'off', 231 | 'no-var': 'error', 232 | 'object-shorthand': 'error', 233 | 'one-var': ['error', 'never'], 234 | 'prefer-arrow/prefer-arrow-functions': 'error', 235 | 'prefer-const': 'error', 236 | 'quote-props': ['error', 'as-needed'], 237 | quotes: 'error', 238 | radix: 'error', 239 | semi: 'error', 240 | 'space-before-function-paren': [ 241 | 'error', 242 | { 243 | anonymous: 'never', 244 | asyncArrow: 'always', 245 | named: 'never', 246 | }, 247 | ], 248 | 'spaced-comment': [ 249 | 'error', 250 | 'always', 251 | { 252 | markers: ['/'], 253 | }, 254 | ], 255 | 'use-isnan': 'error', 256 | 'valid-typeof': 'off', 257 | '@typescript-eslint/tslint/config': [ 258 | 'error', 259 | { 260 | rules: { 261 | 'import-spacing': true, 262 | typedef: [true, 'call-signature'], 263 | whitespace: [true, 'check-branch', 'check-decl', 'check-operator', 'check-separator', 'check-type', 'check-typecast'], 264 | }, 265 | }, 266 | ], 267 | }, 268 | }; 269 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/src/lib/pinch-zoom.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ElementRef, 4 | EventEmitter, 5 | HostBinding, 6 | Input, 7 | OnChanges, 8 | OnDestroy, 9 | OnInit, Output, 10 | SimpleChanges 11 | } from '@angular/core'; 12 | 13 | import { Properties } from './interfaces'; 14 | import { defaultProperties, backwardCompatibilityProperties } from './properties'; 15 | import { IvyPinch } from './ivypinch'; 16 | 17 | 18 | interface ComponentProperties extends Properties { 19 | disabled?: boolean; 20 | overflow?: 'hidden' | 'visible'; 21 | disableZoomControl?: 'disable' | 'never' | 'auto'; 22 | backgroundColor?: string; 23 | } 24 | 25 | export const _defaultComponentProperties: ComponentProperties = { 26 | overflow: 'hidden', 27 | disableZoomControl: 'auto', 28 | backgroundColor: 'rgba(0,0,0,0.85)', 29 | }; 30 | 31 | @Component({ 32 | selector: 'pinch-zoom, [pinch-zoom]', 33 | exportAs: 'pinchZoom', 34 | templateUrl: './pinch-zoom.component.html', 35 | styleUrls: ['./pinch-zoom.component.sass'], 36 | standalone: true, 37 | imports: [] 38 | }) 39 | export class PinchZoomComponent implements OnInit, OnDestroy, OnChanges { 40 | private pinchZoom: IvyPinch; 41 | private _properties!: ComponentProperties; 42 | private readonly defaultComponentProperties!: ComponentProperties; 43 | private _transitionDuration!: number; 44 | private _doubleTap!: boolean; 45 | private _doubleTapScale!: number; 46 | private _autoZoomOut!: boolean; 47 | private _limitZoom!: number | 'original image size'; 48 | 49 | @Input('properties') set properties(value: ComponentProperties) { 50 | if (value) { 51 | this._properties = value; 52 | } 53 | } 54 | 55 | get properties(): ComponentProperties { 56 | return this._properties; 57 | } 58 | 59 | // transitionDuration 60 | @Input('transition-duration') set transitionDurationBackwardCompatibility(value: number) { 61 | if (value) { 62 | this._transitionDuration = value; 63 | } 64 | } 65 | 66 | @Input('transitionDuration') set transitionDuration(value: number) { 67 | if (value) { 68 | this._transitionDuration = value; 69 | } 70 | } 71 | 72 | get transitionDuration(): number { 73 | return this._transitionDuration; 74 | } 75 | 76 | // doubleTap 77 | @Input('double-tap') set doubleTapBackwardCompatibility(value: boolean) { 78 | if (value) { 79 | this._doubleTap = value; 80 | } 81 | } 82 | 83 | @Input('doubleTap') set doubleTap(value: boolean) { 84 | if (value) { 85 | this._doubleTap = value; 86 | } 87 | } 88 | 89 | get doubleTap(): boolean { 90 | return this._doubleTap; 91 | } 92 | 93 | // doubleTapScale 94 | @Input('double-tap-scale') set doubleTapScaleBackwardCompatibility(value: number) { 95 | if (value) { 96 | this._doubleTapScale = value; 97 | } 98 | } 99 | 100 | @Input('doubleTapScale') set doubleTapScale(value: number) { 101 | if (value) { 102 | this._doubleTapScale = value; 103 | } 104 | } 105 | 106 | get doubleTapScale(): number { 107 | return this._doubleTapScale; 108 | } 109 | 110 | // autoZoomOut 111 | @Input('auto-zoom-out') set autoZoomOutBackwardCompatibility(value: boolean) { 112 | if (value) { 113 | this._autoZoomOut = value; 114 | } 115 | } 116 | 117 | @Input('autoZoomOut') set autoZoomOut(value: boolean) { 118 | if (value) { 119 | this._autoZoomOut = value; 120 | } 121 | } 122 | 123 | get autoZoomOut(): boolean { 124 | return this._autoZoomOut; 125 | } 126 | 127 | // limitZoom 128 | @Input('limit-zoom') set limitZoomBackwardCompatibility(value: number | 'original image size') { 129 | if (value) { 130 | this._limitZoom = value; 131 | } 132 | } 133 | 134 | @Input('limitZoom') set limitZoom(value: number | 'original image size') { 135 | if (value) { 136 | this._limitZoom = value; 137 | } 138 | } 139 | 140 | get limitZoom(): number | 'original image size' { 141 | return this._limitZoom; 142 | } 143 | 144 | @Input() disabled!: boolean; 145 | @Input() disablePan!: boolean; 146 | @Input() overflow!: 'hidden' | 'visible'; 147 | @Input() zoomControlScale!: number; 148 | @Input() disableZoomControl!: 'disable' | 'never' | 'auto'; 149 | @Input() backgroundColor!: string; 150 | @Input() limitPan!: boolean; 151 | @Input() minPanScale!: number; 152 | @Input() minScale!: number; 153 | @Input() listeners!: 'auto' | 'mouse and touch'; 154 | @Input() wheel!: boolean; 155 | @Input() autoHeight!: boolean; 156 | @Input() wheelZoomFactor!: number; 157 | @Input() draggableImage!: boolean; 158 | @Input() draggableOnPinch!: boolean; 159 | @Output() public zoomChanged: EventEmitter = new EventEmitter(); 160 | 161 | @HostBinding('style.overflow') 162 | get hostOverflow(): 'hidden' | 'visible' { 163 | return this.properties['overflow']; 164 | } 165 | 166 | @HostBinding('style.background-color') 167 | get hostBackgroundColor(): string { 168 | return this.properties['backgroundColor']; 169 | } 170 | 171 | get isTouchScreen(): boolean { 172 | const prefixes = ' -webkit- -moz- -o- -ms- '.split(' '); 173 | const mq = (query: string): boolean => { 174 | return window.matchMedia(query).matches; 175 | }; 176 | 177 | if ('ontouchstart' in window) { 178 | return true; 179 | } 180 | 181 | // include the 'heartz' as a way to have a non matching MQ to help terminate the join 182 | // https://git.io/vznFH 183 | const query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join(''); 184 | return mq(query); 185 | } 186 | 187 | get isDragging(): boolean { 188 | return this.pinchZoom?.isDragging(); 189 | } 190 | 191 | get isDisabled(): boolean { 192 | return this._properties.disabled; 193 | } 194 | 195 | get scale(): number { 196 | return this.pinchZoom.scale; 197 | } 198 | 199 | get isZoomedIn(): boolean { 200 | return this.scale > 1; 201 | } 202 | 203 | get scaleLevel(): number { 204 | return Math.round(this.scale / this._zoomControlScale); 205 | } 206 | 207 | get maxScale(): number { 208 | return this.pinchZoom.maxScale; 209 | } 210 | 211 | get isZoomLimitReached(): boolean { 212 | return this.scale >= this.maxScale; 213 | } 214 | 215 | get _zoomControlScale(): number { 216 | return this.getPropertiesValue('zoomControlScale'); 217 | } 218 | 219 | constructor(private elementRef: ElementRef) { 220 | this.defaultComponentProperties = this.getDefaultComponentProperties(); 221 | this.applyPropertiesDefault(this.defaultComponentProperties, {}); 222 | } 223 | 224 | ngOnInit(): void { 225 | this.initPinchZoom(); 226 | 227 | /* Calls the method until the image size is available */ 228 | this.detectLimitZoom(); 229 | } 230 | 231 | ngOnChanges(changes: SimpleChanges): void { 232 | let changedProperties = this.getProperties(changes); 233 | changedProperties = this.renameProperties(changedProperties); 234 | 235 | this.applyPropertiesDefault(this.defaultComponentProperties, changedProperties); 236 | } 237 | 238 | ngOnDestroy(): void { 239 | this.destroy(); 240 | } 241 | 242 | private initPinchZoom(): void { 243 | if (this._properties.disabled) { 244 | return; 245 | } 246 | 247 | this._properties.limitZoom = this.limitZoom; 248 | this._properties.element = this.elementRef.nativeElement.querySelector('.pinch-zoom-content'); 249 | this.pinchZoom = new IvyPinch(this.properties, this.zoomChanged); 250 | } 251 | 252 | private getProperties( 253 | changes: SimpleChanges, 254 | ): ComponentProperties | Record { 255 | let properties: ComponentProperties = {}; 256 | 257 | for (const prop in changes) { 258 | if (prop !== 'properties') { 259 | properties[prop] = changes[prop].currentValue; 260 | } 261 | if (prop === 'properties') { 262 | properties = changes[prop].currentValue; 263 | } 264 | } 265 | return properties; 266 | } 267 | 268 | private renameProperties( 269 | properties: ComponentProperties | Record, 270 | ): ComponentProperties { 271 | for (const prop in properties) { 272 | if (backwardCompatibilityProperties[prop]) { 273 | properties[backwardCompatibilityProperties[prop]] = properties[prop]; 274 | delete properties[prop]; 275 | } 276 | } 277 | 278 | return properties as ComponentProperties; 279 | } 280 | 281 | private applyPropertiesDefault(defaultProperties: ComponentProperties, properties: ComponentProperties): void { 282 | this.properties = Object.assign({}, defaultProperties, properties); 283 | } 284 | 285 | toggleZoom(): void { 286 | this.pinchZoom?.toggleZoom(); 287 | } 288 | 289 | zoomIn(value: number): number { 290 | return this.pinchZoom?.zoomIn(value); 291 | } 292 | 293 | zoomOut(value: number): number { 294 | return this.pinchZoom?.zoomOut(value); 295 | } 296 | 297 | isControl(): boolean { 298 | if (this.isDisabled) { 299 | return false; 300 | } 301 | 302 | if (this._properties.disableZoomControl === 'disable') { 303 | return false; 304 | } 305 | 306 | if (this.isTouchScreen && this._properties.disableZoomControl === 'auto') { 307 | return false; 308 | } 309 | 310 | return true; 311 | } 312 | 313 | detectLimitZoom(): void { 314 | this.pinchZoom?.detectLimitZoom(); 315 | } 316 | 317 | destroy(): void { 318 | this.pinchZoom?.destroy(); 319 | } 320 | 321 | private getPropertiesValue(propertyName: K): ComponentProperties[K] { 322 | if (this.properties && this.properties[propertyName]) { 323 | return this.properties[propertyName]; 324 | } else { 325 | return this.defaultComponentProperties[propertyName]; 326 | } 327 | } 328 | 329 | private getDefaultComponentProperties(): ComponentProperties { 330 | return { ...defaultProperties, ..._defaultComponentProperties }; 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Properties

3 | 4 | 5 |
6 |

transition-duration: 1000

7 |

Defines the speed of the animation of positioning and transforming.

8 |

Zoom State: {{this.zoomstate}}

9 | 10 | 13 | 14 |
15 | 16 | 17 |
18 |

limit-zoom: 2

19 |

Limit the maximum available scale. By default, the maximum scale is calculated based on the original image size.

20 |

Zoom State: {{this.zoomstate}}

21 | 22 | 25 | 26 |
27 | 28 | 29 |
30 |

minScale: 1

31 |

Limit the minimum acceptable scale. With a value of 1, it is recommended to use this parameter with limitPan.

32 |

Zoom State: {{this.zoomstate}}

33 | 34 | 37 | 38 |
39 | 40 | 41 |
42 |

auto-zoom-out: true

43 |

Automatic restoration of the original size of an image after its zooming in by two fingers.

44 |

Zoom State: {{this.zoomstate}}

45 | 46 | 49 | 50 |
51 | 52 | 53 |
54 |

double-tap: false

55 |

Zooming in and zooming out of an image, depending on its current condition, with double tap.

56 |

Zoom State: {{this.zoomstate}}

57 | 58 | 61 | 62 |
63 | 64 | 65 |
66 |

double-tap-scale: 4

67 |

Double tap scaling factor.

68 |

Zoom State: {{this.zoomstate}}

69 | 70 | 73 | 74 |
75 | 76 | 77 |
78 |

disabled: true

79 |

Zoom State: {{this.zoomstate}}

80 | 81 | 84 | 85 |
86 | 87 | 88 |
89 |

disablePan: true

90 |

Turn off panning with one finger.

91 |

Zoom State: {{this.zoomstate}}

92 | 93 | 96 | 97 |
98 | 99 | 100 |
101 |

minPanScale: 2

102 |

Minimum zoom at which panning is enabled.

103 |

Zoom State: {{this.zoomstate}}

104 | 105 | 108 | 109 |
110 | 111 | 112 |
113 |

overflow: 'visible'

114 |

115 | hidden - the overflow is clipped, and the rest of the content will be invisible. visible - the overflow is not clipped. The 116 | content renders outside the element's box. 117 |

118 |

Zoom State: {{this.zoomstate}}

119 | 120 | 123 | 124 |
125 | 126 | 127 |
128 |

disableZoomControl: 'disable'

129 |

130 | Disable zoom controls. auto - Disable zoom controls on touch screen devices. never - show zoom controls on all devices. disable 131 | - disable zoom controls on all devices. 132 |

133 |

Zoom State: {{this.zoomstate}}

134 | 135 | 138 | 139 |
140 | 141 | 142 |
143 |

zoomControlScale: 2

144 |

Zoom factor when using zoom controls.

145 |

Zoom State: {{this.zoomstate}}

146 | 147 | 150 | 151 |
152 | 153 | 154 |
155 |

backgroundColor: 'rgba(0,0,0,0.65)'

156 |

The background color of the container.

157 |

Zoom State: {{this.zoomstate}}

158 | 159 | 162 | 163 |
164 | 165 | 166 |
167 |

limitPan: true

168 |

Stop panning when the edge of the image reaches the edge of the screen.

169 |

Zoom State: {{this.zoomstate}}

170 | 171 | 174 | 175 |
176 | 177 | 178 |
179 |

listeners: 'auto'

180 |

181 | By default, subscriptions are made for mouse and touch screen events. The value auto means that the subscription will be only 182 | for touch events or only for mouse events, depending on the type of screen. 183 |

184 |

Zoom State: {{this.zoomstate}}

185 | 186 | 189 | 190 |
191 | 192 | 193 |
194 |

wheel: false

195 |

Scale with the mouse wheel.

196 |

Zoom State: {{this.zoomstate}}

197 | 198 | 201 | 202 |
203 | 204 | 205 |
206 |

wheelZoomFactor: 0.5

207 |

Zoom factor when zoomed in with the mouse wheel.

208 |

Zoom State: {{this.zoomstate}}

209 | 210 | 213 | 214 |
215 | 216 | 217 |
218 |

autoHeight: true

219 |

220 | Calculate the height of the container based on the width and height attributes of the image. By default, the width of the 221 | container is 100%, and the height is determined after the image information is loaded - this may cause a delay in determining 222 | the height of the container. If you want the container to initially have dimensions corresponding to the dimensions of the 223 | image, then specify the attributes width and height for the tag. When setting the property value to `true`, a 224 | subscription to the window resize listener will be created. 225 |

226 |

Zoom State: {{this.zoomstate}}

227 | 228 | 233 | 234 |
235 | 236 | 237 |
238 |

draggableImage: true

239 |

Sets the attribute draggable to the img tag.

240 |

Zoom State: {{this.zoomstate}}

241 | 242 | 245 | 246 |
247 | 248 | 249 |
250 |

draggableOnPinch: true

251 |

Zoom State: {{this.zoomstate}}

252 | 253 | 256 | 257 |
258 | 259 | 260 |

Methods

261 |
262 | 263 | 266 | 267 |
268 | 269 | 270 |
271 |
272 |
273 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Note 2 | 3 | This Project has been forked to be updated to work with Angular versions 19 and 20. 4 | 5 | # Pinch zoom for Angular 6 | 7 | The module provides opportunities for image zooming in, zooming out and positioning with use of gestures on a touch screen. 8 | 9 | ## Installation 10 | 11 | Install the npm package. 12 | 13 | ``` 14 | npm i @meddv/ngx-pinch-zoom 15 | ``` 16 | 17 | Import module: 18 | 19 | ```ts 20 | import { PinchZoomComponent } from '@meddv/ngx-pinch-zoom'; 21 | 22 | @NgModule({ 23 | imports: [ PinchZoomComponent ] 24 | }) 25 | ``` 26 | 27 | ## Usage 28 | 29 | For use, put your image inside the <pinch-zoom> container. Please, pay attention to the parameters of your viewport metatag. If you use Pinch Zoom, it is required to limit zooming of a web-page, by entering the following parameters: <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">. 30 | 31 | ```html 32 | 33 | 34 | 35 | ``` 36 | 37 | ## Properties 38 | 39 | | name | type | default | description | 40 | | ------------------- | ----------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 41 | | transition-duration | number | 200 | Defines the speed of the animation of positioning and transforming. | 42 | | limit-zoom | number, "original image size" | "original image size" | Limit the maximum available scale. By default, the maximum scale is calculated based on the original image size. | 43 | | minScale | number | 0 | Limit the minimum acceptable scale. With a value of 1, it is recommended to use this parameter with `limitPan` | 44 | | auto-zoom-out | boolean | false | Automatic restoration of the original size of an image after its zooming in by two fingers. | 45 | | double-tap | boolean | true | Zooming in and zooming out of an image, depending on its current condition, with double tap. | 46 | | disabled | boolean | false | Disable zoom. | 47 | | disablePan | boolean | false | Turn off panning with one finger. | 48 | | overflow | "hidden", "visible" | "hidden" | `hidden` - the overflow is clipped, and the rest of the content will be invisible. `visible` - the overflow is not clipped. The content renders outside the element's box. | 49 | | disableZoomControl | "disable", "never", "auto" | "auto" | Disable zoom controls. `auto` - Disable zoom controls on touch screen devices. `never` - show zoom controls on all devices. `disable` - disable zoom controls on all devices. | 50 | | zoomControlScale | number | 1 | Zoom factor when using zoom controls. | 51 | | backgroundColor | string | "rgba(0,0,0,0.85)" | The background color of the container. | 52 | | limitPan | boolean | false | Stop panning when the edge of the image reaches the edge of the screen. | 53 | | minPanScale | number | 1.0001 | Minimum zoom at which panning is enabled. | 54 | | listeners | "auto", "mouse and touch" | "mouse and touch" | By default, subscriptions are made for mouse and touch screen events. The value `auto` means that the subscription will be only for touch events or only for mouse events, depending on the type of screen. | 55 | | wheel | boolean | true | Scale with the mouse wheel. | 56 | | wheelZoomFactor | number | 0.2 | Zoom factor when zoomed in with the mouse wheel. | 57 | | autoHeight | boolean | false | Calculate the height of the container based on the `width` and `height` attributes of the image. By default, the width of the container is 100%, and the height is determined after the image information is loaded - this may cause a delay in determining the height of the container. If you want the container to initially have dimensions corresponding to the dimensions of the image, then specify the attributes `width` and `height` for the `` tag. When setting the property value to `true`, a subscription to the window resize listener will be created. | 58 | | draggableImage | boolean | false | Sets the attribute `draggable` to the `` tag. | 59 | | draggableOnPinch | boolean | false | When set to `true` content can be moved around while touching or pinching with two fingers. | 60 | 61 | ## Outputs 62 | 63 | | name | description | 64 | | ----------- | ---------------------------------------------- | 65 | | zoomChanged | Emits current `scale: number` if it's changed. | 66 | 67 | ## Methods 68 | 69 | | name | description | 70 | | ---------------------- | -------------------------------------------------------------------------------------------- | 71 | | toggleZoom() | Image zooming in and out, depending on its current state. | 72 | | zoomIn(value: number) | Zoom in by `value`, respects `limit-zoom` option. Returns `scale: number`. | 73 | | zoomOut(value: number) | Zoom out by `value`, respects `minScale` option. Returns `scale: number`. | 74 | | destroy() | Unsubscribe from mouse events and touches, as well as remove added styles from the DOM tree. | 75 | 76 | ## Contributor services 77 | 78 | Contact us over our Issue Tracker. 79 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/README.md: -------------------------------------------------------------------------------- 1 | # Note 2 | 3 | This Project has been forked to be updated to work with Angular versions 19 and 20. 4 | 5 | # Pinch zoom for Angular 6 | 7 | The module provides opportunities for image zooming in, zooming out and positioning with use of gestures on a touch screen. 8 | 9 | ## Installation 10 | 11 | Install the npm package. 12 | 13 | ``` 14 | npm i @meddv/ngx-pinch-zoom 15 | ``` 16 | 17 | Import module: 18 | 19 | ```ts 20 | import { PinchZoomComponent } from '@meddv/ngx-pinch-zoom'; 21 | 22 | @NgModule({ 23 | imports: [ PinchZoomComponent ] 24 | }) 25 | ``` 26 | 27 | ## Usage 28 | 29 | For use, put your image inside the <pinch-zoom> container. Please, pay attention to the parameters of your viewport metatag. If you use Pinch Zoom, it is required to limit zooming of a web-page, by entering the following parameters: <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">. 30 | 31 | ```html 32 | 33 | 34 | 35 | ``` 36 | 37 | ## Properties 38 | 39 | | name | type | default | description | 40 | | ------------------- | ----------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 41 | | transition-duration | number | 200 | Defines the speed of the animation of positioning and transforming. | 42 | | limit-zoom | number, "original image size" | "original image size" | Limit the maximum available scale. By default, the maximum scale is calculated based on the original image size. | 43 | | minScale | number | 0 | Limit the minimum acceptable scale. With a value of 1, it is recommended to use this parameter with `limitPan` | 44 | | auto-zoom-out | boolean | false | Automatic restoration of the original size of an image after its zooming in by two fingers. | 45 | | double-tap | boolean | true | Zooming in and zooming out of an image, depending on its current condition, with double tap. | 46 | | disabled | boolean | false | Disable zoom. | 47 | | disablePan | boolean | false | Turn off panning with one finger. | 48 | | overflow | "hidden", "visible" | "hidden" | `hidden` - the overflow is clipped, and the rest of the content will be invisible. `visible` - the overflow is not clipped. The content renders outside the element's box. | 49 | | disableZoomControl | "disable", "never", "auto" | "auto" | Disable zoom controls. `auto` - Disable zoom controls on touch screen devices. `never` - show zoom controls on all devices. `disable` - disable zoom controls on all devices. | 50 | | zoomControlScale | number | 1 | Zoom factor when using zoom controls. | 51 | | backgroundColor | string | "rgba(0,0,0,0.85)" | The background color of the container. | 52 | | limitPan | boolean | false | Stop panning when the edge of the image reaches the edge of the screen. | 53 | | minPanScale | number | 1.0001 | Minimum zoom at which panning is enabled. | 54 | | listeners | "auto", "mouse and touch" | "mouse and touch" | By default, subscriptions are made for mouse and touch screen events. The value `auto` means that the subscription will be only for touch events or only for mouse events, depending on the type of screen. | 55 | | wheel | boolean | true | Scale with the mouse wheel. | 56 | | wheelZoomFactor | number | 0.2 | Zoom factor when zoomed in with the mouse wheel. | 57 | | autoHeight | boolean | false | Calculate the height of the container based on the `width` and `height` attributes of the image. By default, the width of the container is 100%, and the height is determined after the image information is loaded - this may cause a delay in determining the height of the container. If you want the container to initially have dimensions corresponding to the dimensions of the image, then specify the attributes `width` and `height` for the `` tag. When setting the property value to `true`, a subscription to the window resize listener will be created. | 58 | | draggableImage | boolean | false | Sets the attribute `draggable` to the `` tag. | 59 | | draggableOnPinch | boolean | false | When set to `true` content can be moved around while touching or pinching with two fingers. | 60 | 61 | ## Outputs 62 | 63 | | name | description | 64 | | ----------- | ---------------------------------------------- | 65 | | zoomChanged | Emits current `scale: number` if it's changed. | 66 | 67 | ## Methods 68 | 69 | | name | description | 70 | | ---------------------- | -------------------------------------------------------------------------------------------- | 71 | | toggleZoom() | Image zooming in and out, depending on its current state. | 72 | | zoomIn(value: number) | Zoom in by `value`, respects `limit-zoom` option. Returns `scale: number`. | 73 | | zoomOut(value: number) | Zoom out by `value`, respects `minScale` option. Returns `scale: number`. | 74 | | destroy() | Unsubscribe from mouse events and touches, as well as remove added styles from the DOM tree. | 75 | 76 | ## Contributor services 77 | 78 | Contact us over our Issue Tracker. 79 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/src/lib/touches.ts: -------------------------------------------------------------------------------- 1 | export interface Properties { 2 | element: HTMLElement; 3 | listeners?: 'auto' | 'mouse and touch'; 4 | touchListeners?: TouchListeners; 5 | mouseListeners?: MouseListeners; 6 | otherListeners?: OtherListeners; 7 | resize?: boolean; 8 | } 9 | 10 | export type EventType = 11 | | undefined 12 | | 'touchstart' 13 | | 'touchend' 14 | | 'touchmove' 15 | | 'mousedown' 16 | | 'mouseup' 17 | | 'mousemove' 18 | | 'pan' 19 | | 'pinch' 20 | | 'horizontal-swipe' 21 | | 'vertical-swipe' 22 | | 'tap' 23 | | 'longtap' 24 | | 'wheel' 25 | | 'double-tap' 26 | | 'resize'; 27 | export type TouchHandler ='handleTouchstart' | 'handleTouchmove' | 'handleTouchend'; 28 | export type MouseHandler = 'handleMousedown' | 'handleMousemove' | 'handleMouseup' | 'handleWheel'; 29 | export type OtherHandler = 'handleResize'; 30 | 31 | export type TouchListeners = Partial>; 32 | export type MouseListeners = Partial>; 33 | export type OtherListeners = Partial>; 34 | 35 | export class Touches { 36 | private properties: Properties; 37 | private element: HTMLElement; 38 | private elementPosition: DOMRect; 39 | private eventType: EventType = undefined; 40 | private handlers: TouchListeners | MouseListeners | OtherListeners = {}; 41 | private startX = 0; 42 | private startY = 0; 43 | private lastTap = 0; 44 | private doubleTapTimeout: number; 45 | private doubleTapMinTimeout = 300; 46 | private tapMinTimeout = 200; 47 | private touchstartTime = 0; 48 | private i: number = 0; 49 | private isMousedown = false; 50 | 51 | private _touchListeners: Record<'touchstart' | 'touchmove' | 'touchend', TouchHandler> = { 52 | touchstart: 'handleTouchstart', 53 | touchmove: 'handleTouchmove', 54 | touchend: 'handleTouchend', 55 | }; 56 | private _mouseListeners: Record<'mousedown' | 'mousemove' | 'mouseup' | 'wheel', MouseHandler> = { 57 | mousedown: 'handleMousedown', 58 | mousemove: 'handleMousemove', 59 | mouseup: 'handleMouseup', 60 | wheel: 'handleWheel', 61 | }; 62 | private _otherListeners: Record<'resize', OtherHandler> = { 63 | resize: 'handleResize', 64 | }; 65 | 66 | private get touchListeners(): TouchListeners { 67 | return this.properties.touchListeners ? this.properties.touchListeners : this._touchListeners; 68 | } 69 | 70 | private get mouseListeners(): MouseListeners { 71 | return this.properties.mouseListeners ? this.properties.mouseListeners : this._mouseListeners; 72 | } 73 | 74 | private get otherListeners(): OtherListeners { 75 | return this.properties.otherListeners ? this.properties.otherListeners : this._otherListeners; 76 | } 77 | 78 | constructor(properties: Properties) { 79 | this.properties = properties; 80 | this.element = this.properties.element; 81 | this.elementPosition = this.getElementPosition(); 82 | 83 | this.toggleEventListeners('addEventListener'); 84 | } 85 | 86 | public destroy(): void { 87 | this.toggleEventListeners('removeEventListener'); 88 | } 89 | 90 | private toggleEventListeners(action: 'addEventListener' | 'removeEventListener'): void { 91 | let listeners: TouchListeners | MouseListeners | OtherListeners; 92 | 93 | if (this.properties.listeners === 'mouse and touch') { 94 | listeners = Object.assign(this.touchListeners, this.mouseListeners); 95 | } else { 96 | listeners = this.detectTouchScreen() ? this.touchListeners : this.mouseListeners; 97 | } 98 | 99 | if (this.properties.resize) { 100 | listeners = Object.assign(listeners, this.otherListeners); 101 | } 102 | 103 | for (const listener in listeners) { 104 | const handler = listeners[listener]; 105 | 106 | // Window 107 | if (listener === 'resize') { 108 | if (action === 'addEventListener') { 109 | window.addEventListener(listener, this[handler], false); 110 | } 111 | if (action === 'removeEventListener') { 112 | window.removeEventListener(listener, this[handler], false); 113 | } 114 | // Document 115 | } else if (listener === 'mouseup' || listener === 'mousemove') { 116 | if (action === 'addEventListener') { 117 | document.addEventListener(listener, this[handler], false); 118 | } 119 | if (action === 'removeEventListener') { 120 | document.removeEventListener(listener, this[handler], false); 121 | } 122 | // Element 123 | } else { 124 | if (action === 'addEventListener') { 125 | this.element.addEventListener(listener, this[handler], false); 126 | } 127 | if (action === 'removeEventListener') { 128 | this.element.removeEventListener(listener, this[handler], false); 129 | } 130 | } 131 | } 132 | } 133 | 134 | public addEventListeners(listener: string): void { 135 | const handler: MouseHandler = this._mouseListeners[listener]; 136 | window.addEventListener(listener, this[handler], false); 137 | } 138 | 139 | public removeEventListeners(listener: string): void { 140 | const handler: MouseHandler = this._mouseListeners[listener]; 141 | window.removeEventListener(listener, this[handler], false); 142 | } 143 | 144 | /* 145 | * Listeners 146 | */ 147 | 148 | /* Touchstart */ 149 | 150 | private handleTouchstart = (event: TouchEvent): void => { 151 | this.elementPosition = this.getElementPosition(); 152 | this.touchstartTime = new Date().getTime(); 153 | 154 | if (this.eventType === undefined) { 155 | this.getTouchstartPosition(event); 156 | } 157 | 158 | this.runHandler('touchstart', event); 159 | }; 160 | 161 | /* Touchmove */ 162 | 163 | private handleTouchmove = (event: TouchEvent): void => { 164 | const touches = event.touches; 165 | 166 | // Pan 167 | if (this.detectPan(touches)) { 168 | this.runHandler('pan', event); 169 | } 170 | 171 | // Pinch 172 | if (this.detectPinch(event)) { 173 | this.runHandler('pinch', event); 174 | } 175 | }; 176 | 177 | private handleLinearSwipe(event: any): void { 178 | //event.preventDefault(); 179 | 180 | this.i++; 181 | 182 | if (this.i > 3) { 183 | this.eventType = this.getLinearSwipeType(event); 184 | } 185 | 186 | if (this.eventType === 'horizontal-swipe') { 187 | this.runHandler('horizontal-swipe', event); 188 | } 189 | 190 | if (this.eventType === 'vertical-swipe') { 191 | this.runHandler('vertical-swipe', event); 192 | } 193 | } 194 | 195 | /* Touchend */ 196 | 197 | private handleTouchend = (event: TouchEvent): void => { 198 | const touches = event.touches; 199 | 200 | // Double Tap 201 | if (this.detectDoubleTap()) { 202 | this.runHandler('double-tap', event); 203 | } 204 | 205 | // Tap 206 | this.detectTap(); 207 | 208 | this.runHandler('touchend', event); 209 | this.eventType = 'touchend'; 210 | 211 | if (touches && touches.length === 0) { 212 | this.eventType = undefined; 213 | this.i = 0; 214 | } 215 | }; 216 | 217 | /* Mousedown */ 218 | 219 | private handleMousedown = (event: MouseEvent): void => { 220 | this.isMousedown = true; 221 | this.elementPosition = this.getElementPosition(); 222 | this.touchstartTime = new Date().getTime(); 223 | 224 | if (this.eventType === undefined) { 225 | this.getMousedownPosition(event); 226 | } 227 | 228 | this.runHandler('mousedown', event); 229 | }; 230 | 231 | /* Mousemove */ 232 | 233 | private handleMousemove = (event: MouseEvent): void => { 234 | //event.preventDefault(); 235 | 236 | if (!this.isMousedown) { 237 | return; 238 | } 239 | 240 | // Pan 241 | this.runHandler('pan', event); 242 | 243 | // Linear swipe 244 | switch (this.detectLinearSwipe(event)) { 245 | case 'horizontal-swipe': 246 | // FIXME: looks like an error 247 | // @ts-ignore 248 | event.swipeType = 'horizontal-swipe'; 249 | this.runHandler('horizontal-swipe', event); 250 | break; 251 | case 'vertical-swipe': 252 | // FIXME: looks like an error 253 | // @ts-ignore 254 | event.swipeType = 'vertical-swipe'; 255 | this.runHandler('vertical-swipe', event); 256 | break; 257 | } 258 | 259 | // Linear swipe 260 | if ( 261 | this.detectLinearSwipe(event) || 262 | this.eventType === 'horizontal-swipe' || 263 | this.eventType === 'vertical-swipe' 264 | ) { 265 | this.handleLinearSwipe(event); 266 | } 267 | }; 268 | 269 | /* Mouseup */ 270 | 271 | private handleMouseup = (event: MouseEvent): void => { 272 | // Tap 273 | this.detectTap(); 274 | 275 | this.isMousedown = false; 276 | this.runHandler('mouseup', event); 277 | this.eventType = undefined; 278 | this.i = 0; 279 | }; 280 | 281 | /* Wheel */ 282 | 283 | private handleWheel = (event: WheelEvent): void => { 284 | this.runHandler('wheel', event); 285 | }; 286 | 287 | /* Resize */ 288 | 289 | private handleResize = (event: Event): void => { 290 | this.runHandler('resize', event); 291 | }; 292 | 293 | private runHandler(eventName: EventType, event: unknown):void { 294 | if (this.handlers[eventName]) { 295 | this.handlers[eventName](event); 296 | } 297 | } 298 | 299 | /* 300 | * Detection 301 | */ 302 | 303 | private detectPan(touches: TouchList): boolean { 304 | return (touches.length === 1 && !this.eventType) || this.eventType === 'pan'; 305 | } 306 | 307 | private detectDoubleTap(): boolean { 308 | if (this.eventType != undefined) { 309 | return; 310 | } 311 | 312 | const currentTime = new Date().getTime(); 313 | const tapLength = currentTime - this.lastTap; 314 | 315 | window.clearTimeout(this.doubleTapTimeout); 316 | 317 | if (tapLength < this.doubleTapMinTimeout && tapLength > 0) { 318 | return true; 319 | } else { 320 | this.doubleTapTimeout = window.setTimeout(() => { 321 | window.clearTimeout(this.doubleTapTimeout); 322 | }, this.doubleTapMinTimeout); 323 | } 324 | this.lastTap = currentTime; 325 | 326 | return undefined; 327 | } 328 | 329 | private detectTap(): void { 330 | if (this.eventType != undefined) { 331 | return; 332 | } 333 | 334 | const currentTime = new Date().getTime(); 335 | const tapLength = currentTime - this.touchstartTime; 336 | 337 | if (tapLength > 0) { 338 | if (tapLength < this.tapMinTimeout) { 339 | this.runHandler('tap', {}); 340 | } else { 341 | this.runHandler('longtap', {}); 342 | } 343 | } 344 | } 345 | 346 | private detectPinch(event: TouchEvent): boolean { 347 | const touches = event.touches; 348 | return (touches.length === 2 && this.eventType === undefined) || this.eventType === 'pinch'; 349 | } 350 | 351 | private detectLinearSwipe(event: MouseEvent | TouchEvent): 'vertical-swipe' | 'horizontal-swipe' { 352 | const touches = (event as TouchEvent).touches; 353 | 354 | if (touches) { 355 | if ( 356 | (touches.length === 1 && !this.eventType) || 357 | this.eventType === 'horizontal-swipe' || 358 | this.eventType === 'vertical-swipe' 359 | ) { 360 | return this.getLinearSwipeType(event); 361 | } 362 | } else { 363 | if (!this.eventType || this.eventType === 'horizontal-swipe' || this.eventType === 'vertical-swipe') { 364 | return this.getLinearSwipeType(event); 365 | } 366 | } 367 | 368 | return undefined; 369 | } 370 | 371 | private getLinearSwipeType(event: TouchEvent | MouseEvent): 'vertical-swipe' | 'horizontal-swipe' { 372 | if (this.eventType !== 'horizontal-swipe' && this.eventType !== 'vertical-swipe') { 373 | const movementX = Math.abs(this.moveLeft(0, event) - this.startX); 374 | const movementY = Math.abs(this.moveTop(0, event) - this.startY); 375 | 376 | if (movementY * 3 > movementX) { 377 | return 'vertical-swipe'; 378 | } else { 379 | return 'horizontal-swipe'; 380 | } 381 | } else { 382 | return this.eventType; 383 | } 384 | } 385 | 386 | private getElementPosition(): DOMRect { 387 | return this.element.getBoundingClientRect(); 388 | } 389 | 390 | private getTouchstartPosition(event: TouchEvent): void { 391 | this.startX = event.touches[0].clientX - this.elementPosition.left; 392 | this.startY = event.touches[0].clientY - this.elementPosition.top; 393 | } 394 | 395 | private getMousedownPosition(event: MouseEvent): void { 396 | this.startX = event.clientX - this.elementPosition.left; 397 | this.startY = event.clientY - this.elementPosition.top; 398 | } 399 | 400 | private moveLeft(index: number, event: TouchEvent | MouseEvent): number { 401 | const touches = (event as TouchEvent).touches; 402 | 403 | if (touches) { 404 | return touches[index].clientX - this.elementPosition.left; 405 | } else { 406 | return (event as MouseEvent).clientX - this.elementPosition.left; 407 | } 408 | } 409 | 410 | private moveTop(index: number, event: TouchEvent | MouseEvent): number { 411 | const touches = (event as TouchEvent).touches; 412 | 413 | if (touches) { 414 | return touches[index].clientY - this.elementPosition.top; 415 | } else { 416 | return (event as MouseEvent).clientY - this.elementPosition.top; 417 | } 418 | } 419 | 420 | private detectTouchScreen(): boolean { 421 | const prefixes = ' -webkit- -moz- -o- -ms- '.split(' '); 422 | const mq = (query: string): boolean => { 423 | return window.matchMedia(query).matches; 424 | }; 425 | 426 | if ('ontouchstart' in window) { 427 | return true; 428 | } 429 | 430 | // include the 'heartz' as a way to have a non matching MQ to help terminate the join 431 | // https://git.io/vznFH 432 | const query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join(''); 433 | return mq(query); 434 | } 435 | 436 | /* Public properties and methods */ 437 | public on(event: EventType, handler: (event: Event) => void): void { 438 | if (event) { 439 | this.handlers[event] = handler; 440 | } 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /projects/ngx-pinch-zoom/src/lib/ivypinch.ts: -------------------------------------------------------------------------------- 1 | import { EventType, Touches } from './touches'; 2 | import { Properties } from './interfaces'; 3 | import { defaultProperties } from './properties'; 4 | import {EventEmitter} from "@angular/core"; 5 | 6 | export class IvyPinch { 7 | private readonly properties: Properties = defaultProperties; 8 | private touches: Touches; 9 | private readonly element: HTMLElement; 10 | private readonly elementTarget: string; 11 | private parentElement: HTMLElement; 12 | public scale: number = 1; 13 | private initialScale: number = 1; 14 | private elementPosition: DOMRect; 15 | private eventType: EventType; 16 | private startX: number = 0; 17 | private startY: number = 0; 18 | private moveX: number = 0; 19 | private moveY: number = 0; 20 | private initialMoveX: number = 0; 21 | private initialMoveY: number = 0; 22 | private moveXC: number = 0; 23 | private moveYC: number = 0; 24 | private distance: number = 0; 25 | private initialDistance: number = 0; 26 | public maxScale!: number; 27 | private defaultMaxScale: number = 3; 28 | private initialPinchCenterX = 0; 29 | private initialPinchCenterY = 0; 30 | private zoomChanged: EventEmitter; 31 | 32 | // Minimum scale at which panning works 33 | get minPanScale(): number { 34 | return this.getPropertiesValue('minPanScale'); 35 | } 36 | 37 | get fullImage(): { path: string; minScale?: number } { 38 | return this.properties.fullImage; 39 | } 40 | 41 | constructor(properties: Properties, zoomChanged: EventEmitter) { 42 | this.element = properties.element; 43 | this.zoomChanged = zoomChanged; 44 | 45 | if (!this.element) { 46 | return; 47 | } 48 | 49 | if (typeof properties.limitZoom === 'number') { 50 | this.maxScale = properties.limitZoom; 51 | } 52 | this.elementTarget = this.element.querySelector('*').tagName; 53 | this.parentElement = this.element.parentElement; 54 | this.properties = Object.assign({}, defaultProperties, properties); 55 | this.detectLimitZoom(); 56 | 57 | this.touches = new Touches({ 58 | element: properties.element, 59 | listeners: properties.listeners, 60 | resize: properties.autoHeight, 61 | mouseListeners: { 62 | mousedown: 'handleMousedown', 63 | mouseup: 'handleMouseup', 64 | wheel: 'handleWheel', 65 | }, 66 | }); 67 | 68 | /* Init */ 69 | this.setBasicStyles(); 70 | 71 | /* 72 | * Listeners 73 | */ 74 | 75 | this.touches.on('touchstart', this.handleTouchstart); 76 | this.touches.on('touchend', this.handleTouchend); 77 | this.touches.on('mousedown', this.handleTouchstart); 78 | this.touches.on('mouseup', this.handleTouchend); 79 | this.touches.on('pan', this.handlePan); 80 | this.touches.on('mousemove', this.handlePan); 81 | this.touches.on('pinch', this.handlePinch); 82 | 83 | if (this.properties.wheel) { 84 | this.touches.on('wheel', this.handleWheel); 85 | } 86 | 87 | if (this.properties.doubleTap) { 88 | this.touches.on('double-tap', this.handleDoubleTap); 89 | } 90 | 91 | if (this.properties.autoHeight) { 92 | this.touches.on('resize', this.handleResize); 93 | } 94 | } 95 | 96 | /* Touchstart */ 97 | 98 | private handleTouchstart = (event: TouchEvent | MouseEvent): void => { 99 | this.touches.addEventListeners('mousemove'); 100 | this.getElementPosition(); 101 | 102 | if (this.eventType === undefined) { 103 | this.getTouchstartPosition(event); 104 | } 105 | }; 106 | 107 | /* Touchend */ 108 | 109 | private handleTouchend = (event: TouchEvent | MouseEvent): void => { 110 | /* touchend */ 111 | if (event.type === 'touchend') { 112 | const touches = (event as TouchEvent).touches; 113 | 114 | // Min scale 115 | if (this.scale < 1) { 116 | this.scale = 1; 117 | this.zoomChanged.emit(this.scale); 118 | } 119 | 120 | // Auto Zoom Out 121 | if (this.properties.autoZoomOut && this.eventType === 'pinch') { 122 | this.scale = 1; 123 | this.zoomChanged.emit(this.scale); 124 | } 125 | 126 | // Align image 127 | if (this.eventType === 'pinch' || (this.eventType === 'pan' && this.scale > this.minPanScale)) { 128 | this.alignImage(); 129 | } 130 | 131 | // Update initial values 132 | if ( 133 | this.eventType === 'pinch' || 134 | this.eventType === 'pan' || 135 | this.eventType === 'horizontal-swipe' || 136 | this.eventType === 'vertical-swipe' 137 | ) { 138 | this.updateInitialValues(); 139 | } 140 | 141 | this.eventType = 'touchend'; 142 | 143 | if (touches && touches.length === 0) { 144 | this.eventType = undefined; 145 | } 146 | } 147 | 148 | /* mouseup */ 149 | if (event.type === 'mouseup') { 150 | this.updateInitialValues(); 151 | this.eventType = undefined; 152 | } 153 | 154 | this.touches.removeEventListeners('mousemove'); 155 | }; 156 | 157 | /* 158 | * Handlers 159 | */ 160 | 161 | private handlePan = (event: TouchEvent | MouseEvent): void => { 162 | if (this.scale < this.minPanScale || this.properties.disablePan) { 163 | return; 164 | } 165 | 166 | event.preventDefault(); 167 | const { clientX, clientY } = this.getClientPosition(event); 168 | 169 | if (!this.eventType) { 170 | this.startX = clientX - this.elementPosition.left; 171 | this.startY = clientY - this.elementPosition.top; 172 | } 173 | 174 | this.eventType = 'pan'; 175 | this.moveX = this.initialMoveX + (this.moveLeft(event, 0) - this.startX); 176 | this.moveY = this.initialMoveY + (this.moveTop(event, 0) - this.startY); 177 | 178 | if (this.properties.limitPan) { 179 | this.limitPanY(); 180 | this.limitPanX(); 181 | } 182 | 183 | /* mousemove */ 184 | if (event.type === 'mousemove' && this.scale > this.minPanScale) { 185 | this.centeringImage(); 186 | } 187 | 188 | this.transformElement(0); 189 | }; 190 | 191 | private handleDoubleTap = (event: TouchEvent): void => { 192 | this.toggleZoom(event); 193 | return; 194 | }; 195 | 196 | private handlePinch = (event: TouchEvent): void => { 197 | event.preventDefault(); 198 | 199 | if (!this.properties.draggableOnPinch) { 200 | if (this.eventType === undefined || this.eventType === 'pinch') { 201 | const touches = event.touches; 202 | 203 | if (!this.eventType) { 204 | this.initialDistance = this.getDistance(touches); 205 | 206 | const moveLeft0 = this.moveLeft(event, 0); 207 | const moveLeft1 = this.moveLeft(event, 1); 208 | const moveTop0 = this.moveTop(event, 0); 209 | const moveTop1 = this.moveTop(event, 1); 210 | 211 | this.moveXC = (moveLeft0 + moveLeft1) / 2 - this.initialMoveX; 212 | this.moveYC = (moveTop0 + moveTop1) / 2 - this.initialMoveY; 213 | } 214 | 215 | this.eventType = 'pinch'; 216 | this.distance = this.getDistance(touches); 217 | this.scale = this.initialScale * (this.distance / this.initialDistance); 218 | this.zoomChanged.emit(this.scale); 219 | this.moveX = this.initialMoveX - ((this.distance / this.initialDistance) * this.moveXC - this.moveXC); 220 | this.moveY = this.initialMoveY - ((this.distance / this.initialDistance) * this.moveYC - this.moveYC); 221 | 222 | this.handleLimitZoom(); 223 | 224 | if (this.properties.limitPan) { 225 | this.limitPanY(); 226 | this.limitPanX(); 227 | } 228 | 229 | this.transformElement(0); 230 | } 231 | 232 | return; 233 | } 234 | 235 | const touches = event.touches; 236 | 237 | if (!this.eventType) { 238 | this.eventType = 'pinch'; 239 | this.initialDistance = this.getDistance(touches); 240 | const lx0 = this.moveLeft(event, 0), 241 | lx1 = this.moveLeft(event, 1), 242 | ty0 = this.moveTop(event, 0), 243 | ty1 = this.moveTop(event, 1); 244 | this.initialPinchCenterX = (lx0 + lx1) / 2; 245 | this.initialPinchCenterY = (ty0 + ty1) / 2; 246 | this.moveXC = this.initialPinchCenterX - this.initialMoveX; 247 | this.moveYC = this.initialPinchCenterY - this.initialMoveY; 248 | } 249 | 250 | this.eventType = 'pinch'; 251 | this.distance = this.getDistance(touches); 252 | const scaleRatio = this.distance / this.initialDistance; 253 | this.scale = this.initialScale * scaleRatio; 254 | this.zoomChanged.emit(this.scale); 255 | 256 | const curLX0 = this.moveLeft(event, 0), 257 | curLX1 = this.moveLeft(event, 1), 258 | curTY0 = this.moveTop(event, 0), 259 | curTY1 = this.moveTop(event, 1); 260 | const currentCenterX = (curLX0 + curLX1) / 2; 261 | const currentCenterY = (curTY0 + curTY1) / 2; 262 | 263 | const deltaX = currentCenterX - this.initialPinchCenterX; 264 | const deltaY = currentCenterY - this.initialPinchCenterY; 265 | const scaleTransX = (scaleRatio - 1) * this.moveXC; 266 | const scaleTransY = (scaleRatio - 1) * this.moveYC; 267 | 268 | this.moveX = this.initialMoveX + deltaX - scaleTransX; 269 | this.moveY = this.initialMoveY + deltaY - scaleTransY; 270 | 271 | this.handleLimitZoom(); 272 | if (this.properties.limitPan) { 273 | this.limitPanY(); 274 | this.limitPanX(); 275 | } 276 | this.transformElement(0); 277 | }; 278 | 279 | private handleWheel = (event: WheelEvent): void => { 280 | event.preventDefault(); 281 | 282 | const wheelZoomFactor = this.properties.wheelZoomFactor || 0; 283 | const zoomFactor = event.deltaY < 0 ? wheelZoomFactor : -wheelZoomFactor; 284 | let newScale = this.initialScale + zoomFactor; 285 | 286 | /* Round value */ 287 | if (newScale < 1 + wheelZoomFactor) { 288 | newScale = 1; 289 | } else if (newScale < this.maxScale && newScale > this.maxScale - wheelZoomFactor) { 290 | newScale = this.maxScale; 291 | } 292 | 293 | if (newScale < 1 || newScale > this.maxScale) { 294 | return; 295 | } 296 | 297 | if (newScale === this.scale) { 298 | return; 299 | } 300 | 301 | this.getElementPosition(); 302 | this.scale = newScale; 303 | this.zoomChanged.emit(this.scale); 304 | 305 | 306 | /* Get cursor position over image */ 307 | const xCenter = event.clientX - this.elementPosition.left - this.initialMoveX; 308 | const yCenter = event.clientY - this.elementPosition.top - this.initialMoveY; 309 | 310 | this.setZoom({ 311 | scale: newScale, 312 | center: [xCenter, yCenter], 313 | }); 314 | }; 315 | 316 | private handleResize = (_event: Event): void => { 317 | this.setAutoHeight(); 318 | }; 319 | 320 | private handleLimitZoom(): void { 321 | const limitZoom = this.maxScale; 322 | const minScale = this.properties.minScale || 0; 323 | 324 | if (this.scale > limitZoom || this.scale <= minScale) { 325 | const imageWidth = this.getImageWidth(); 326 | const imageHeight = this.getImageHeight(); 327 | const enlargedImageWidth = imageWidth * this.scale; 328 | const enlargedImageHeight = imageHeight * this.scale; 329 | const moveXRatio = this.moveX / (enlargedImageWidth - imageWidth); 330 | const moveYRatio = this.moveY / (enlargedImageHeight - imageHeight); 331 | 332 | if (this.scale > limitZoom) { 333 | this.scale = limitZoom; 334 | this.zoomChanged.emit(this.scale); 335 | } 336 | 337 | if (this.scale <= minScale) { 338 | this.scale = minScale; 339 | this.zoomChanged.emit(this.scale); 340 | } 341 | 342 | const newImageWidth = imageWidth * this.scale; 343 | const newImageHeight = imageHeight * this.scale; 344 | 345 | this.moveX = -Math.abs(moveXRatio * (newImageWidth - imageWidth)); 346 | this.moveY = -Math.abs(-moveYRatio * (newImageHeight - imageHeight)); 347 | } 348 | } 349 | 350 | private moveLeft(event: TouchEvent | MouseEvent, index: number = 0): number { 351 | const clientX = this.getClientPosition(event, index).clientX; 352 | return clientX - this.elementPosition.left; 353 | } 354 | 355 | private moveTop(event: TouchEvent | MouseEvent, index: number = 0): number { 356 | const clientY = this.getClientPosition(event, index).clientY; 357 | return clientY - this.elementPosition.top; 358 | } 359 | 360 | /* 361 | * Detection 362 | */ 363 | 364 | private centeringImage(): boolean { 365 | const img = this.getImageElement(); 366 | const initialMoveX = this.moveX; 367 | const initialMoveY = this.moveY; 368 | 369 | if (this.moveY > 0) { 370 | this.moveY = 0; 371 | } 372 | if (this.moveX > 0) { 373 | this.moveX = 0; 374 | } 375 | 376 | if (img) { 377 | this.limitPanY(); 378 | this.limitPanX(); 379 | } 380 | if (img && this.scale < 1) { 381 | if (this.moveX < this.element.offsetWidth * (1 - this.scale)) { 382 | this.moveX = this.element.offsetWidth * (1 - this.scale); 383 | } 384 | } 385 | 386 | return initialMoveX !== this.moveX || initialMoveY !== this.moveY; 387 | } 388 | 389 | private limitPanY(): void { 390 | const imgHeight = this.getImageHeight(); 391 | const scaledImgHeight = imgHeight * this.scale; 392 | const parentHeight = this.parentElement.offsetHeight; 393 | const elementHeight = this.element.offsetHeight; 394 | 395 | if (scaledImgHeight < parentHeight) { 396 | this.moveY = (parentHeight - elementHeight * this.scale) / 2; 397 | } else { 398 | const imgOffsetTop = ((imgHeight - elementHeight) * this.scale) / 2; 399 | 400 | if (this.moveY > imgOffsetTop) { 401 | this.moveY = imgOffsetTop; 402 | } else if (scaledImgHeight + Math.abs(imgOffsetTop) - parentHeight + this.moveY < 0) { 403 | this.moveY = -(scaledImgHeight + Math.abs(imgOffsetTop) - parentHeight); 404 | } 405 | } 406 | } 407 | 408 | private limitPanX(): void { 409 | const imgWidth = this.getImageWidth(); 410 | const scaledImgWidth = imgWidth * this.scale; 411 | const parentWidth = this.parentElement.offsetWidth; 412 | const elementWidth = this.element.offsetWidth; 413 | 414 | if (scaledImgWidth < parentWidth) { 415 | this.moveX = (parentWidth - elementWidth * this.scale) / 2; 416 | } else { 417 | const imgOffsetLeft = ((imgWidth - elementWidth) * this.scale) / 2; 418 | 419 | if (this.moveX > imgOffsetLeft) { 420 | this.moveX = imgOffsetLeft; 421 | } else if (scaledImgWidth + Math.abs(imgOffsetLeft) - parentWidth + this.moveX < 0) { 422 | this.moveX = -(imgWidth * this.scale + Math.abs(imgOffsetLeft) - parentWidth); 423 | } 424 | } 425 | } 426 | 427 | private setBasicStyles(): void { 428 | this.element.style.display = 'flex'; 429 | this.element.style.alignItems = 'center'; 430 | this.element.style.justifyContent = 'center'; 431 | this.element.style.transformOrigin = '0 0'; 432 | this.setImageSize(); 433 | this.setDraggableImage(); 434 | } 435 | 436 | private removeBasicStyles(): void { 437 | this.element.style.display = ''; 438 | this.element.style.alignItems = ''; 439 | this.element.style.justifyContent = ''; 440 | this.element.style.transformOrigin = ''; 441 | this.removeImageSize(); 442 | this.removeDraggableImage(); 443 | } 444 | 445 | private setDraggableImage(): void { 446 | const imgElement = this.getImageElement(); 447 | 448 | if (imgElement) { 449 | imgElement.draggable = this.properties.draggableImage; 450 | } 451 | } 452 | 453 | private removeDraggableImage(): void { 454 | const imgElement = this.getImageElement(); 455 | 456 | if (imgElement) { 457 | imgElement.draggable = true; 458 | } 459 | } 460 | 461 | private setImageSize(): void { 462 | const imgElement = this.getImageElements(); 463 | 464 | if (imgElement.length) { 465 | imgElement[0].style.maxWidth = '100%'; 466 | imgElement[0].style.maxHeight = '100%'; 467 | 468 | this.setAutoHeight(); 469 | } 470 | } 471 | 472 | private setAutoHeight(): void { 473 | const imgElement = this.getImageElements(); 474 | 475 | if (!this.properties.autoHeight || !imgElement.length) { 476 | return; 477 | } 478 | 479 | const imgNaturalWidth = imgElement[0].getAttribute('width'); 480 | const imgNaturalHeight = imgElement[0].getAttribute('height'); 481 | const sizeRatio = +imgNaturalWidth / +imgNaturalHeight; 482 | const parentWidth = this.parentElement.offsetWidth; 483 | 484 | imgElement[0].style.maxHeight = parentWidth / sizeRatio + 'px'; 485 | } 486 | 487 | private removeImageSize(): void { 488 | const imgElement = this.getImageElements(); 489 | 490 | if (imgElement.length) { 491 | imgElement[0].style.maxWidth = ''; 492 | imgElement[0].style.maxHeight = ''; 493 | } 494 | } 495 | 496 | private getElementPosition(): void { 497 | this.elementPosition = this.element.parentElement.getBoundingClientRect(); 498 | } 499 | 500 | private getTouchstartPosition(event: TouchEvent | MouseEvent): void { 501 | const { clientX, clientY } = this.getClientPosition(event); 502 | 503 | this.startX = clientX - this.elementPosition.left; 504 | this.startY = clientY - this.elementPosition.top; 505 | } 506 | 507 | private getClientPosition(event: TouchEvent | MouseEvent, index: number = 0): { clientX: number; clientY: number } { 508 | let clientX: number; 509 | let clientY: number; 510 | 511 | if (event.type === 'touchstart' || event.type === 'touchmove') { 512 | clientX = (event as TouchEvent).touches[index].clientX; 513 | clientY = (event as TouchEvent).touches[index].clientY; 514 | } 515 | if (event.type === 'mousedown' || event.type === 'mousemove') { 516 | clientX = (event as MouseEvent).clientX; 517 | clientY = (event as MouseEvent).clientY; 518 | } 519 | 520 | return { 521 | clientX, 522 | clientY, 523 | }; 524 | } 525 | 526 | private resetScale(): void { 527 | this.scale = 1; 528 | this.zoomChanged.emit(this.scale); 529 | this.moveX = 0; 530 | this.moveY = 0; 531 | this.updateInitialValues(); 532 | this.transformElement(this.properties.transitionDuration); 533 | } 534 | 535 | private updateInitialValues(): void { 536 | this.initialScale = this.scale; 537 | this.initialMoveX = this.moveX; 538 | this.initialMoveY = this.moveY; 539 | } 540 | 541 | private getDistance(touches: TouchList): number { 542 | return Math.sqrt( 543 | Math.pow(touches[0].pageX - touches[1].pageX, 2) + Math.pow(touches[0].pageY - touches[1].pageY, 2), 544 | ); 545 | } 546 | 547 | private getImageHeight(): number { 548 | const img = this.getImageElement() as HTMLImageElement; 549 | return img.offsetHeight; 550 | } 551 | 552 | private getImageWidth(): number { 553 | const img = this.getImageElement() as HTMLImageElement; 554 | return img.offsetWidth; 555 | } 556 | 557 | private transformElement(duration: number): void { 558 | this.element.style.transition = 'all ' + duration + 'ms'; 559 | this.element.style.transform = 560 | 'matrix(' + 561 | Number(this.scale) + 562 | ', 0, 0, ' + 563 | Number(this.scale) + 564 | ', ' + 565 | Number(this.moveX) + 566 | ', ' + 567 | Number(this.moveY) + 568 | ')'; 569 | } 570 | 571 | private isTouchScreen(): boolean { 572 | const prefixes = ' -webkit- -moz- -o- -ms- '.split(' '); 573 | 574 | if ('ontouchstart' in window) { 575 | return true; 576 | } 577 | 578 | // include the 'heartz' as a way to have a non matching MQ to help terminate the join 579 | // https://git.io/vznFH 580 | const query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join(''); 581 | return this.getMatchMedia(query); 582 | } 583 | 584 | private getMatchMedia(query: string): boolean { 585 | return window.matchMedia(query).matches; 586 | } 587 | 588 | public isDragging(): boolean { 589 | if (this.properties.disablePan) { 590 | return false; 591 | } 592 | 593 | const imgHeight = this.getImageHeight(); 594 | const imgWidth = this.getImageWidth(); 595 | 596 | if (this.scale > 1) { 597 | return ( 598 | imgHeight * this.scale > this.parentElement.offsetHeight || 599 | imgWidth * this.scale > this.parentElement.offsetWidth 600 | ); 601 | } 602 | if (this.scale === 1) { 603 | return imgHeight > this.parentElement.offsetHeight || imgWidth > this.parentElement.offsetWidth; 604 | } 605 | 606 | return undefined; 607 | } 608 | 609 | public detectLimitZoom(): void { 610 | // Assign to default only if it is not passed through constructor 611 | this.maxScale ??= this.defaultMaxScale; 612 | 613 | if (this.properties.limitZoom === 'original image size' && this.elementTarget === 'IMG') { 614 | // We are waiting for the element with the image to be available 615 | this.pollLimitZoomForOriginalImage(); 616 | } 617 | } 618 | 619 | private pollLimitZoomForOriginalImage(): void { 620 | const poll = setInterval(() => { 621 | const maxScaleForOriginalImage = this.getMaxScaleForOriginalImage(); 622 | if (typeof maxScaleForOriginalImage === 'number') { 623 | this.maxScale = maxScaleForOriginalImage; 624 | clearInterval(poll); 625 | } 626 | }, 10); 627 | } 628 | 629 | private getMaxScaleForOriginalImage(): number { 630 | let maxScale!: number; 631 | const img = this.element.getElementsByTagName('img')[0]; 632 | 633 | if (img.naturalWidth && img.offsetWidth) { 634 | maxScale = img.naturalWidth / img.offsetWidth; 635 | } 636 | 637 | return maxScale; 638 | } 639 | 640 | private getImageElement(): HTMLElement { 641 | const imgElement = this.element.getElementsByTagName(this.elementTarget); 642 | 643 | if (imgElement.length) { 644 | return imgElement[0] as HTMLElement; 645 | } 646 | } 647 | 648 | private getImageElements(): HTMLCollectionOf { 649 | return this.element.getElementsByTagName(this.elementTarget) as HTMLCollectionOf; 650 | } 651 | 652 | public toggleZoom(event: TouchEvent | boolean = false): void { 653 | if (this.initialScale === 1) { 654 | if (event && (event as TouchEvent).changedTouches) { 655 | if (this.properties.doubleTapScale === undefined) { 656 | return; 657 | } 658 | 659 | const changedTouches = (event as TouchEvent).changedTouches; 660 | this.scale = this.initialScale * this.properties.doubleTapScale; 661 | this.zoomChanged.emit(this.scale); 662 | this.moveX = 663 | this.initialMoveX - 664 | (changedTouches[0].clientX - this.elementPosition.left) * (this.properties.doubleTapScale - 1); 665 | this.moveY = 666 | this.initialMoveY - 667 | (changedTouches[0].clientY - this.elementPosition.top) * (this.properties.doubleTapScale - 1); 668 | } else { 669 | const zoomControlScale = this.properties.zoomControlScale || 0; 670 | this.scale = this.initialScale * (zoomControlScale + 1); 671 | this.zoomChanged.emit(this.scale); 672 | this.moveX = this.initialMoveX - (this.element.offsetWidth * (this.scale - 1)) / 2; 673 | this.moveY = this.initialMoveY - (this.element.offsetHeight * (this.scale - 1)) / 2; 674 | } 675 | 676 | this.centeringImage(); 677 | this.updateInitialValues(); 678 | this.transformElement(this.properties.transitionDuration); 679 | } else { 680 | this.resetScale(); 681 | } 682 | } 683 | 684 | public zoomIn(value: number): number { 685 | const scale = this.scale + value; 686 | 687 | if (scale >= this.maxScale) { 688 | this.scale = this.maxScale; 689 | this.zoomChanged.emit(this.scale); 690 | 691 | return this.scale; 692 | } 693 | 694 | this.setZoom({ scale }); 695 | 696 | return this.scale; 697 | } 698 | 699 | public zoomOut(value: number): number { 700 | const scale = this.scale - value; 701 | 702 | if (scale <= this.properties.minScale) { 703 | this.scale = this.properties.minScale; 704 | this.zoomChanged.emit(this.scale); 705 | 706 | return this.scale; 707 | } 708 | 709 | this.setZoom({ scale }); 710 | 711 | return this.scale; 712 | } 713 | 714 | private setZoom(properties: { scale: number; center?: number[] }): void { 715 | this.scale = properties.scale; 716 | this.zoomChanged.emit(this.scale); 717 | let xCenter; 718 | let yCenter; 719 | const visibleAreaWidth = this.element.offsetWidth; 720 | const visibleAreaHeight = this.element.offsetHeight; 721 | const scalingPercent = (visibleAreaWidth * this.scale) / (visibleAreaWidth * this.initialScale); 722 | 723 | if (properties.center) { 724 | xCenter = properties.center[0]; 725 | yCenter = properties.center[1]; 726 | } else { 727 | xCenter = visibleAreaWidth / 2 - this.initialMoveX; 728 | yCenter = visibleAreaHeight / 2 - this.initialMoveY; 729 | } 730 | 731 | this.moveX = this.initialMoveX - (scalingPercent * xCenter - xCenter); 732 | this.moveY = this.initialMoveY - (scalingPercent * yCenter - yCenter); 733 | 734 | this.centeringImage(); 735 | this.updateInitialValues(); 736 | this.transformElement(this.properties.transitionDuration); 737 | } 738 | 739 | private alignImage(): void { 740 | const isMoveChanged = this.centeringImage(); 741 | 742 | if (isMoveChanged) { 743 | this.updateInitialValues(); 744 | this.transformElement(this.properties.transitionDuration); 745 | } 746 | } 747 | 748 | public destroy(): void { 749 | this.removeBasicStyles(); 750 | this.touches.destroy(); 751 | } 752 | 753 | private getPropertiesValue(propertyName: K): Properties[K] { 754 | if (this.properties && this.properties[propertyName]) { 755 | return this.properties[propertyName]; 756 | } else { 757 | return defaultProperties[propertyName]; 758 | } 759 | } 760 | } 761 | --------------------------------------------------------------------------------