├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.MD ├── index.ts ├── package.json ├── src ├── image-zoom-container.component.metadata.json ├── image-zoom-container.component.ts ├── image-zoom-lens.component.metadata.json ├── image-zoom-lens.component.ts ├── image-zoom.directive.metadata.json └── image-zoom.directive.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Angular 2 | /compiled 3 | -*.metadata.json 4 | 5 | # Node 6 | node_modules/* 7 | npm-debug.log 8 | 9 | -# TypeScript 10 | -typings 11 | -*.js 12 | -*.map 13 | -*.d.ts 14 | - 15 | 16 | # JetBrains 17 | .idea 18 | .project 19 | .settings 20 | .idea/* 21 | *.iml 22 | 23 | # Windows 24 | Thumbs.db 25 | Desktop.ini 26 | 27 | # Mac 28 | .DS_Store 29 | **/.DS_Store 30 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules/* 3 | npm-debug.log 4 | 5 | # Angular 6 | /compiled 7 | 8 | # DO NOT IGNORE COMPILED FILES FOR NPM 9 | # TypeScript 10 | *.ts 11 | !*.d.ts 12 | 13 | # JetBrains 14 | .idea 15 | .project 16 | .settings 17 | .idea/* 18 | *.iml 19 | 20 | # Windows 21 | Thumbs.db 22 | Desktop.ini 23 | 24 | # Mac 25 | .DS_Store 26 | **/.DS_Store 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - '4.2.1' 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nick Richardson 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 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # angular2-image-zoom 2 | Angular 2.x.x Compatible 3 | 4 | ## Installation 5 | 6 | To install this library, run: 7 | 8 | ```bash 9 | $ npm install angular2-image-zoom --save 10 | ``` 11 | 12 | ## Examples 13 | First, import the ImageZoom Module 14 | 15 | ```typescript 16 | import {ImageZoomModule} from 'angular2-image-zoom'; 17 | ``` 18 | 19 | Then, add ImageZoom to your modules import array 20 | 21 | ```typescript 22 | @NgModule({ 23 | imports : [CommonModule, ImageZoomModule, ...], 24 | }) 25 | ``` 26 | Then just add the [imageZoom] tag to your img element 27 | 28 | ```html 29 | 30 | ``` 31 | 32 | # Options 33 | Name | Type | Default | Description | 34 | ---|---|---|--- | 35 | imageZoom | string | null | Link to larger [src] image 36 | allowZoom | boolean | true | Whether or not zooming is enabled 37 | clickToZoom | boolean | false | Force a user to click before zooming begins, click again to stop zooming 38 | scrollZoom | boolean | true | Allow mousewheel to change zoom level 39 | windowPosition | number | 1 | Position of zoom window. (1-16) 40 | lensStyle | string | 'WINDOW' | Type of zoom ('LENS', 'WINDOW') 41 | lensWidth | number | 300 | Width of zoom lens 42 | lensHeight | number | 300 | Height of zoom lens 43 | lensBorder | number | 2 | Size of lens border (in pixels) 44 | delay | number | 100 | Delay before zoom lens appears (in milliseconds) 45 | zoomLevel | number | auto-calculated | Initial zoom level (smaller number indicates larger zoom) 46 | minZoomLevel | number | .2 | TODO 47 | maxZoomLevel | number | auto-calculated | TODO 48 | zoomLevelIncrement | number | .1 | Size of each change in zoom level while scrolling 49 | 50 | 51 | 52 | 53 | 54 | 55 | This library is a work in progress and any issues/pull-requests are welcomed! 56 | 57 | ## TODO 58 | 1. Create unit and E2E tests 59 | 2. ~~Allow window to be placed anywhere~~ 60 | 3. ~~Show viewing lens over image to show zoom area~~ 61 | 4. ~~Allow mouse scrolling to change zoom level~~ 62 | 5. ~~Allow inner zoom lens~~ 63 | 6. Lots more.... 64 | 65 | ## Development 66 | 67 | To generate all `* 68 | }.js`, `*.js.map` and `*.d.ts` files: 69 | 70 | ```bash 71 | $ npm run tsc 72 | ``` 73 | 74 | ## License 75 | 76 | MIT © [Nick Richardson](nick.richardson@mediapixeldesign.com) 77 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {ImageZoom} from './src/image-zoom.directive'; 4 | import {ImageZoomLens} from './src/image-zoom-lens.component'; 5 | import {ImageZoomContainer} from './src/image-zoom-container.component'; 6 | 7 | export * from './src/image-zoom.directive'; 8 | export * from './src/image-zoom-lens.component'; 9 | export * from './src/image-zoom-container.component'; 10 | 11 | @NgModule({ 12 | imports : [CommonModule], 13 | exports : [ImageZoom], 14 | declarations : [ImageZoom, ImageZoomContainer, ImageZoomLens], 15 | entryComponents: [ImageZoomContainer, ImageZoomLens] 16 | }) 17 | export class ImageZoomModule { 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-image-zoom", 3 | "version": "1.2.2", 4 | "scripts": { 5 | "lint": "tslint src/**/*.ts", 6 | "test": "tsc && karma start", 7 | "prepare": "tsc && ngc", 8 | "tsc": "tsc", 9 | "ngc": "./node_modules/.bin/ngc -p tsconfig.json" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:brtnshrdr/angular2-image-zoom.git" 14 | }, 15 | "author": { 16 | "name": "Nick Richardson", 17 | "email": "nick.richardson@mediapixeldesign.com" 18 | }, 19 | "keywords": [ 20 | "angular", 21 | "angular2", 22 | "zoom", 23 | "image-zoom", 24 | "image zoom", 25 | "magnify", 26 | "hover", 27 | "enhance", 28 | "image" 29 | ], 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "git@github.com:brtnshrdr/angular2-image-zoom.git/issues" 33 | }, 34 | "main": "./index.js", 35 | "devDependencies": { 36 | "@angular/core": "^4.4.3", 37 | "@angular/common": "^4.4.3", 38 | "@angular/compiler": "^4.4.3", 39 | "@angular/compiler-cli": "^4.4.3", 40 | "@angular/platform-server": "^4.4.3", 41 | "@angular/animations": "^4.4.3", 42 | "@angular/platform-browser": "^4.4.3", 43 | "codelyzer": "~3.2.1", 44 | "rxjs": "^5.0.1", 45 | "tslint": "~5.7.0", 46 | "typescript": "2.3.4", 47 | "zone.js": "^0.8.4", 48 | "@types/protractor": "^4.0.0", 49 | "@types/jasmine": "^2.5.40", 50 | "@types/selenium-webdriver": "^2.53.39" 51 | }, 52 | "engines": { 53 | "node": ">=0.8.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/image-zoom-container.component.metadata.json: -------------------------------------------------------------------------------- 1 | [{"__symbolic":"module","version":3,"metadata":{"ImageZoomContainer":{"__symbolic":"class","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Component"},"arguments":[{"selector":"image-zoom-container","template":"","styles":["\n :host {\n position: absolute;\n text-align: center;\n overflow: hidden;\n z-index: 100;\n float: left;\n background: rgb(255, 255, 255) no-repeat;\n pointer-events: none;\n }\n "]}]}],"members":{"onMousemove":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mousemove",["$event"]]}]}],"onMouseleave":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mouseleave",["$event"]]}]}],"onMouseScroll":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["MozMousePixelScroll",["$event"]]},{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["DOMMouseScroll",["$event"]]},{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mousewheel",["$event"]]}]}],"generateStyles":[{"__symbolic":"method"}],"setBackgroundPostion":[{"__symbolic":"method"}],"setZoomSize":[{"__symbolic":"method"}],"setWindowPosition":[{"__symbolic":"method"}],"setOptions":[{"__symbolic":"method"}],"setVisibility":[{"__symbolic":"method"}],"setParentImageContainer":[{"__symbolic":"method"}],"__ctor__":[{"__symbolic":"constructor","parameters":[{"__symbolic":"reference","module":"@angular/core","name":"ElementRef"}]}]}}}},{"__symbolic":"module","version":1,"metadata":{"ImageZoomContainer":{"__symbolic":"class","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Component"},"arguments":[{"selector":"image-zoom-container","template":"","styles":["\n :host {\n position: absolute;\n text-align: center;\n overflow: hidden;\n z-index: 100;\n float: left;\n background: rgb(255, 255, 255) no-repeat;\n pointer-events: none;\n }\n "]}]}],"members":{"onMousemove":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mousemove",["$event"]]}]}],"onMouseleave":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mouseleave",["$event"]]}]}],"onMouseScroll":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["MozMousePixelScroll",["$event"]]},{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["DOMMouseScroll",["$event"]]},{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mousewheel",["$event"]]}]}],"generateStyles":[{"__symbolic":"method"}],"setBackgroundPostion":[{"__symbolic":"method"}],"setZoomSize":[{"__symbolic":"method"}],"setWindowPosition":[{"__symbolic":"method"}],"setOptions":[{"__symbolic":"method"}],"setVisibility":[{"__symbolic":"method"}],"setParentImageContainer":[{"__symbolic":"method"}],"__ctor__":[{"__symbolic":"constructor","parameters":[{"__symbolic":"reference","module":"@angular/core","name":"ElementRef"}]}]}}}}] -------------------------------------------------------------------------------- /src/image-zoom-container.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, HostListener, ElementRef} from '@angular/core'; 2 | import {ImageZoom} from './image-zoom.directive'; 3 | 4 | @Component({ 5 | selector : 'image-zoom-container', 6 | template : ``, 7 | styles : [` 8 | :host { 9 | position: absolute; 10 | text-align: center; 11 | overflow: hidden; 12 | z-index: 100; 13 | float: left; 14 | background: rgb(255, 255, 255) no-repeat; 15 | pointer-events: none; 16 | } 17 | `] 18 | }) 19 | export class ImageZoomContainer { 20 | private visible: boolean; 21 | private windowWidth: number; 22 | private windowHeight: number; 23 | private borderSize: number; 24 | private imageHeight: number; 25 | private imageWidth: number; 26 | private top: number; 27 | private left: number; 28 | private positionX: number; 29 | private positionY: number; 30 | private image: string; 31 | 32 | private el: HTMLElement; 33 | 34 | private parentImageContainer: ImageZoom; 35 | 36 | @HostListener('mousemove', ['$event']) 37 | private onMousemove(event: MouseEvent) { 38 | this.parentImageContainer.onMousemove(event); 39 | } 40 | 41 | @HostListener('mouseleave', ['$event']) 42 | public onMouseleave(event: MouseEvent) { 43 | this.parentImageContainer.onMouseleave(event); 44 | } 45 | 46 | @HostListener('MozMousePixelScroll', ['$event']) 47 | @HostListener('DOMMouseScroll', ['$event']) 48 | @HostListener('mousewheel', ['$event']) 49 | public onMouseScroll(event: any) { // MouseWheelEvent is throwing undefined error in SystemJS 50 | this.parentImageContainer.onMouseScroll(event); 51 | } 52 | 53 | 54 | private generateStyles() { 55 | this.el.style.width = this.windowWidth + 'px'; 56 | this.el.style.height = this.windowHeight + 'px'; 57 | this.el.style.border = `${this.borderSize}px solid rgb(136, 136, 136)`; 58 | this.el.style.left = this.left + 'px'; 59 | this.el.style.top = this.top + 'px'; 60 | this.el.style.backgroundImage = `url(${this.image})`; 61 | } 62 | 63 | public setBackgroundPostion(x: number, y: number) { 64 | this.el.style.backgroundPosition = `${x}px ${y}px`; 65 | this.positionX = x; 66 | this.positionY = y; 67 | } 68 | 69 | public setZoomSize(width: number, height: number) { 70 | this.el.style.backgroundSize = `${width}px ${height}px`; 71 | this.imageWidth = width; 72 | this.imageHeight = height; 73 | } 74 | 75 | setWindowPosition(left: number, top: number) { 76 | this.el.style.left = left + 'px'; 77 | this.el.style.top = top + 'px'; 78 | this.left = left; 79 | this.top = top; 80 | } 81 | 82 | 83 | public setOptions(windowWidth: number, windowHeight: number, borderSize: number, image: string) { 84 | this.windowWidth = windowWidth; 85 | this.windowHeight = windowHeight; 86 | this.borderSize = borderSize; 87 | this.image = image; 88 | this.generateStyles(); 89 | } 90 | 91 | public setVisibility(visible: boolean) { 92 | this.el.style.display = visible ? 'block' : 'none'; 93 | this.visible = visible; 94 | } 95 | 96 | 97 | public setParentImageContainer(parentImageContainer: ImageZoom) { 98 | this.parentImageContainer = parentImageContainer; 99 | } 100 | 101 | constructor(private _elementRef: ElementRef) { 102 | this.el = this._elementRef.nativeElement; 103 | this.setVisibility(false); 104 | } 105 | 106 | } -------------------------------------------------------------------------------- /src/image-zoom-lens.component.metadata.json: -------------------------------------------------------------------------------- 1 | [{"__symbolic":"module","version":3,"metadata":{"ImageZoomLens":{"__symbolic":"class","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Component"},"arguments":[{"selector":"image-zoom-lens","template":"","styles":["\n :host {\n float: right;\n overflow: hidden;\n z-index: 999;\n opacity: .4;\n zoom: 1;\n cursor: default;\n border: 1px solid rgb(0, 0, 0);\n position: absolute;\n background: rgb(255, 255, 255) no-repeat 0 0;\n pointer-events: none;\n }\n "]}]}],"members":{"onMousemove":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mousemove",["$event"]]}]}],"onMouseleave":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mouseleave",["$event"]]}]}],"onMouseScroll":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["MozMousePixelScroll",["$event"]]},{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["DOMMouseScroll",["$event"]]},{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mousewheel",["$event"]]}]}],"setLensSize":[{"__symbolic":"method"}],"setWindowPosition":[{"__symbolic":"method"}],"setOptions":[{"__symbolic":"method"}],"setVisibility":[{"__symbolic":"method"}],"setParentImageContainer":[{"__symbolic":"method"}],"__ctor__":[{"__symbolic":"constructor","parameters":[{"__symbolic":"reference","module":"@angular/core","name":"ElementRef"}]}]}}}},{"__symbolic":"module","version":1,"metadata":{"ImageZoomLens":{"__symbolic":"class","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Component"},"arguments":[{"selector":"image-zoom-lens","template":"","styles":["\n :host {\n float: right;\n overflow: hidden;\n z-index: 999;\n opacity: .4;\n zoom: 1;\n cursor: default;\n border: 1px solid rgb(0, 0, 0);\n position: absolute;\n background: rgb(255, 255, 255) no-repeat 0 0;\n pointer-events: none;\n }\n "]}]}],"members":{"onMousemove":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mousemove",["$event"]]}]}],"onMouseleave":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mouseleave",["$event"]]}]}],"onMouseScroll":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["MozMousePixelScroll",["$event"]]},{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["DOMMouseScroll",["$event"]]},{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mousewheel",["$event"]]}]}],"setLensSize":[{"__symbolic":"method"}],"setWindowPosition":[{"__symbolic":"method"}],"setOptions":[{"__symbolic":"method"}],"setVisibility":[{"__symbolic":"method"}],"setParentImageContainer":[{"__symbolic":"method"}],"__ctor__":[{"__symbolic":"constructor","parameters":[{"__symbolic":"reference","module":"@angular/core","name":"ElementRef"}]}]}}}}] -------------------------------------------------------------------------------- /src/image-zoom-lens.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, HostListener, ElementRef} from '@angular/core'; 2 | import {ImageZoom} from './image-zoom.directive'; 3 | 4 | @Component({ 5 | selector : 'image-zoom-lens', 6 | template : ``, 7 | styles : [` 8 | :host { 9 | float: right; 10 | overflow: hidden; 11 | z-index: 999; 12 | opacity: .4; 13 | zoom: 1; 14 | cursor: default; 15 | border: 1px solid rgb(0, 0, 0); 16 | position: absolute; 17 | background: rgb(255, 255, 255) no-repeat 0 0; 18 | pointer-events: none; 19 | } 20 | `] 21 | }) 22 | export class ImageZoomLens { 23 | private visible: boolean; 24 | private lensWidth: number; 25 | private lensHeight: number; 26 | private top: number; 27 | private left: number; 28 | private borderSize: number; 29 | 30 | private el: HTMLElement; 31 | 32 | private parentImageContainer: ImageZoom; 33 | 34 | @HostListener('mousemove', ['$event']) 35 | private onMousemove(event: MouseEvent) { 36 | this.parentImageContainer.onMousemove(event); 37 | } 38 | 39 | @HostListener('mouseleave', ['$event']) 40 | public onMouseleave(event: MouseEvent) { 41 | let x: number = event.clientX; 42 | let y: number = event.clientY; 43 | if(x <= this.parentImageContainer.img.x || x >= (this.parentImageContainer.img.x + this.parentImageContainer.img.width)) { 44 | this.parentImageContainer.onMouseleave(event); 45 | } else if(y <= this.parentImageContainer.img.y || y >= (this.parentImageContainer.img.y + this.parentImageContainer.img.height)){ 46 | this.parentImageContainer.onMouseleave(event); 47 | } else { 48 | this.parentImageContainer.onMousemove(event); // "mouseleave" event was just the mouse moving faster than the lens 49 | } 50 | } 51 | 52 | @HostListener('MozMousePixelScroll', ['$event']) 53 | @HostListener('DOMMouseScroll', ['$event']) 54 | @HostListener('mousewheel', ['$event']) 55 | public onMouseScroll(event: any) { // MouseWheelEvent is throwing undefined error in SystemJS 56 | this.parentImageContainer.onMouseScroll(event); 57 | } 58 | 59 | public setLensSize(width: number, height: number) { 60 | this.el.style.width = width + 'px'; 61 | this.el.style.height = height + 'px'; 62 | this.lensWidth = width; 63 | this.lensHeight = height; 64 | } 65 | 66 | setWindowPosition(left: number, top: number) { 67 | this.el.style.left = left + 'px'; 68 | this.el.style.top = top + 'px'; 69 | this.left = left; 70 | this.top = top; 71 | } 72 | 73 | 74 | public setOptions(borderSize: number) { 75 | this.el.style.border = `${borderSize}px solid rgb(136, 136, 136)`; 76 | this.borderSize = borderSize; 77 | } 78 | 79 | public setVisibility(visible: boolean) { 80 | this.el.style.display = visible ? 'block' : 'none'; 81 | this.visible = visible; 82 | } 83 | 84 | 85 | public setParentImageContainer(parentImageContainer: ImageZoom) { 86 | this.parentImageContainer = parentImageContainer; 87 | } 88 | 89 | constructor(private _elementRef: ElementRef) { 90 | this.el = this._elementRef.nativeElement; 91 | this.setVisibility(false); 92 | } 93 | 94 | } -------------------------------------------------------------------------------- /src/image-zoom.directive.metadata.json: -------------------------------------------------------------------------------- 1 | [{"__symbolic":"module","version":3,"metadata":{"ImageZoom":{"__symbolic":"class","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Directive"},"arguments":[{"selector":"[imageZoom]"}]}],"members":{"imageZoom":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"allowZoom":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"clickToZoom":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"scrollZoom":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"windowPosition":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"lensStyle":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"lensWidth":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"lensHeight":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"lensBorder":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"delay":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"minZoomLevel":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"zoomLevel":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"maxZoomLevel":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"zoomLevelIncrement":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"__ctor__":[{"__symbolic":"constructor","parameters":[{"__symbolic":"reference","module":"@angular/core","name":"ElementRef"},{"__symbolic":"reference","module":"@angular/core","name":"ComponentFactoryResolver"},{"__symbolic":"reference","module":"@angular/core","name":"ViewContainerRef"}]}],"imageChanged":[{"__symbolic":"method"}],"setImageZoomContainer":[{"__symbolic":"method"}],"setImageZoomContainerVisiblity":[{"__symbolic":"method"}],"setImageZoomLens":[{"__symbolic":"method"}],"setImageZoomLensVisibility":[{"__symbolic":"method"}],"setImageZoomLensPosition":[{"__symbolic":"method"}],"setImageZoomLensSize":[{"__symbolic":"method"}],"setImageBackgroundPosition":[{"__symbolic":"method"}],"setWindowPosition":[{"__symbolic":"method"}],"changeZoomLevel":[{"__symbolic":"method"}],"calculateOffsets":[{"__symbolic":"method"}],"calculateBoundaries":[{"__symbolic":"method"}],"allowZooming":[{"__symbolic":"method"}],"onMousemove":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mousemove",["$event"]]}]}],"onMouseenter":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mouseenter",["$event"]]}]}],"onMouseleave":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mouseleave",["$event"]]}]}],"onMouseScroll":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["MozMousePixelScroll",["$event"]]},{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["DOMMouseScroll",["$event"]]},{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mousewheel",["$event"]]}]}],"onClick":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["click",["$event"]]}]}],"ngOnChanges":[{"__symbolic":"method"}],"ngOnInit":[{"__symbolic":"method"}],"ngOnDestroy":[{"__symbolic":"method"}]}}}},{"__symbolic":"module","version":1,"metadata":{"ImageZoom":{"__symbolic":"class","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Directive"},"arguments":[{"selector":"[imageZoom]"}]}],"members":{"imageZoom":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"allowZoom":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"clickToZoom":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"scrollZoom":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"windowPosition":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"lensStyle":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"lensWidth":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"lensHeight":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"lensBorder":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"delay":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"minZoomLevel":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"zoomLevel":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"maxZoomLevel":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"zoomLevelIncrement":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"__ctor__":[{"__symbolic":"constructor","parameters":[{"__symbolic":"reference","module":"@angular/core","name":"ElementRef"},{"__symbolic":"reference","module":"@angular/core","name":"ComponentFactoryResolver"},{"__symbolic":"reference","module":"@angular/core","name":"ViewContainerRef"}]}],"imageChanged":[{"__symbolic":"method"}],"setImageZoomContainer":[{"__symbolic":"method"}],"setImageZoomContainerVisiblity":[{"__symbolic":"method"}],"setImageZoomLens":[{"__symbolic":"method"}],"setImageZoomLensVisibility":[{"__symbolic":"method"}],"setImageZoomLensPosition":[{"__symbolic":"method"}],"setImageZoomLensSize":[{"__symbolic":"method"}],"setImageBackgroundPosition":[{"__symbolic":"method"}],"setWindowPosition":[{"__symbolic":"method"}],"changeZoomLevel":[{"__symbolic":"method"}],"calculateOffsets":[{"__symbolic":"method"}],"calculateBoundaries":[{"__symbolic":"method"}],"allowZooming":[{"__symbolic":"method"}],"onMousemove":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mousemove",["$event"]]}]}],"onMouseenter":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mouseenter",["$event"]]}]}],"onMouseleave":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mouseleave",["$event"]]}]}],"onMouseScroll":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["MozMousePixelScroll",["$event"]]},{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["DOMMouseScroll",["$event"]]},{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["mousewheel",["$event"]]}]}],"onClick":[{"__symbolic":"method","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"HostListener"},"arguments":["click",["$event"]]}]}],"ngOnChanges":[{"__symbolic":"method"}],"ngOnInit":[{"__symbolic":"method"}],"ngOnDestroy":[{"__symbolic":"method"}]}}}}] -------------------------------------------------------------------------------- /src/image-zoom.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, Input, HostListener, ComponentFactoryResolver, ComponentRef, ViewContainerRef, OnInit, OnDestroy, OnChanges, SimpleChanges, ElementRef, ViewChild} from '@angular/core'; 2 | import {ImageZoomContainer} from './image-zoom-container.component'; 3 | import {ImageZoomLens} from './image-zoom-lens.component'; 4 | 5 | @Directive({ 6 | selector : '[imageZoom]' 7 | }) 8 | export class ImageZoom implements OnInit, OnDestroy, OnChanges { 9 | @Input() imageZoom: string; 10 | @Input() allowZoom: boolean = true; 11 | @Input() clickToZoom: boolean = false; 12 | @Input() scrollZoom: boolean = true; 13 | @Input() windowPosition: number = 1; 14 | @Input() lensStyle: string = 'WINDOW'; 15 | @Input() lensWidth: number = 300; 16 | @Input() lensHeight: number = 300; 17 | @Input() lensBorder: number = 2; 18 | @Input() delay: number = 100; 19 | @Input() minZoomLevel: number = .2; 20 | @Input() zoomLevel: number; 21 | 22 | @Input() 23 | private set maxZoomLevel(maxZoomLevel: number) { 24 | this._maxZoomLevel = maxZoomLevel; 25 | this._autoCalculateZoom = false; 26 | } 27 | 28 | @Input() private zoomLevelIncrement: number = .1; 29 | 30 | public img: HTMLImageElement; 31 | public imageZoomContainer: ImageZoomContainer; 32 | public imageZoomLens: ImageZoomLens; 33 | 34 | public isZooming: boolean = false; 35 | 36 | private _elementPosX: number; 37 | private _elementPosY: number; 38 | private _elementOffsetX: number; 39 | private _elementOffsetY: number; 40 | private _containerOffsetX: number; 41 | private _containerOffsetY: number; 42 | 43 | private _zoomedImage: HTMLImageElement; 44 | private _zoomedImageLoaded: boolean = false; 45 | private _zoomedImageWidth: number; 46 | private _zoomedImageHeight: number; 47 | private _maxZoomLevel: number; 48 | private _autoCalculateZoom: boolean = true; 49 | 50 | private _widthRatio: number; 51 | private _heightRatio: number; 52 | 53 | private _currentX: number = 0; 54 | private _currentY: number = 0; 55 | private _mouseMoveDebounce: number = 0; 56 | private _mouseEnterDebounce: number = 0; 57 | private _lastEvent: MouseEvent; 58 | private _previousCursor: string; 59 | 60 | private _imageLoaded: boolean = false; 61 | private _componentsCreated: boolean = false; 62 | 63 | private _imageZoomContainerRef: ComponentRef; 64 | private _imageZoomLensRef: ComponentRef; 65 | 66 | constructor(private _elementRef: ElementRef, private _componentFactoryResolver: ComponentFactoryResolver, private _viewContainerRef: ViewContainerRef) { 67 | if(this._elementRef.nativeElement.nodeName !== 'IMG') { 68 | console.error('ImageZoom not placed on image element', this._elementRef.nativeElement); 69 | return; 70 | } 71 | this.img = this._elementRef.nativeElement; 72 | this.img.onload = () => { 73 | this._imageLoaded = true; 74 | if(this._componentsCreated) { // Possible race condition if the component resolver hasn't finished yet 75 | this.imageChanged(); 76 | } 77 | }; 78 | 79 | let imageZoomContainerFactory = this._componentFactoryResolver.resolveComponentFactory(ImageZoomContainer); 80 | let imageZoomLensFactory = this._componentFactoryResolver.resolveComponentFactory(ImageZoomLens); 81 | 82 | this._imageZoomContainerRef = this._viewContainerRef.createComponent(imageZoomContainerFactory); 83 | this.imageZoomContainer = this._imageZoomContainerRef.instance; 84 | this.imageZoomContainer.setParentImageContainer(this); 85 | 86 | this._imageZoomLensRef = this._viewContainerRef.createComponent(imageZoomLensFactory); 87 | this.imageZoomLens = this._imageZoomLensRef.instance; 88 | this.imageZoomLens.setParentImageContainer(this); 89 | 90 | this._componentsCreated = true; 91 | if(this._imageLoaded) { 92 | this.imageChanged(); 93 | } 94 | } 95 | 96 | private imageChanged() { 97 | this._zoomedImageLoaded = false; 98 | this._zoomedImage = new Image(); 99 | this._zoomedImage.onload = () => { 100 | this._zoomedImageWidth = this._zoomedImage.width; 101 | this._zoomedImageHeight = this._zoomedImage.height; 102 | if(this._zoomedImageWidth < this.img.width) { 103 | this.allowZoom = false; 104 | return; 105 | } 106 | this._zoomedImageLoaded = true; 107 | this.calculateOffsets(); 108 | this.setImageZoomContainer(); 109 | this.setImageZoomLens(); 110 | this.setImageZoomLensPosition(); 111 | this.setImageZoomLensSize(); 112 | if(this._autoCalculateZoom) { 113 | if(this._zoomedImageWidth > this._zoomedImageHeight) { 114 | this._maxZoomLevel = this._zoomedImageHeight / this.lensHeight; 115 | } else { 116 | this._maxZoomLevel = this._zoomedImageWidth / this.lensWidth; 117 | } 118 | } 119 | if(!this.zoomLevel) 120 | { 121 | this.zoomLevel = (this._maxZoomLevel / 1.5); 122 | } 123 | this.changeZoomLevel(); 124 | }; 125 | this._zoomedImage.src = this.imageZoom ? this.imageZoom : this.img.src; 126 | } 127 | 128 | private setImageZoomContainer() { 129 | this.imageZoomContainer.setOptions(this.lensWidth, this.lensHeight, this.lensBorder, this._zoomedImage.src); 130 | } 131 | 132 | private setImageZoomContainerVisiblity(visible: boolean) { 133 | this.imageZoomContainer.setVisibility(visible); 134 | } 135 | 136 | private setImageZoomLens() { 137 | this.imageZoomLens.setOptions(this.lensBorder); 138 | } 139 | 140 | private setImageZoomLensVisibility(visible: boolean) { 141 | if(!visible || this.lensStyle.toUpperCase() === 'WINDOW') { 142 | this.imageZoomLens.setVisibility(visible); 143 | } 144 | } 145 | 146 | private setImageZoomLensPosition() { 147 | this.imageZoomLens.setWindowPosition(this._currentX - (this.lensWidth / this._widthRatio / 2) - this._containerOffsetX, this._currentY - (this.lensHeight / this._heightRatio / 2) - this._containerOffsetY); 148 | } 149 | 150 | private setImageZoomLensSize() { 151 | this.imageZoomLens.setLensSize(this.lensWidth / this._widthRatio, this.lensHeight / this._heightRatio); 152 | } 153 | 154 | private setImageBackgroundPosition() { 155 | let x: number = (((this._currentX - this._containerOffsetX) - this._elementOffsetX) * this._widthRatio - this.lensWidth / 2) * -1; 156 | let y: number = (((this._currentY - this._containerOffsetY) - this._elementOffsetY) * this._heightRatio - this.lensHeight / 2) * -1; 157 | this.imageZoomContainer.setBackgroundPostion(x, y); 158 | } 159 | 160 | private setWindowPosition() { 161 | if(this.lensStyle.toUpperCase() === 'LENS') { 162 | this.imageZoomContainer.setWindowPosition(this._currentX - (this.lensWidth / 2) - this.lensBorder - this._containerOffsetX, this._currentY - (this.lensHeight / 2) - this.lensBorder - this._containerOffsetY); // Account for lens border shifting image down and to the right 163 | } else if(this.lensStyle.toUpperCase() === 'WINDOW') { 164 | let windowX: number = this._elementPosX + this.img.width; 165 | let windowY: number = this._elementPosY; 166 | switch (this.windowPosition) { // Calculate x position 167 | case 1: 168 | case 2: 169 | case 3: 170 | case 4: 171 | case 16: 172 | break; // Default above 173 | case 5: 174 | case 15: 175 | windowX -= this.lensWidth; 176 | break; 177 | case 6: 178 | case 14: 179 | windowX -= ((this.lensWidth / 2) + (this.img.width / 2)); 180 | break; 181 | case 7: 182 | case 13: 183 | windowX -= this.img.width; 184 | break; 185 | case 8: 186 | case 9: 187 | case 10: 188 | case 11: 189 | case 12: 190 | windowX -= (this.img.width + this.lensWidth); 191 | break; 192 | } 193 | switch (this.windowPosition) { // Calculate Y position 194 | case 1: 195 | case 11: 196 | break; // Default above 197 | case 2: 198 | case 10: 199 | windowY += ((this.img.height / 2) - (this.lensHeight / 2)); 200 | break; 201 | case 3: 202 | case 9: 203 | windowY += (this.img.height - this.lensHeight); 204 | break; 205 | case 4: 206 | case 5: 207 | case 6: 208 | case 7: 209 | case 8: 210 | windowY += (this.img.height); 211 | break; 212 | case 12: 213 | case 13: 214 | case 14: 215 | case 15: 216 | case 16: 217 | windowY -= (this.lensHeight); 218 | break; 219 | } 220 | this.imageZoomContainer.setWindowPosition(windowX - this._containerOffsetX, windowY - this._containerOffsetY); 221 | } 222 | } 223 | 224 | private changeZoomLevel() { 225 | this._widthRatio = (this._zoomedImageWidth / this.zoomLevel) / this.img.width; 226 | this._heightRatio = (this._zoomedImageHeight / this.zoomLevel) / this.img.height; 227 | this.imageZoomContainer.setZoomSize(this._zoomedImageWidth / this.zoomLevel, this._zoomedImageHeight / this.zoomLevel); 228 | this.setImageBackgroundPosition(); 229 | } 230 | 231 | private calculateOffsets() { 232 | this._elementPosX = this.img.getBoundingClientRect().left; 233 | this._elementPosY = this.img.getBoundingClientRect().top; 234 | if(this.lensHeight > this.img.height) { 235 | this.lensHeight = this.img.height; 236 | } 237 | let parent: HTMLElement = (this.img.offsetParent); 238 | let offsetX: number = 0; 239 | let offsetY: number = 0; 240 | while (parent !== undefined && parent !== null && parent.offsetParent !== undefined && parent.offsetParent !== null) { 241 | offsetX += parent.offsetLeft; 242 | offsetY += parent.offsetTop; 243 | parent = (parent.offsetParent); 244 | } 245 | this._containerOffsetX = offsetX; 246 | this._containerOffsetY = offsetY; 247 | this._elementOffsetX = this.img.offsetLeft; 248 | this._elementOffsetY = this.img.offsetTop; 249 | this.setImageZoomContainer(); 250 | } 251 | 252 | private calculateBoundaries(clientX: number, clientY: number) { 253 | let xPos = clientX - this._elementOffsetX; 254 | let rightBoundary: number = (this.img.width - ((this.lensWidth / 2) / this._widthRatio)) + this._containerOffsetX; 255 | let leftBoundary: number = ((this.lensWidth / 2) / this._widthRatio) + this._containerOffsetX; 256 | if(xPos >= rightBoundary) { 257 | this._currentX = rightBoundary + this._elementOffsetX; 258 | } else if(xPos <= leftBoundary) { 259 | this._currentX = leftBoundary + this._elementOffsetX; 260 | } else { 261 | this._currentX = clientX; 262 | } 263 | 264 | let yPos = clientY - this._elementOffsetY; 265 | let topBoundary: number = ((this.lensHeight / 2) / this._heightRatio) + this._containerOffsetY; 266 | let bottomBoundary: number = (this.img.height - ((this.lensHeight / 2) / this._heightRatio)) + this._containerOffsetY; 267 | if(yPos >= bottomBoundary) { 268 | this._currentY = bottomBoundary + this._elementOffsetY; 269 | } else if(yPos <= topBoundary) { 270 | this._currentY = topBoundary + this._elementOffsetY; 271 | } else { 272 | this._currentY = clientY; 273 | } 274 | } 275 | 276 | public allowZooming(): boolean { 277 | return this.allowZoom && this._imageLoaded && this._zoomedImageLoaded; 278 | } 279 | 280 | @HostListener('mousemove', ['$event']) 281 | public onMousemove(event: MouseEvent) { 282 | if(this.allowZooming()) { 283 | this._lastEvent = event; // Make sure we end up at the right place, without calling too frequently 284 | if(this._mouseMoveDebounce !== 0) { 285 | return; 286 | } 287 | this._mouseMoveDebounce = window.setTimeout(() => { 288 | if(!this.isZooming && this._mouseEnterDebounce === 0) { 289 | this.onMouseenter(event); 290 | } 291 | this.calculateBoundaries(this._lastEvent.clientX, this._lastEvent.clientY); 292 | this.setImageBackgroundPosition(); 293 | this.setImageZoomLensPosition(); 294 | this.setWindowPosition(); 295 | this._mouseMoveDebounce = 0; 296 | }, 10); // Wait 10ms to be more performant 297 | } 298 | } 299 | 300 | @HostListener('mouseenter', ['$event']) 301 | public onMouseenter(event: MouseEvent) { 302 | if(!this.isZooming) { 303 | if(this.allowZooming()) { 304 | this.calculateOffsets(); 305 | if(this._mouseEnterDebounce !== 0) { 306 | clearTimeout(this._mouseEnterDebounce); 307 | } 308 | this._mouseEnterDebounce = window.setTimeout(() => { 309 | this.isZooming = true; 310 | clearTimeout(this._mouseEnterDebounce); 311 | this._previousCursor = this.img.style.cursor; 312 | this.img.style.cursor = 'pointer'; 313 | this.setImageZoomContainerVisiblity(true); 314 | this.setImageZoomLensVisibility(true); 315 | }, this.delay); 316 | } 317 | } 318 | } 319 | 320 | @HostListener('mouseleave', ['$event']) 321 | public onMouseleave(event: MouseEvent) { 322 | let x: number = event.clientX; 323 | let y: number = event.clientY; 324 | if(y <= this.img.getBoundingClientRect().top || y >= (this.img.getBoundingClientRect().top + this.img.height) || x <= this.img.getBoundingClientRect().left || x >= (this.img.getBoundingClientRect().left + this.img.width)) { 325 | if(this._mouseEnterDebounce !== 0) { 326 | clearTimeout(this._mouseEnterDebounce); 327 | } 328 | if(this.isZooming) { 329 | this.img.style.cursor = this._previousCursor; 330 | this.setImageZoomContainerVisiblity(false); 331 | this.setImageZoomLensVisibility(false); 332 | this.isZooming = false; 333 | } 334 | } else { 335 | this.onMousemove(event); // "mouseleave" event was just the mouse focus going to the lens 336 | } 337 | 338 | } 339 | 340 | @HostListener('MozMousePixelScroll', ['$event']) 341 | @HostListener('DOMMouseScroll', ['$event']) 342 | @HostListener('mousewheel', ['$event']) 343 | public onMouseScroll(event: any) { // MouseWheelEvent is throwing undefined error in SystemJS 344 | 345 | if(this.scrollZoom && this.allowZooming()) { 346 | event.stopImmediatePropagation(); 347 | event.stopPropagation(); 348 | event.preventDefault(); 349 | 350 | var pos = ((event.wheelDeltaY | event.detail * -1) != 0) ? 351 | (event.wheelDeltaY | event.detail * -1) : 352 | (event.deltaY | event.wheelDelta); 353 | 354 | if(pos > 0) { // Scroll up 355 | if(this.zoomLevel > (this.minZoomLevel + this.zoomLevelIncrement)) { 356 | this.zoomLevel -= this.zoomLevelIncrement; 357 | this.changeZoomLevel(); 358 | } 359 | } else { // Scroll down 360 | if(this.zoomLevel < (this._maxZoomLevel - this.zoomLevelIncrement)) { 361 | this.zoomLevel += this.zoomLevelIncrement; 362 | this.changeZoomLevel(); 363 | } 364 | } 365 | this.calculateBoundaries(this._lastEvent.clientX, this._lastEvent.clientY); 366 | this.setWindowPosition(); 367 | this.setImageZoomLensSize(); 368 | this.setImageZoomLensPosition(); 369 | return false; 370 | } else { 371 | return true; 372 | } 373 | } 374 | 375 | @HostListener('click', ['$event']) 376 | public onClick(event: MouseEvent) { 377 | if(this.clickToZoom) { 378 | this.allowZoom = !this.allowZoom; 379 | if(this.allowZooming()) { 380 | this.onMousemove(event); 381 | } else { 382 | this.onMouseleave(event); 383 | } 384 | } 385 | } 386 | 387 | ngOnChanges(changeRecord: SimpleChanges) { 388 | for (let prop in changeRecord) { 389 | if(!changeRecord[prop].isFirstChange()) { 390 | if(prop === 'imageZoom') { // Image changed 391 | this.imageChanged(); 392 | } else if(prop === 'lensWidth' || prop === 'lensHeight') { 393 | if(this.lensHeight > this.img.height) { 394 | this.lensHeight = this.img.height; 395 | } 396 | this.setImageZoomContainer(); 397 | this.setImageBackgroundPosition(); 398 | } else if(prop === 'zoomLevel') { 399 | this.changeZoomLevel(); 400 | } 401 | } 402 | } 403 | } 404 | 405 | ngOnInit() { 406 | if(this.clickToZoom) { 407 | this.allowZoom = false; 408 | } 409 | } 410 | 411 | ngOnDestroy() { 412 | this._imageZoomContainerRef.destroy(); 413 | this._imageZoomLensRef.destroy(); 414 | } 415 | 416 | } 417 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "module": "commonjs", 5 | "target": "ES5", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "lib": ["es2016", "dom"] 11 | }, 12 | "angularCompilerOptions": { 13 | "genDir": "compiled" 14 | }, 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "files": [ 19 | "index.ts" 20 | ], 21 | "exclude": [ 22 | "node_modules" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "label-position": true, 19 | "label-undefined": true, 20 | "max-line-length": [ 21 | true, 22 | 140 23 | ], 24 | "member-access": false, 25 | "member-ordering": [ 26 | true, 27 | "static-before-instance", 28 | "variables-before-functions" 29 | ], 30 | "no-arg": true, 31 | "no-bitwise": true, 32 | "no-console": [ 33 | true, 34 | "debug", 35 | "info", 36 | "time", 37 | "timeEnd", 38 | "trace" 39 | ], 40 | "no-construct": true, 41 | "no-debugger": true, 42 | "no-duplicate-key": true, 43 | "no-duplicate-variable": true, 44 | "no-empty": false, 45 | "no-eval": true, 46 | "no-inferrable-types": true, 47 | "no-shadowed-variable": true, 48 | "no-string-literal": false, 49 | "no-switch-case-fall-through": true, 50 | "no-trailing-whitespace": true, 51 | "no-unused-expression": true, 52 | "no-unused-variable": true, 53 | "no-unreachable": true, 54 | "no-use-before-declare": true, 55 | "no-var-keyword": true, 56 | "object-literal-sort-keys": false, 57 | "one-line": [ 58 | true, 59 | "check-open-brace", 60 | "check-catch", 61 | "check-else", 62 | "check-whitespace" 63 | ], 64 | "quotemark": [ 65 | true, 66 | "single" 67 | ], 68 | "radix": true, 69 | "semicolon": [ 70 | "always" 71 | ], 72 | "triple-equals": [ 73 | true, 74 | "allow-null-check" 75 | ], 76 | "typedef-whitespace": [ 77 | true, 78 | { 79 | "call-signature": "nospace", 80 | "index-signature": "nospace", 81 | "parameter": "nospace", 82 | "property-declaration": "nospace", 83 | "variable-declaration": "nospace" 84 | } 85 | ], 86 | "variable-name": false, 87 | "whitespace": [ 88 | true, 89 | "check-decl", 90 | "check-operator", 91 | "check-separator", 92 | "check-type" 93 | ], 94 | 95 | "directive-selector-name": [true, "camelCase"], 96 | "component-selector-name": [true, "kebab-case"], 97 | "directive-selector-type": [true, "attribute"], 98 | "component-selector-type": [true, "element"], 99 | "use-input-property-decorator": true, 100 | "use-output-property-decorator": true, 101 | "use-host-property-decorator": true, 102 | "no-input-rename": true, 103 | "no-output-rename": true, 104 | "use-life-cycle-interface": true, 105 | "use-pipe-transform-interface": true, 106 | "component-class-suffix": true, 107 | "directive-class-suffix": true 108 | } 109 | } 110 | --------------------------------------------------------------------------------