├── 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 | toggleZoom()
269 | destroy()
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 |
--------------------------------------------------------------------------------