From 6087f22ab0f79a3740cd9edd16121f42244bb860 Mon Sep 17 00:00:00 2001
From: Enlcxx
Date: Wed, 9 Oct 2024 22:27:14 -0500
Subject: [PATCH] feat(cropper): add `LyImageCropperBase` without Alyle UI
dependency
---
src/api/docs/components/image-cropper.html | 4 +-
.../cropper-basic-usage.component.html.html | 4 +-
.../cropper-basic-usage.component.ts.html | 12 +-
.../demos/cropper-basic-usage.module.ts.html | 4 +-
src/api/docs/demos/cropper-dialog.html.html | 4 +-
src/api/docs/demos/cropper-dialog.ts.html | 9 +-
.../demos/cropper-with-dialog.module.ts.html | 4 +-
src/api/docs/demos/doc-viewer.html.html | 11 +
src/api/docs/demos/docs-viewer.module.ts.html | 37 +
src/api/docs/demos/docs-viewer.ts.html | 193 ++
src/api/docs/demos/element-registry.ts.html | 148 ++
.../demos/elements-loader.service.ts.html | 66 +
...with-prefix-and-suffix.component.html.html | 2 +-
.../grid-demo-responsive.component.html.html | 12 +-
.../docs/getting-started/installation.html | 2 +-
src/api/docs/guides/lazy-loading.html | 19 -
.../docs/guides/migrating-to-alyle-ui-3.html | 127 --
.../cropper-basic-usage.component.html | 4 +-
.../cropper-basic-usage.component.ts | 12 +-
.../cropper-basic-usage.module.ts | 4 +-
.../cropper-with-dialog/cropper-dialog.html | 4 +-
.../cropper-with-dialog/cropper-dialog.ts | 9 +-
.../cropper-with-dialog.module.ts | 4 +-
.../image-cropper-demo/image-cropper.md | 4 +-
src/lib/image-cropper/_image-cropper-base.ts | 1816 ++++++++++++++++
.../image-cropper-area-base.html | 7 +
src/lib/image-cropper/image-cropper-area.html | 2 +-
src/lib/image-cropper/image-cropper-base.html | 25 +
src/lib/image-cropper/image-cropper-base.scss | 162 ++
src/lib/image-cropper/image-cropper-base.ts | 46 +
src/lib/image-cropper/image-cropper.module.ts | 13 +-
src/lib/image-cropper/image-cropper.ts | 1828 +----------------
src/lib/image-cropper/public_api.ts | 2 +
33 files changed, 2598 insertions(+), 2002 deletions(-)
create mode 100644 src/api/docs/demos/doc-viewer.html.html
create mode 100644 src/api/docs/demos/docs-viewer.module.ts.html
create mode 100644 src/api/docs/demos/docs-viewer.ts.html
create mode 100644 src/api/docs/demos/element-registry.ts.html
create mode 100644 src/api/docs/demos/elements-loader.service.ts.html
delete mode 100644 src/api/docs/guides/lazy-loading.html
delete mode 100644 src/api/docs/guides/migrating-to-alyle-ui-3.html
create mode 100644 src/lib/image-cropper/_image-cropper-base.ts
create mode 100644 src/lib/image-cropper/image-cropper-area-base.html
create mode 100644 src/lib/image-cropper/image-cropper-base.html
create mode 100644 src/lib/image-cropper/image-cropper-base.scss
create mode 100644 src/lib/image-cropper/image-cropper-base.ts
diff --git a/src/api/docs/components/image-cropper.html b/src/api/docs/components/image-cropper.html
index 7610bd4e3..bad259698 100644
--- a/src/api/docs/components/image-cropper.html
+++ b/src/api/docs/components/image-cropper.html
@@ -8,7 +8,7 @@ Basic Usage
Add the < ly-img-cropper>
to your template:
-< ly-img-cropper
+< ly-img-cropper-base
[config] = " myConfig"
[(scale)] = " scale"
(ready) = " onReady($event)"
@@ -19,7 +19,7 @@ Basic Usage
(error) = " onError($event)"
>
< span> Drag and drop image</ span>
-</ ly-img-cropper>
+</ ly-img-cropper-base>
< ng-container *ngIf = " ready" >
< ly-slider
diff --git a/src/api/docs/demos/cropper-basic-usage.component.html.html b/src/api/docs/demos/cropper-basic-usage.component.html.html
index d7fc09e1d..b17784343 100644
--- a/src/api/docs/demos/cropper-basic-usage.component.html.html
+++ b/src/api/docs/demos/cropper-basic-usage.component.html.html
@@ -7,7 +7,7 @@
< div *ngIf = " ready" >
< button (click) = " cropper.rotate(-90)" ly-button appearance = " icon" > < ly-icon> rotate_90_degrees_ccw</ ly-icon> </ button>
</ div>
-< ly-img-cropper
+< ly-img-cropper-base
[config] = " myConfig"
(ready) = " ready = true"
(cleaned) = " ready = false"
@@ -16,7 +16,7 @@
(error) = " onError($event)"
>
< span> Drag and drop image</ span>
-</ ly-img-cropper>
+</ ly-img-cropper-base>
< button ly-button *ngIf = " ready" color = " accent" (click) = " cropper.crop()" >
< ly-icon> crop</ ly-icon> crop
diff --git a/src/api/docs/demos/cropper-basic-usage.component.ts.html b/src/api/docs/demos/cropper-basic-usage.component.ts.html
index 2bd4f8394..ee597e29a 100644
--- a/src/api/docs/demos/cropper-basic-usage.component.ts.html
+++ b/src/api/docs/demos/cropper-basic-usage.component.ts.html
@@ -1,19 +1,17 @@
import { Component, ChangeDetectionStrategy, ViewChild } from '@angular/core' ;
-import { StyleRenderer, lyl, ThemeVariables, SelectorsFn } from '@alyle/ui' ;
+import { StyleRenderer, lyl, ThemeVariables } from '@alyle/ui' ;
import {
ImgCropperConfig,
ImgCropperEvent,
- LyImageCropper,
+ LyImageCropperBase,
ImgCropperErrorEvent,
- STYLES as CROPPER_STYLES
} from '@alyle/ui/image-cropper' ;
-const STYLES = ( _theme: ThemeVariables, selectors: SelectorsFn) => {
- const cropper = selectors ( CROPPER_STYLES ) ;
+const STYLES = ( _theme: ThemeVariables) => {
return {
root: lyl ` {
- ${ cropper. root} {
+ .ly-cropper-root {
aspect-ratio : 3 / 2
max-width : 600px
}
@@ -38,7 +36,7 @@
$$ = this . sRenderer. renderSheet ( STYLES , 'root' ) ;
croppedImage? : string | null = null ;
ready = false ;
- @ ViewChild ( LyImageCropper) readonly cropper! : LyImageCropper;
+ @ ViewChild ( LyImageCropperBase) readonly cropper! : LyImageCropperBase;
myConfig: ImgCropperConfig = {
width: 200 , // Default `250`
height: 200 , // Default `200`
diff --git a/src/api/docs/demos/cropper-basic-usage.module.ts.html b/src/api/docs/demos/cropper-basic-usage.module.ts.html
index 8d7fab0c1..5188d1251 100644
--- a/src/api/docs/demos/cropper-basic-usage.module.ts.html
+++ b/src/api/docs/demos/cropper-basic-usage.module.ts.html
@@ -1,7 +1,7 @@
import { NgModule } from '@angular/core' ;
import { CommonModule } from '@angular/common' ;
import { CropperBasicUsageComponent } from './cropper-basic-usage.component' ;
-import { LyImageCropperModule } from '@alyle/ui/image-cropper' ;
+import { LyImageCropperBaseModule } from '@alyle/ui/image-cropper' ;
import { LyButtonModule } from '@alyle/ui/button' ;
import { LyIconModule } from '@alyle/ui/icon' ;
@@ -13,7 +13,7 @@
] ,
imports: [
CommonModule,
- LyImageCropperModule,
+ LyImageCropperBaseModule,
LyButtonModule,
LyIconModule
]
diff --git a/src/api/docs/demos/cropper-dialog.html.html b/src/api/docs/demos/cropper-dialog.html.html
index 46a450be6..47432ec8d 100644
--- a/src/api/docs/demos/cropper-dialog.html.html
+++ b/src/api/docs/demos/cropper-dialog.html.html
@@ -10,7 +10,7 @@
< button (click) = " cropper.setScale(1)" ly-button > 1:1</ button>
</ div>
- < ly-img-cropper
+ < ly-img-cropper-base
[config] = " myConfig"
[(scale)] = " scale"
(ready) = " ready = true"
@@ -22,7 +22,7 @@
(error) = " onError($event)"
>
< span> Drag and drop image</ span>
- </ ly-img-cropper>
+ </ ly-img-cropper-base>
< div [className] = " classes.sliderContainer" >
< div [class] = " classes.slider" >
diff --git a/src/api/docs/demos/cropper-dialog.ts.html b/src/api/docs/demos/cropper-dialog.ts.html
index e7daf4e5a..6102d5c64 100644
--- a/src/api/docs/demos/cropper-dialog.ts.html
+++ b/src/api/docs/demos/cropper-dialog.ts.html
@@ -3,8 +3,7 @@
import { LyDialogRef, LY_DIALOG_DATA } from '@alyle/ui/dialog' ;
import { LySliderChange, STYLES as SLIDER_STYLES } from '@alyle/ui/slider' ;
import {
- STYLES as CROPPER_STYLES ,
- LyImageCropper,
+ LyImageCropperBase,
ImgCropperConfig,
ImgCropperEvent,
ImgCropperErrorEvent,
@@ -13,13 +12,11 @@
const STYLES = ( _theme: ThemeVariables, ref: ThemeRef) => {
ref. renderStyleSheet ( SLIDER_STYLES ) ;
- ref. renderStyleSheet ( CROPPER_STYLES ) ;
const slider = ref. selectorsOf ( SLIDER_STYLES ) ;
- const cropper = ref. selectorsOf ( CROPPER_STYLES ) ;
return {
root: lyl ` {
- ${ cropper. root} {
+ .ly-cropper-root {
max-width : 320px
height : 320px
}
@@ -55,7 +52,7 @@
scale: number ;
minScale: number ;
maxScale: number ;
- @ ViewChild ( LyImageCropper, { static : true } ) cropper: LyImageCropper;
+ @ ViewChild ( LyImageCropperBase, { static : true } ) cropper: LyImageCropperBase;
myConfig: ImgCropperConfig = {
width: 150 ,
height: 150 ,
diff --git a/src/api/docs/demos/cropper-with-dialog.module.ts.html b/src/api/docs/demos/cropper-with-dialog.module.ts.html
index 7cd14be1a..c542c56da 100644
--- a/src/api/docs/demos/cropper-with-dialog.module.ts.html
+++ b/src/api/docs/demos/cropper-with-dialog.module.ts.html
@@ -1,7 +1,7 @@
import { NgModule } from '@angular/core' ;
import { CommonModule } from '@angular/common' ;
import { FormsModule } from '@angular/forms' ;
-import { LyImageCropperModule } from '@alyle/ui/image-cropper' ;
+import { LyImageCropperBaseModule } from '@alyle/ui/image-cropper' ;
import { LySliderModule } from '@alyle/ui/slider' ;
import { LyButtonModule } from '@alyle/ui/button' ;
import { LyIconModule } from '@alyle/ui/icon' ;
@@ -20,7 +20,7 @@
imports: [
CommonModule,
FormsModule,
- LyImageCropperModule,
+ LyImageCropperBaseModule,
LySliderModule,
LyButtonModule,
LyIconModule,
diff --git a/src/api/docs/demos/doc-viewer.html.html b/src/api/docs/demos/doc-viewer.html.html
new file mode 100644
index 000000000..8f112478d
--- /dev/null
+++ b/src/api/docs/demos/doc-viewer.html.html
@@ -0,0 +1,11 @@
+< div *ngIf = " isLoading | async" >
+ < h1 lySkeleton > ...</ h1>
+ < p lySkeleton > ...</ p>
+ < div lySkeleton [class] = " ADS_STYLES" > </ div>
+ < h2 lySkeleton > ...</ h2>
+</ div>
+
+< div *ngIf = " isError | async as err" >
+ < h1> {{ $any(err).title }}</ h1>
+</ div>
+
\ No newline at end of file
diff --git a/src/api/docs/demos/docs-viewer.module.ts.html b/src/api/docs/demos/docs-viewer.module.ts.html
new file mode 100644
index 000000000..22f8c870f
--- /dev/null
+++ b/src/api/docs/demos/docs-viewer.module.ts.html
@@ -0,0 +1,37 @@
+import { NgModule } from '@angular/core' ;
+import { CommonModule } from '@angular/common' ;
+import { RouterModule, Routes } from '@angular/router' ;
+import { HttpClientModule } from '@angular/common/http' ;
+import { LyCommonModule } from '@alyle/ui' ;
+import { LySkeletonModule } from '@alyle/ui/skeleton' ;
+
+import { DocViewer } from './docs-viewer' ;
+import { DemoViewModule } from '@app/demo-view' ;
+import { ElementsLoader } from './elements-loader.service' ;
+import { ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN , ELEMENT_MODULE_LOAD_CALLBACKS } from './element-registry' ;
+
+const routes: Routes = [
+ { path: '' , component: DocViewer }
+] ;
+
+@ NgModule ( {
+ imports: [
+ LyCommonModule,
+ CommonModule,
+ HttpClientModule,
+ DemoViewModule,
+ LySkeletonModule,
+ RouterModule. forChild ( routes)
+ ] ,
+ declarations: [ DocViewer ] ,
+ exports: [ DocViewer ] ,
+ providers: [
+ ElementsLoader,
+ {
+ provide: ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN ,
+ useValue: ELEMENT_MODULE_LOAD_CALLBACKS
+ }
+ ]
+} )
+export class DocViewerModule { }
+
\ No newline at end of file
diff --git a/src/api/docs/demos/docs-viewer.ts.html b/src/api/docs/demos/docs-viewer.ts.html
new file mode 100644
index 000000000..a90e4c920
--- /dev/null
+++ b/src/api/docs/demos/docs-viewer.ts.html
@@ -0,0 +1,193 @@
+import { Component, Input, ElementRef, EventEmitter, Renderer2, Injector, ChangeDetectionStrategy } from '@angular/core' ;
+import { observeOn, switchMap, takeUntil, take, catchError, tap } from 'rxjs/operators' ;
+import { asapScheduler, of } from 'rxjs' ;
+import { HttpClient, HttpErrorResponse } from '@angular/common/http' ;
+
+import { ElementsLoader } from './elements-loader.service' ;
+import { LyTypographyVariables } from '@alyle/ui/typography' ;
+import { ThemeVariables, LyTheme2, lyl, StyleCollection, StyleTemplate } from '@alyle/ui' ;
+import { ViewComponent } from '@app/demo-view/view/view.component' ;
+import { Ads, ADS_STYLES } from '@sha red/ads' ;
+import { SEOService } from '@app/sha red/seo.service' ;
+import { Platform } from '@angular/cdk/platform' ;
+
+// Initialization prevents flicker once pre-rendering is on
+const initialDocViewerElement = typeof document === 'object' && ! ! document
+ ? document. querySelector ( 'aui-doc-viewer > div' )
+ : null ;
+let initialDocViewerContent = initialDocViewerElement ? initialDocViewerElement. innerHTML : '' ;
+
+interface Err {
+ title: string ;
+}
+
+const STYLES = ( theme: ThemeVariables & LyTypographyVariables) => {
+ const { h3, h4, h5, h6, subtitle1, subtitle2 } = theme. typography. lyTyp! ;
+ const getStyle = ( typ: StyleCollection< ( ) => StyleTemplate> | ( ( ) => StyleTemplate) ) => {
+ return typ instanceof StyleCollection
+ ? typ. setTransformer ( ( _) => _ ( ) )
+ : typ ( ) ;
+ } ;
+ return {
+ root: lyl ` {
+ > div > {
+ h1 {
+ {
+ ...${ getStyle ( h3! ) }
+ }
+ font-size : ${ theme. pxToRem ( 40 ) } tant">!impor tant
+ margin : 1em 0
+ }
+ h2 {
+ ...${ getStyle ( h4! ) }
+ }
+ h3 {
+ ... ${ getStyle ( h5! ) }
+ }
+ h4 {
+ ... ${ getStyle ( h6! ) }
+ }
+ h5 {
+ ... ${ getStyle ( subtitle1! ) }
+ }
+ h6 {
+ ... ${ getStyle ( subtitle2! ) }
+ }
+ }
+ } `
+ } ;
+} ;
+
+@ Component ( {
+ selector: 'aui-doc-viewer' ,
+ templateUrl: './doc-viewer.html' ,
+ changeDetection: ChangeDetectionStrategy. OnPush
+} )
+export class DocViewer {
+ readonly classes = this . theme. renderStyleSheet ( STYLES ) ;
+ readonly hostElement: HTMLElement;
+ private onDestroy$ = new EventEmitter< void > ( ) ;
+ private docContents$ = new EventEmitter< string > ( ) ;
+ private void $ = of < void > ( undefined ) ;
+ readonly isLoading = new EventEmitter< boolean > ( ) ;
+ readonly isError = new EventEmitter< Err | void > ( ) ;
+
+ readonly ADS_STYLES = this . theme. renderStyle ( ADS_STYLES ) ;
+
+ @ Input ( )
+ get path ( ) {
+ return this . _path;
+ }
+ set path ( val: string ) {
+ if ( val !== this . path) {
+ this . _path = val;
+ if ( val !== '/' && val !== '' ) {
+ this . docContents$. emit ( val) ;
+ } else {
+ this . seo. setTitle ( ) ;
+ this . seo. setNoIndex ( false ) ;
+ this . hostElement. innerHTML = '' ;
+ this . isError. emit ( null ! ) ;
+ }
+ }
+ }
+ private _path: string ;
+
+ constructor (
+ injector: Injector,
+ elementRef: ElementRef,
+ private http: HttpClient,
+ private elementsLoader: ElementsLoader,
+ private theme: LyTheme2,
+ private renderer: Renderer2,
+ private ads: Ads,
+ private seo: SEOService,
+ private _platform: Platform
+ ) {
+ this . isLoading. emit ( ! initialDocViewerContent) ;
+ this . hostElement = renderer. createElement ( 'div' ) ;
+ renderer. appendChild ( elementRef. nativeElement, this . hostElement) ;
+ this . renderer. addClass ( elementRef. nativeElement, this . classes. root) ;
+ this . hostElement. innerHTML = initialDocViewerContent;
+
+ if ( this . _platform. isBrowser) {
+ const { createCustomElement } = require ( '@angular/elements' ) ;
+ const element = createCustomElement ( ViewComponent, { injector } ) ;
+ customElements. define ( 'demo-view' , element) ;
+ }
+
+ this . docContents$
+ . pipe (
+ observeOn ( asapScheduler) ,
+ switchMap ( path => this . render ( path) ) ,
+ takeUntil ( this . onDestroy$)
+ )
+ . subscribe ( ) ;
+ }
+
+ onDestroy ( ) {
+ this . onDestroy$. emit ( ) ;
+ }
+
+ render ( path: string ) {
+ path = this . seo. url ( path) . pathname;
+ this . isLoading. emit ( ! initialDocViewerContent) ;
+ if ( ! initialDocViewerContent) {
+ this . hostElement. innerHTML = '' ;
+ }
+ this . isError. emit ( null ! ) ;
+ return this . void$
+ . pipe (
+ switchMap ( async ( ) => {
+ return ( path. startsWith ( '/api/@alyle/ui' ) || path === '/api' )
+ ? 'API'
+ : ( await Promise . all ( [
+ this . http. get ( ` api/docs ${ path} .html ` , {
+ responseType: 'text'
+ } ) . pipe (
+ take ( 1 ) ,
+ catchError ( ( err: HttpErrorResponse | Error) => {
+ this . hostElement. innerHTML = '' ;
+ const errorMessage = ( err instanceof Error ) ? err. stack : err. message;
+ const is404 = ( err instanceof Error ) ? false : err. status === 404 ;
+ console . error ( 'Err' , errorMessage) ;
+ this . isLoading. emit ( false ) ;
+ this . seo. setNoIndex ( true ) ;
+ const errMsg = is404 ? 'PAGE NOT FOUND' : 'REQUEST FOR DOCUMENT FAILED' ;
+ this . seo. setTitle ( ` Alyle UI - ${ errMsg} ` ) ;
+ this . isError. emit ( {
+ title: errMsg
+ } ) ;
+ return this . void$;
+ } ) ,
+ ) . toPromise ( ) ,
+ this . elementsLoader. load ( path)
+ ] ) ) [ 0 ] ;
+ } ) ,
+ tap ( ( html) => {
+ if ( html) {
+ initialDocViewerContent = '' ;
+ if ( html !== 'API' ) {
+ this . isLoading. emit ( false ) ;
+ this . seo. setNoIndex ( false ) ;
+ const { hostElement } = this ;
+ hostElement. innerHTML = html;
+ const h1 = hostElement. querySelector ( 'h1' ) ;
+ let title = ( h1 && h1. textContent) || 'Untitled' ;
+ if ( path. includes ( '/components/' ) ) {
+ title = ` ${ title} Angular Component ` ;
+ }
+ this . seo. setTitle ( ` Alyle UI - ${ title} ` ) ;
+ // Show skeleton screen Platform is Server
+ if ( ! this . _platform. isBrowser) {
+ hostElement. innerHTML = '' ;
+ this . isLoading. emit ( true ) ;
+ }
+ this . ads. update ( path, this . theme) ;
+ }
+ }
+ } )
+ ) ;
+ }
+}
+
\ No newline at end of file
diff --git a/src/api/docs/demos/element-registry.ts.html b/src/api/docs/demos/element-registry.ts.html
new file mode 100644
index 000000000..d1ccfb8da
--- /dev/null
+++ b/src/api/docs/demos/element-registry.ts.html
@@ -0,0 +1,148 @@
+import { LoadChildrenCallback } from '@angular/router' ;
+import { InjectionToken, Type } from '@angular/core' ;
+
+export const ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES = [
+ {
+ path: '/customization/dynamic-styles' ,
+ loadChildren : ( ) => import ( './customization/dynamic-styles.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/customization/paper' ,
+ loadChildren : ( ) => import ( './customization/paper.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/customization/multiple-themes' ,
+ loadChildren : ( ) => import ( './customization/multiple-themes.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/grid' ,
+ loadChildren : ( ) => import ( './layout/grid-demo/grid.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/responsive' ,
+ loadChildren : ( ) => import ( './layout/responsive/responsive.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/tabs' ,
+ loadChildren : ( ) => import ( './layout/tabs-demo/tabs.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/avatar' ,
+ loadChildren : ( ) => import ( './components/avatar-demo/avatar.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/badge' ,
+ loadChildren : ( ) => import ( './components/badge-demo/badge.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/button' ,
+ loadChildren : ( ) => import ( './components/button-demo/button.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/card' ,
+ loadChildren : ( ) => import ( './components/card-demo/card.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/carousel' ,
+ loadChildren : ( ) => import ( './components/carousel-demo/carousel.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/checkbox' ,
+ loadChildren : ( ) => import ( './components/checkbox-demo/checkbox.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/dialog' ,
+ loadChildren : ( ) => import ( './components/dialog-demo/dialog.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/divider' ,
+ loadChildren : ( ) => import ( './components/divider-demo/divider.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/drawer' ,
+ loadChildren : ( ) => import ( './components/drawer-demo/drawer.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/expansion' ,
+ loadChildren : ( ) => import ( './components/expansion-demo/expansion.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/field' ,
+ loadChildren : ( ) => import ( './components/field-demo/field.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/icon' ,
+ loadChildren : ( ) => import ( './components/icon-demo/icon.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/image-cropper' ,
+ loadChildren : ( ) => import ( './components/image-cropper-demo/image-cropper.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/list' ,
+ loadChildren : ( ) => import ( './components/list-demo/list.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/menu' ,
+ loadChildren : ( ) => import ( './components/menu-demo/menu.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/radio' ,
+ loadChildren : ( ) => import ( './components/radio-demo/radio.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/select' ,
+ loadChildren : ( ) => import ( './components/select-demo/select.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/slider' ,
+ loadChildren : ( ) => import ( './components/slider-demo/slider.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/snack-bar' ,
+ loadChildren : ( ) => import ( './components/snack-bar-demo/snack-bar.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/toolbar' ,
+ loadChildren : ( ) => import ( './components/toolbar-demo/toolbar.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/toolbar' ,
+ loadChildren : ( ) => import ( './components/toolbar-demo/toolbar.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/tooltip' ,
+ loadChildren : ( ) => import ( './components/tooltip-demo/tooltip.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/typography' ,
+ loadChildren : ( ) => import ( './components/typography-demo/typography.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/skeleton' ,
+ loadChildren : ( ) => import ( './components/skeleton-demo/skeleton.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+ {
+ path: '/components/table' ,
+ loadChildren : ( ) => import ( './components/table-demo/table.lazy.module' ) . then ( mod => mod. LazyModule)
+ } ,
+] ;
+
+/**
+ * Interface expected to be implemented by all modules that declare a component that can be used as
+ * a custom element.
+ */
+export interface WithCustomElementComponent {
+ customElementComponents: Type< any > [ ] ;
+}
+
+/** Injection token to provide the element path modules. */
+export const ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN = new InjectionToken<
+ Map< string , LoadChildrenCallback>
+> ( 'ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN' ) ;
+
+export const ELEMENT_MODULE_LOAD_CALLBACKS = new Map< string , LoadChildrenCallback> ( ) ;
+ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES . forEach ( route => {
+ ELEMENT_MODULE_LOAD_CALLBACKS . set ( route. path, route. loadChildren) ;
+} ) ;
+
\ No newline at end of file
diff --git a/src/api/docs/demos/elements-loader.service.ts.html b/src/api/docs/demos/elements-loader.service.ts.html
new file mode 100644
index 000000000..5babf6d50
--- /dev/null
+++ b/src/api/docs/demos/elements-loader.service.ts.html
@@ -0,0 +1,66 @@
+import { Injectable, Inject, Type, createNgModuleRef, NgModuleRef } from '@angular/core' ;
+import { ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN , WithCustomElementComponent } from './element-registry' ;
+import { LoadChildrenCallback } from '@angular/router' ;
+import { createCustomElement } from '@angular/elements' ;
+
+@ Injectable ( )
+export class ElementsLoader {
+ /** Map of unregistered custom elements and their respective module paths to load. */
+ private elementsToLoad: Map< string , LoadChildrenCallback> ;
+ /** Map of custom elements that are in the process of being loaded and registered. */
+ private elementsLoading = new Map< string , Promise < void >> ( ) ;
+ constructor (
+ private moduleRef: NgModuleRef< any > ,
+ @ Inject ( ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN ) elementModulePaths: Map< string , LoadChildrenCallback>
+ ) {
+ this . elementsToLoad = new Map ( elementModulePaths) ;
+ }
+
+ load ( path: string ) {
+ if ( this . elementsLoading. has ( path) ) {
+ // The custom element is in the process of being loaded and registered.
+ return this . elementsLoading. get ( path) as Promise < void > ;
+ }
+
+ if ( this . elementsToLoad. has ( path) ) {
+ // Load and register the custom element (for the first time).
+ const modulePathLoader = this . elementsToLoad. get ( path) as LoadChildrenCallback;
+ const loadedAndRegistered =
+ ( modulePathLoader ( ) as Promise < Type< WithCustomElementComponent>> )
+ . then ( elementModule => {
+ const elementModuleRef = createNgModuleRef ( elementModule, this . moduleRef. injector) ;
+ const injector = elementModuleRef. injector;
+ const CustomElementComponents = elementModuleRef. instance. customElementComponents;
+ return Promise . all ( CustomElementComponents. map ( comp => {
+ const selector = ( comp as any ) . ɵcmp. selectors[ 0 ] [ 0 ] ;
+ const CustomElement = createCustomElement ( comp, { injector} ) ;
+ customElements. define ( selector, CustomElement) ;
+ return customElements. whenDefined ( selector) ;
+ } ) ) ;
+
+ } )
+ . then ( ( ) => {
+ // The custom element has been successfully loaded and registered.
+ // Remove from `elementsLoading` and `elementsToLoad`.
+ this . elementsLoading. delete ( path) ;
+ this . elementsToLoad. delete ( path) ;
+ } )
+ . catch ( err => {
+ // The custom element has failed to load and register.
+ // Remove from `elementsLoading`.
+ // (Do not remove from `elementsToLoad` in case it was a temporary error.)
+ this . elementsLoading. delete ( path) ;
+ return Promise . reject ( err) ;
+ } ) ;
+
+ this . elementsLoading. set ( path, loadedAndRegistered) ;
+ return loadedAndRegistered;
+ }
+
+ // The custom element has already been loaded and registered.
+ return Promise . resolve ( ) ;
+ }
+
+
+}
+
\ No newline at end of file
diff --git a/src/api/docs/demos/field-with-prefix-and-suffix.component.html.html b/src/api/docs/demos/field-with-prefix-and-suffix.component.html.html
index f156ae585..ad9899bb1 100644
--- a/src/api/docs/demos/field-with-prefix-and-suffix.component.html.html
+++ b/src/api/docs/demos/field-with-prefix-and-suffix.component.html.html
@@ -16,7 +16,7 @@
type = " string"
value = " example"
>
- < span lySuffix > @gmail.com</ span>
+ < span lySuffix > @ gmail.com</ span>
< ly-label> Email</ ly-label>
< ly-hint> Hint</ ly-hint>
</ ly-field>
diff --git a/src/api/docs/demos/grid-demo-responsive.component.html.html b/src/api/docs/demos/grid-demo-responsive.component.html.html
index bf481dcc2..f5b3d4eb2 100644
--- a/src/api/docs/demos/grid-demo-responsive.component.html.html
+++ b/src/api/docs/demos/grid-demo-responsive.component.html.html
@@ -4,22 +4,22 @@
</ ly-grid>
< ly-grid item col = " 6 12@XSmall" >
- < div [className] = " classes.item" > col=6 12@XSmall</ div>
+ < div [className] = " classes.item" > col=6 12@ XSmall</ div>
</ ly-grid>
< ly-grid item col = " 6 12@XSmall" >
- < div [className] = " classes.item" > col=6 12@XSmall</ div>
+ < div [className] = " classes.item" > col=6 12@ XSmall</ div>
</ ly-grid>
< ly-grid item col = " 3 6@XSmall@Small" >
- < div [className] = " classes.item" > col=3 6@XSmall@Small</ div>
+ < div [className] = " classes.item" > col=3 6@ XSmall@ Small</ div>
</ ly-grid>
< ly-grid item col = " 3 6@XSmall@Small" >
- < div [className] = " classes.item" > col=3 6@XSmall@Small</ div>
+ < div [className] = " classes.item" > col=3 6@ XSmall@ Small</ div>
</ ly-grid>
< ly-grid item col = " 3 6@XSmall@Small" >
- < div [className] = " classes.item" > col=3 6@XSmall@Small</ div>
+ < div [className] = " classes.item" > col=3 6@ XSmall@ Small</ div>
</ ly-grid>
< ly-grid item col = " 3 6@XSmall@Small" >
- < div [className] = " classes.item" > col=3 6@XSmall@Small</ div>
+ < div [className] = " classes.item" > col=3 6@ XSmall@ Small</ div>
</ ly-grid>
</ ly-grid>
\ No newline at end of file
diff --git a/src/api/docs/getting-started/installation.html b/src/api/docs/getting-started/installation.html
index af8ed7128..386da5530 100644
--- a/src/api/docs/getting-started/installation.html
+++ b/src/api/docs/getting-started/installation.html
@@ -7,7 +7,7 @@ Installation
href="https://cli.angular.io/">Angular CLI and for an existing one follow the next steps.
- The most recent release of Alyle UI now offers compatibility with Angular 16.
+ The most recent release of Alyle UI now offers compatibility with Angular 17.
Angular CLI
Using with the Angular CLI command will update your Angular project so that it is ready to be used.
diff --git a/src/api/docs/guides/lazy-loading.html b/src/api/docs/guides/lazy-loading.html
deleted file mode 100644
index 039ff89ba..000000000
--- a/src/api/docs/guides/lazy-loading.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-Lazy Loading
-Since the gestures do not work in lazy-loading (as ly-carousel
), manually add the following:
-import { HAMMER_GESTURE_CONFIG , HammerModule } from '@angular/platform-browser' ;
-import { LyHammerGestureConfig } from '@alyle/ui' ;
-
-@ NgModule ( {
- ...
- imports: [
- ...
- HammerModule
- ] ,
- providers: [
- ...
- { provide: HAMMER_GESTURE_CONFIG , useClass: LyHammerGestureConfig }
- ]
-} )
-export class AppModule { }
-
\ No newline at end of file
diff --git a/src/api/docs/guides/migrating-to-alyle-ui-3.html b/src/api/docs/guides/migrating-to-alyle-ui-3.html
deleted file mode 100644
index 32ae23ef9..000000000
--- a/src/api/docs/guides/migrating-to-alyle-ui-3.html
+++ /dev/null
@@ -1,127 +0,0 @@
-
-Migrating to Alyle UI 3
-Alyle UI 3 has significant changes, so it requires some changes in its code.
-
- If you are new using the Alyle UI, ignore this.
-
-Using @alyle/ui/color
instead chroma.js
-Alyle UI components no longer use Chroma js, instead use color
. color
is an Alyle UI library, with basic functions to manipulate color.
-Now the colors defined in the themes are no longer a strings, to define a color you can use color
.
-Before:
-export class CustomMinimaLight implements PartialThemeVariables {
- name = 'minima-light' ;
- primary = {
- default : ' rgba(156, 39, 176, 0.94)' ,
- contrast: ' #fff'
- } ;
- accent = {
- default : ' #e91e63' ,
- contrast: ' #fff'
- } ;
-}
-
-After:
-export class CustomMinimaLight implements PartialThemeVariables {
- name = 'minima-light' ;
- primary = {
- default : new Color ( 156 , 39 , 176 , 0.94 ) ,
- contrast: new Color ( 0xffffff )
- } ;
- accent = {
- default : new Color ( 0xe91e63 )
- contrast: new Color ( 0xffffff )
- } ;
-}
-
-Dynamic Styles
-The new feature of Alyle UI 3, is that now styles support template string.
-For example, before styles were objects:
-const STYLES = ( theme: ThemeVariables) => {
- return {
- root: {
- color: ' blue' ,
- [ '{active}' ] : {
- color: ' red'
- }
- } ,
- active: null
- }
-} ;
-
-Now it can be done this way:
-const STYLES = ( theme: ThemeVariables, ref: ThemeRef) => {
- const __ = ref. selectorsOf ( STYLES ) ; // --> {root: '.root-a', active: '.active-b'}
- return {
- root : ( ) => lyl ` {
- color : blue
- ${ __. active} {
- color : red
- }
- } ` ,
- active: null
- }
-} ;
-
-Let's see another example:
-Before:
-import { LyTheme2, ThemeVariables } from '@alyle/ui' ;
-
-const styles = ( theme: ThemeVariables) => ( {
- '@global' : {
- body: {
- backgroundColor: theme. background. default,
- color: theme. text. default,
- fontFamily: theme. typography. fontFamily,
- margin: 0 ,
- direction: theme. direction
- }
- }
-} ) ;
-
-After:
-import { LyTheme2, lyl, ThemeVariables } from '@alyle/ui' ;
-
-const styles = ( theme: ThemeVariables) => ( {
- $global: lyl ` {
- body {
- background-color : ${ theme. background. default}
- color : ${ theme. text. default}
- font-family : ${ theme. typography. fontFamily}
- margin : 0
- direction : ${ theme. direction}
- }
- } `
-} ) ;
-
-
- Note that the properties of the CSS statements are of the kebab-case format.
-
-Renaming Image cropper
-The Image Cropper had a bad name and was very long. That's why we now rename LyResizingCroppingImages
to LyImageCropper
& @alyle/ui/resizing-cropping-images
to @alyle/ui/image-cropper
. The selector can be used both < ly-img-cropper>
and < ly-image-cropper>
-LyResponsibleModule
is deprecated
-LyResponsibleModule
is deprecated, use LyCommonModule
instead, more details here .
-Rename [width]
and [height]
of Drawer
-Since there is a directive with the inputs width
and height
this brings a conflict with the drawer component, therefore, it is necessary to rename these entries as seen below.
-From [width]
to [drawerWidth]
and from [height]
to [drawerHeight]
.
-before:
-< ly-drawer ly-paper
- [width] = " ' 230px 0@XSmall'"
- bg = " background:primary"
- [withClass] = " classes.drawer"
- [opened] = " currentRoutePath !== ''"
- [mode] = " currentRoutePath !== '' ? 'side' : 'over'"
- [spacingAbove] = " currentRoutePath !== '' ? '64px 56px@XSmall' : 0"
->
-
-after:
-< ly-drawer ly-paper
- [drawerWidth] = " ' 230px 0@XSmall'"
- bg = " background:primary"
- [withClass] = " classes.drawer"
- [opened] = " currentRoutePath !== ''"
- [mode] = " currentRoutePath !== '' ? 'side' : 'over'"
- [spacingAbove] = " currentRoutePath !== '' ? '64px 56px@XSmall' : 0"
->
-
-Questions?
-Do you have questions? join the chat at Discord
\ No newline at end of file
diff --git a/src/app/docs/components/image-cropper-demo/cropper-basic-usage/cropper-basic-usage.component.html b/src/app/docs/components/image-cropper-demo/cropper-basic-usage/cropper-basic-usage.component.html
index e3e5a0f91..fddcc34e2 100644
--- a/src/app/docs/components/image-cropper-demo/cropper-basic-usage/cropper-basic-usage.component.html
+++ b/src/app/docs/components/image-cropper-demo/cropper-basic-usage/cropper-basic-usage.component.html
@@ -7,7 +7,7 @@
rotate_90_degrees_ccw
-
Drag and drop image
-
+
crop crop
diff --git a/src/app/docs/components/image-cropper-demo/cropper-basic-usage/cropper-basic-usage.component.ts b/src/app/docs/components/image-cropper-demo/cropper-basic-usage/cropper-basic-usage.component.ts
index f6e82aece..e18965de8 100644
--- a/src/app/docs/components/image-cropper-demo/cropper-basic-usage/cropper-basic-usage.component.ts
+++ b/src/app/docs/components/image-cropper-demo/cropper-basic-usage/cropper-basic-usage.component.ts
@@ -1,19 +1,17 @@
import { Component, ChangeDetectionStrategy, ViewChild } from '@angular/core';
-import { StyleRenderer, lyl, ThemeVariables, SelectorsFn } from '@alyle/ui';
+import { StyleRenderer, lyl, ThemeVariables } from '@alyle/ui';
import {
ImgCropperConfig,
ImgCropperEvent,
- LyImageCropper,
+ LyImageCropperBase,
ImgCropperErrorEvent,
- STYLES as CROPPER_STYLES
} from '@alyle/ui/image-cropper';
-const STYLES = (_theme: ThemeVariables, selectors: SelectorsFn) => {
- const cropper = selectors(CROPPER_STYLES);
+const STYLES = (_theme: ThemeVariables) => {
return {
root: lyl `{
- ${cropper.root} {
+ .ly-cropper-root {
aspect-ratio: 3 / 2
max-width: 600px
}
@@ -38,7 +36,7 @@ export class CropperBasicUsageComponent {
$$ = this.sRenderer.renderSheet(STYLES, 'root');
croppedImage?: string | null = null;
ready = false;
- @ViewChild(LyImageCropper) readonly cropper!: LyImageCropper;
+ @ViewChild(LyImageCropperBase) readonly cropper!: LyImageCropperBase;
myConfig: ImgCropperConfig = {
width: 200, // Default `250`
height: 200, // Default `200`
diff --git a/src/app/docs/components/image-cropper-demo/cropper-basic-usage/cropper-basic-usage.module.ts b/src/app/docs/components/image-cropper-demo/cropper-basic-usage/cropper-basic-usage.module.ts
index 96ba63a8d..fe946a6fd 100644
--- a/src/app/docs/components/image-cropper-demo/cropper-basic-usage/cropper-basic-usage.module.ts
+++ b/src/app/docs/components/image-cropper-demo/cropper-basic-usage/cropper-basic-usage.module.ts
@@ -1,7 +1,7 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CropperBasicUsageComponent } from './cropper-basic-usage.component';
-import { LyImageCropperModule } from '@alyle/ui/image-cropper';
+import { LyImageCropperBaseModule } from '@alyle/ui/image-cropper';
import { LyButtonModule } from '@alyle/ui/button';
import { LyIconModule } from '@alyle/ui/icon';
@@ -13,7 +13,7 @@ import { LyIconModule } from '@alyle/ui/icon';
],
imports: [
CommonModule,
- LyImageCropperModule,
+ LyImageCropperBaseModule,
LyButtonModule,
LyIconModule
]
diff --git a/src/app/docs/components/image-cropper-demo/cropper-with-dialog/cropper-dialog.html b/src/app/docs/components/image-cropper-demo/cropper-with-dialog/cropper-dialog.html
index cd80f17b9..202bdfc9c 100644
--- a/src/app/docs/components/image-cropper-demo/cropper-with-dialog/cropper-dialog.html
+++ b/src/app/docs/components/image-cropper-demo/cropper-with-dialog/cropper-dialog.html
@@ -10,7 +10,7 @@
1:1
-
Drag and drop image
-
+
diff --git a/src/app/docs/components/image-cropper-demo/cropper-with-dialog/cropper-dialog.ts b/src/app/docs/components/image-cropper-demo/cropper-with-dialog/cropper-dialog.ts
index 979b14d66..ca38b4ded 100644
--- a/src/app/docs/components/image-cropper-demo/cropper-with-dialog/cropper-dialog.ts
+++ b/src/app/docs/components/image-cropper-demo/cropper-with-dialog/cropper-dialog.ts
@@ -3,8 +3,7 @@ import { StyleRenderer, WithStyles, lyl, ThemeRef, ThemeVariables } from '@alyle
import { LyDialogRef, LY_DIALOG_DATA } from '@alyle/ui/dialog';
import { LySliderChange, STYLES as SLIDER_STYLES } from '@alyle/ui/slider';
import {
- STYLES as CROPPER_STYLES,
- LyImageCropper,
+ LyImageCropperBase,
ImgCropperConfig,
ImgCropperEvent,
ImgCropperErrorEvent,
@@ -13,13 +12,11 @@ import {
const STYLES = (_theme: ThemeVariables, ref: ThemeRef) => {
ref.renderStyleSheet(SLIDER_STYLES);
- ref.renderStyleSheet(CROPPER_STYLES);
const slider = ref.selectorsOf(SLIDER_STYLES);
- const cropper = ref.selectorsOf(CROPPER_STYLES);
return {
root: lyl `{
- ${cropper.root} {
+ .ly-cropper-root {
max-width: 320px
height: 320px
}
@@ -55,7 +52,7 @@ export class CropperDialog implements WithStyles, AfterViewInit {
scale: number;
minScale: number;
maxScale: number;
- @ViewChild(LyImageCropper, { static: true }) cropper: LyImageCropper;
+ @ViewChild(LyImageCropperBase, { static: true }) cropper: LyImageCropperBase;
myConfig: ImgCropperConfig = {
width: 150,
height: 150,
diff --git a/src/app/docs/components/image-cropper-demo/cropper-with-dialog/cropper-with-dialog.module.ts b/src/app/docs/components/image-cropper-demo/cropper-with-dialog/cropper-with-dialog.module.ts
index 5f8745bbb..668f35b43 100644
--- a/src/app/docs/components/image-cropper-demo/cropper-with-dialog/cropper-with-dialog.module.ts
+++ b/src/app/docs/components/image-cropper-demo/cropper-with-dialog/cropper-with-dialog.module.ts
@@ -1,7 +1,7 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
-import { LyImageCropperModule } from '@alyle/ui/image-cropper';
+import { LyImageCropperBaseModule } from '@alyle/ui/image-cropper';
import { LySliderModule } from '@alyle/ui/slider';
import { LyButtonModule } from '@alyle/ui/button';
import { LyIconModule } from '@alyle/ui/icon';
@@ -20,7 +20,7 @@ import { CropperDialog } from './cropper-dialog';
imports: [
CommonModule,
FormsModule,
- LyImageCropperModule,
+ LyImageCropperBaseModule,
LySliderModule,
LyButtonModule,
LyIconModule,
diff --git a/src/app/docs/components/image-cropper-demo/image-cropper.md b/src/app/docs/components/image-cropper-demo/image-cropper.md
index 5c87f65b4..70c1b5191 100644
--- a/src/app/docs/components/image-cropper-demo/image-cropper.md
+++ b/src/app/docs/components/image-cropper-demo/image-cropper.md
@@ -15,7 +15,7 @@ The Angular Image Cropper allows users to effortlessly resize, rotate, and crop
Add the
to your template:
```html
-
to your template:
(error)="onError($event)"
>
Drag and drop image
-
+
` event.
+ */
+ maxFileSize?: number | null;
+ /**
+ * Whether the cropper area will be round.
+ * This implies that the cropper area will maintain its aspect ratio.
+ * default: false
+ */
+ round?: boolean;
+ /**
+ * Whether the cropper area is resizable.
+ * default: false
+ */
+ resizableArea?: boolean;
+ /**
+ * Keep the width and height of the growing area the same according
+ * to `ImgCropperConfig.width` and `ImgCropperConfig.height`
+ * default: false
+ */
+ keepAspectRatio?: boolean;
+ /**
+ * Whether the cropper area is responsive.
+ * By default, the width and height of the cropper area is fixed,
+ * so can use when the cropper area is larger than its container,
+ * otherwise this will bring problems when cropping.
+ */
+ responsiveArea?: boolean;
+
+}
+
+/**
+ * The output image
+ * With this option you can resize the output image.
+ * If `width` or `height` are 0, this will be set automatically.
+ * Both cannot be 0.
+ */
+export interface ImgOutput {
+ /**
+ * The cropped image will be resized to this `width`.
+ */
+ width: number;
+ /**
+ * Cropped image will be resized to this `height`.
+ */
+ height: number;
+}
+
+/** Image output */
+export enum ImgResolution {
+ /**
+ * The output image will be equal to the initial size of the cropper area.
+ */
+ Default,
+ /** Just crop the image without resizing */
+ OriginalImage
+}
+
+export enum PointerChange {
+ Down,
+ Up
+}
+
+/** Image output */
+export enum ImgCropperError {
+ /** The loaded image exceeds the size limit set. */
+ Size,
+ /** The file loaded is not image. */
+ Type,
+ /** When the image has not been loaded. */
+ Other
+}
+
+export interface ImgCropperEvent {
+ /** Cropped image data URL */
+ dataURL?: string;
+ name: string | null;
+ /** Filetype */
+ type?: string;
+ /** Cropped area width */
+ areaWidth: number;
+ /** Cropped area height */
+ areaHeight: number;
+ /** Cropped image width */
+ width: number;
+ /** Cropped image height */
+ height: number;
+ /** Original Image data URL */
+ originalDataURL?: string;
+ scale: number;
+ /** Current rotation in degrees */
+ rotation: number;
+ /** Size of the image in bytes */
+ size: number;
+ /** Scaled offset from the left edge of the image */
+ left: number;
+ /** Scaled offset from the top edge of the image */
+ top: number;
+ /**
+ * Scaled offset from the left edge of the image to center of area
+ * Can be used to set image position
+ */
+ xOrigin: number;
+ /**
+ * Scaled offset from the top edge of the image to center of area
+ * Can be used to set image position
+ */
+ yOrigin: number;
+ /** @deprecated Use `xOrigin & yOrigin` instead. */
+ position?: {
+ x: number
+ y: number
+ };
+}
+
+export interface ImgCropperErrorEvent {
+ name?: string;
+ /** Size of the image in bytes */
+ size: number;
+ /** Filetype */
+ type: string;
+ /** Type of error */
+ error: ImgCropperError;
+ errorMsg?: string;
+}
+
+interface ImgRect {
+ x: number;
+ y: number;
+ xc: number;
+ yc: number;
+ /** transform with */
+ wt: number;
+ ht: number;
+}
+
+export interface ImgCropperLoaderConfig {
+ name?: string | null;
+ /** Filetype */
+ type?: string;
+ /** Cropped area width */
+ areaWidth?: number;
+ /** Cropped area height */
+ areaHeight?: number;
+ /** Cropped image width */
+ width?: number;
+ /** Cropped image height */
+ height?: number;
+ /** Original Image data URL */
+ originalDataURL?: string;
+ /** Accept File or Blob */
+ file?: File;
+ scale?: number;
+ /** Current rotation in degrees */
+ rotation?: number;
+ /** Size of the image in bytes */
+ size?: number;
+ /** Offset from the left edge of the image to center of area */
+ xOrigin?: number;
+ /** Offset from the top edge of the image to center of area */
+ yOrigin?: number;
+}
+
+@Directive()
+export class _LyImageCropperBase implements OnInit, AfterViewInit, OnDestroy {
+ static readonly и = 'LyImageCropper';
+ private _currentLoadConfig?: ImgCropperLoaderConfig;
+
+ /** Original image */
+ private _img: HTMLImageElement;
+ private startTransform?: {
+ x: number
+ y: number
+ xOrigin: number
+ yOrigin: number
+ };
+ private _scale?: number;
+ private _scal3Fix?: number;
+ private _minScale?: number;
+ private _maxScale?: number;
+ /** Initial config */
+ private _initialConfig: ImgCropperConfig;
+ private _config: ImgCropperConfig;
+ private _imgRect: ImgRect = {} as any;
+ private _rotation: number = 0;
+ // private _sizeInBytes: number | null;
+ private _isSliding: boolean;
+ private _isMultiTouching: boolean;
+ private _startCenter: { x: number, y: number };
+ private _startDistance: number;
+ /** Keeps track of the last pointer event that was captured by the crop area. */
+ private _lastPointerEvent: MouseEvent | TouchEvent | null;
+ private _startPointerEvent: {
+ x: number
+ y: number
+ } | null;
+ _initialAreaWidth: number;
+ _initialAreaHeight: number;
+ _areaWidthResized: number | null;
+ _areaHeightResized: number | null;
+
+ _absoluteScale: number;
+
+ /**
+ * When is loaded image
+ * @internal
+ */
+ _isLoadedImg: boolean;
+ _urlsToRevoke: string[] = [];
+
+ /** When is loaded image & ready for crop */
+ isLoaded: boolean;
+ /** When the cropper is ready to be interacted */
+ isReady: boolean;
+ isCropped: boolean;
+ /** @private */
+ readonly _isPointerUp = new BehaviorSubject(true);
+
+ @ViewChild('_cropperContainer', { static: true }) _cropperContainer: ElementRef;
+ @ViewChild('_imgContainer', { static: true }) _imgContainer: ElementRef;
+ @ViewChild('_area', {
+ read: ElementRef
+ }) _areaRef: ElementRef;
+ @ViewChild('_imgCanvas', { static: true }) _imgCanvas: ElementRef;
+ @Input()
+ get config(): ImgCropperConfig {
+ return this._config;
+ }
+ set config(val: ImgCropperConfig) {
+ this._config = {...{}, ...new ImgCropperConfig(), ...val};
+ this._initialConfig = {...{}, ...this._config};
+ this._initialAreaWidth = this.config.width;
+ this._initialAreaHeight = this.config.height;
+ if (
+ this._config.round
+ && this.config.width !== this.config.height
+ ) {
+ throw new Error(`${_LyImageCropperBase.и}: Both width and height must be equal when using \`ImgCropperConfig.round = true\``);
+ }
+ const maxFileSize = this._config.maxFileSize;
+ if (maxFileSize) {
+ this.maxFileSize = maxFileSize;
+ }
+ }
+
+ /** Set scale */
+ @Input()
+ get scale(): number | undefined {
+ return this._scale;
+ }
+ set scale(val: number | undefined) {
+ this.setScale(val);
+ }
+
+ /**
+ * Emit event `error` if the file size for the limit.
+ * Note: It only works when the image is received from the ` ` event.
+ */
+ @Input() maxFileSize: number;
+
+ /** Get min scale */
+ get minScale(): number | undefined {
+ return this._minScale;
+ }
+
+ @Output() readonly scaleChange = new EventEmitter();
+
+ /** Emits minimum supported image scale */
+ @Output('minScale') readonly minScaleChange = new EventEmitter();
+
+ /** Emits maximum supported image scale */
+ @Output('maxScale') readonly maxScaleChange = new EventEmitter();
+
+ /** @deprecated Emits when the image is loaded, instead use `ready` */
+ @Output() readonly loaded = new EventEmitter();
+
+ /** Emits when the image is loaded */
+ @Output() readonly imageLoaded = new EventEmitter();
+
+ /** Emits when the cropper is ready to be interacted */
+ @Output() readonly ready = new EventEmitter();
+
+ /** On crop new image */
+ @Output() readonly cropped = new EventEmitter();
+
+ /** Emits when the cropper is cleaned */
+ @Output() readonly cleaned = new EventEmitter();
+
+ /** Emit an error when the loaded image is not valid */
+ // tslint:disable-next-line: no-output-native
+ @Output() readonly error = new EventEmitter();
+
+ private _currentInputElement?: HTMLInputElement;
+
+ /** Emits whenever the component is destroyed. */
+ private readonly _destroy = new Subject();
+
+ /** Used to subscribe to global move and end events */
+ protected _document: Document;
+ private _rotateTimeOut: any;
+ private _pendingRotation = 0;
+ protected areaGridActiveCssClass = '';
+
+ constructor(
+ private _renderer: Renderer2,
+ readonly _elementRef: ElementRef,
+ private cd: ChangeDetectorRef,
+ private _ngZone: NgZone,
+ @Inject(DOCUMENT) _document: any,
+ viewPortRuler: ViewportRuler
+ ) {
+ this._document = _document;
+ viewPortRuler.change()
+ .pipe(takeUntil(this._destroy))
+ .subscribe(() =>
+ this._ngZone.run(() => this.updateCropperPosition())
+ );
+ this._isPointerUp.subscribe((UP) => {
+ if (!this.areaGridActiveCssClass) {
+ return;
+ }
+ if (UP) {
+ this._renderer.removeClass(this._elementRef.nativeElement, this.areaGridActiveCssClass);
+ } else {
+ this._renderer.addClass(this._elementRef.nativeElement, this.areaGridActiveCssClass);
+ }
+ });
+ }
+
+ ngOnInit() {
+ this._ngZone.runOutsideAngular(() => {
+ const element = this._imgContainer.nativeElement;
+ element.addEventListener('mousedown', this._pointerDown, activeEventOptions);
+ element.addEventListener('touchstart', this._pointerDown, activeEventOptions);
+ });
+ }
+
+ ngAfterViewInit() {
+ this._addEventsToScaleWithScroll();
+ }
+
+ ngOnDestroy() {
+ this._destroy.next();
+ this._destroy.complete();
+ const element = this._imgContainer.nativeElement;
+ const host = this._elementRef.nativeElement;
+ this._lastPointerEvent = null;
+ this._removeGlobalEvents();
+ element.removeEventListener('mousedown', this._pointerDown, activeEventOptions);
+ element.removeEventListener('touchstart', this._pointerDown, activeEventOptions);
+ host.removeEventListener('wheel', this._onWheel, activeEventOptions);
+ }
+
+ /** Load image with canvas */
+ // private _imgLoaded(imgElement: HTMLImageElement) {
+ // if (imgElement) {
+ // this._img = imgElement;
+ // const canvas = this._imgCanvas.nativeElement;
+ // canvas.width = imgElement.width;
+ // canvas.height = imgElement.height;
+ // const ctx = canvas.getContext('2d')!;
+ // ctx.clearRect(0, 0, imgElement.width, imgElement.height);
+ // ctx.drawImage(imgElement, 0, 0);
+
+ // /** set min scale */
+ // this._updateMinScale(canvas);
+ // this._updateMaxScale();
+ // }
+ // }
+
+ /** Load image with canvas */
+ private _loadImageToCanvas(imgElement: HTMLImageElement) {
+ if (imgElement) {
+ this._img = imgElement;
+ const canvas = this._imgCanvas.nativeElement;
+ canvas.width = imgElement.width;
+ canvas.height = imgElement.height;
+ const ctx = canvas.getContext('2d')!;
+ ctx.clearRect(0, 0, imgElement.width, imgElement.height);
+ ctx.drawImage(imgElement, 0, 0);
+ }
+ }
+
+
+ private _setStylesForContImg(values: {
+ x?: number, y?: number
+ }) {
+ const newStyles = { } as any;
+ if (values.x != null && values.y != null) {
+ const rootRect = this._cropperContainerRect();
+ const x = rootRect.width / 2 - (values.x);
+ const y = rootRect.height / 2 - (values.y);
+
+ this._imgRect.x = (values.x);
+ this._imgRect.y = (values.y);
+ this._imgRect.xc = (x);
+ this._imgRect.yc = (y);
+
+ }
+
+ newStyles.transform = `translate3d(${(this._imgRect.x)}px,${(this._imgRect.y)}px, 0)`;
+ newStyles.transform += ` scale(${this._scal3Fix})`;
+ newStyles.transform += ` rotate(0)`;
+ newStyles.transformOrigin = `${this._imgRect.xc}px ${this._imgRect.yc}px 0`;
+ for (const key in newStyles) {
+ if (newStyles.hasOwnProperty(key)) {
+ this._renderer.setStyle(this._imgContainer.nativeElement, key, newStyles[key]);
+ }
+ }
+ }
+
+ /**
+ * Update area and image position only if needed,
+ * this is used when window resize
+ */
+ updateCropperPosition() {
+ if (this.isLoaded) {
+ this.updatePosition();
+ this._updateAreaIfNeeded();
+ }
+ }
+
+ /** Load Image from input event */
+ selectInputEvent(img: Event) {
+ this.clean();
+ this._currentInputElement = img.target as HTMLInputElement;
+ const _img = img.target as HTMLInputElement;
+ if (_img.files && _img.files.length !== 1) {
+ return;
+ }
+ const fileSize = _img.files![0].size;
+ const fileName = _img.value.replace(/.*(\/|\\)/, '');
+
+ if (this.maxFileSize && fileSize > this.maxFileSize) {
+ const cropEvent: ImgCropperErrorEvent = {
+ name: fileName,
+ type: _img.files![0].type,
+ size: fileSize,
+ error: ImgCropperError.Size
+ };
+ this.clean();
+ this.error.emit(cropEvent as ImgCropperErrorEvent);
+ return;
+ }
+ new Observable(observer => {
+
+ const reader = new FileReader();
+
+ reader.onerror = err => observer.error(err);
+ reader.onabort = err => observer.error(err);
+ reader.onload = (ev) => setTimeout(() => {
+ observer.next(ev);
+ observer.complete();
+ });
+
+ reader.readAsDataURL(_img.files![0]);
+ })
+ .pipe(take(1), takeUntil(this._destroy))
+ .subscribe(
+ (loadEvent) => {
+ const originalDataURL = (loadEvent.target as FileReader).result as string;
+ this.loadImage({
+ name: fileName,
+ size: _img.files![0].size, // size in bytes
+ type: this.config.type || _img.files![0].type,
+ originalDataURL,
+ file: _img.files![0]
+ });
+
+ this.cd.markForCheck();
+ },
+ () => {
+ const cropEvent: ImgCropperErrorEvent = {
+ name: fileName,
+ size: fileSize,
+ error: ImgCropperError.Other,
+ errorMsg: 'The File could not be loaded.',
+ type: _img.files![0].type
+ };
+ this.clean();
+ this.error.emit(cropEvent as ImgCropperErrorEvent);
+ }
+ );
+
+ }
+
+ /** Set the size of the image, the values can be 0 between 1, where 1 is the original size */
+ setScale(size?: number, noAutoCrop?: boolean) {
+ // fix min scale
+ // const newSize = size! >= this.minScale! && size! <= 1 ? size : this.minScale;
+ const newSize = (size! >= this.minScale!) ? size : this.minScale;
+
+
+ // check
+ const changed = size != null && size !== this.scale && newSize !== this.scale;
+ this._scale = size;
+ if (!changed) {
+ return;
+ }
+ this._scal3Fix = newSize;
+ this._updateAbsoluteScale();
+ // this._updateMinScale();
+ // this._updateMaxScale(); // TODO: check this
+ if (this.isLoaded) {
+ if (changed) {
+ const originPosition = {...this._imgRect};
+ this.startTransform = {
+ x: originPosition.x,
+ y: originPosition.y,
+ xOrigin: originPosition.xc,
+ yOrigin: originPosition.yc
+ };
+ this._setStylesForContImg({});
+ this._simulatePointerMove();
+ } else {
+ return;
+ }
+ } else if (this.minScale) {
+ this._setStylesForContImg({
+ ...this._getCenterPoints()
+ });
+ } else {
+ return;
+ }
+
+ this.scaleChange.emit(size);
+ if (!noAutoCrop) {
+ this._cropIfAutoCrop();
+ }
+
+ }
+
+ private _getCenterPoints() {
+ const root = this._cropperContainer.nativeElement as HTMLElement;
+ const img = this._imgCanvas.nativeElement;
+ const x = (root.offsetWidth - (img.width)) / 2;
+ const y = (root.offsetHeight - (img.height)) / 2;
+ return {
+ x,
+ y
+ };
+ }
+
+ /**
+ * Fit to screen
+ */
+ fitToScreen() {
+ const container = this._cropperContainer.nativeElement as HTMLElement;
+ const min = {
+ width: container.offsetWidth,
+ height: container.offsetHeight
+ };
+ const { width, height } = this._img;
+ const minScale = {
+ width: min.width / width,
+ height: min.height / height
+ };
+ const result = Math.max(minScale.width, minScale.height);
+ this.setScale(result);
+ }
+
+ fit() {
+ this.setScale(this.minScale);
+ }
+
+ private _pointerDown = (event: TouchEvent | MouseEvent) => {
+ this._isPointerUp.next(false);
+ if (isTouchEvent(event)) {
+ if (event.touches.length === 2) {
+ this._isMultiTouching = isTouchEvent(event);
+
+ // Calculate where the fingers have started on the X and Y axis
+ this._startCenter = getCenterFromTouchEvent(event);
+ this._startDistance = calDistanceFromTouchEvent(event);
+ }
+ }
+ // Don't do anything if the
+ // user is using anything other than the main mouse button.
+ if (this._isSliding || (!isTouchEvent(event) && event.button !== 0)) {
+ return;
+ }
+
+ this._ngZone.run(() => {
+
+
+ this._isSliding = true;
+
+ this.startTransform = {
+ x: this._imgRect.x,
+ y: this._imgRect.y,
+ xOrigin: this._imgRect.xc,
+ yOrigin: this._imgRect.yc
+ };
+ this._lastPointerEvent = event;
+ this._startPointerEvent = getGesturePointFromEvent(event);
+ event.preventDefault(); // Prevent page scroll
+ this._bindGlobalEvents(event);
+ });
+
+ }
+
+ /**
+ * Simulate pointerMove with clientX = 0 and clientY = 0,
+ * this is used by `setScale` and `rotate`
+ */
+ private _simulatePointerMove() {
+ this._isSliding = true;
+ this._startPointerEvent = {
+ x: 0,
+ y: 0
+ };
+ this._pointerMove({
+ clientX: 0,
+ clientY: 0,
+ type: 'n',
+ preventDefault: () => {}
+ } as MouseEvent);
+ this._isSliding = false;
+ this._startPointerEvent = null;
+ }
+
+ _markForCheck() {
+ this.cd.markForCheck();
+ }
+
+ /**
+ * Called when the user has moved their pointer after
+ * starting to drag.
+ */
+ private _pointerMove = (event: TouchEvent | MouseEvent) => {
+ if (this._isSliding) {
+ event.preventDefault();
+ this._lastPointerEvent = event;
+ let x: number | undefined, y: number | undefined;
+ let scaleFix = this._scal3Fix;
+ const config = this.config;
+ const startP = this.startTransform;
+ if (!scaleFix || !startP) {
+ return;
+ }
+ const point = getGesturePointFromEvent(event);
+ let deltaX = 0;
+ let deltaY = 0;
+ const imgContainer = this._imgCanvas.nativeElement;
+ /** Main image width */
+ const imgWidth = imgContainer.width;
+ /** Main image height */
+ const imgHeight = imgContainer.height;
+
+ if (this._isMultiTouching) {
+ const newScale = calDistanceFromTouchEvent(event as TouchEvent) / this._startDistance * this._scale!;
+ scaleFix = this._scal3Fix = Math.max(newScale, this.minScale!);
+ const center = getCenterFromTouchEvent(event as TouchEvent);
+ deltaX = center.x - this._startCenter.x;
+ deltaY = center.y - this._startCenter.y;
+ } else {
+ deltaX = point.x - this._startPointerEvent!.x;
+ deltaY = point.y - this._startPointerEvent!.y;
+ }
+
+ if (!scaleFix || !startP) {
+ return;
+ }
+
+ const isMinScaleX = imgWidth * scaleFix < config.width && config.extraZoomOut;
+ const isMinScaleY = imgHeight * scaleFix < config.height && config.extraZoomOut;
+
+ const limitLeft = (config.width / 2 / scaleFix) >= startP.xOrigin - (deltaX / scaleFix);
+ const limitRight = (config.width / 2 / scaleFix) + (imgWidth) - (startP.xOrigin - (deltaX / scaleFix)) <= config.width / scaleFix;
+ const limitTop = ((config.height / 2 / scaleFix) >= (startP.yOrigin - (deltaY / scaleFix)));
+ const limitBottom = (
+ ((config.height / 2 / scaleFix) + (imgHeight) - (startP.yOrigin - (deltaY / scaleFix))) <= (config.height / scaleFix)
+ );
+
+ // Limit for left
+ if ((limitLeft && !isMinScaleX) || (!limitLeft && isMinScaleX)) {
+ x = startP.x + (startP.xOrigin) - (config.width / 2 / scaleFix);
+ }
+
+ // Limit for right
+ if ((limitRight && !isMinScaleX) || (!limitRight && isMinScaleX)) {
+ x = startP.x + (startP.xOrigin) + (config.width / 2 / scaleFix) - imgWidth;
+ }
+
+ // Limit for top
+ if ((limitTop && !isMinScaleY) || (!limitTop && isMinScaleY)) {
+ y = startP.y + (startP.yOrigin) - (config.height / 2 / scaleFix);
+ }
+
+ // Limit for bottom
+ if ((limitBottom && !isMinScaleY) || (!limitBottom && isMinScaleY)) {
+ y = startP.y + (startP.yOrigin) + (config.height / 2 / scaleFix) - imgHeight;
+ }
+
+ // When press shiftKey, deprecated
+ // if (event.srcEvent && event.srcEvent.shiftKey) {
+ // if (Math.abs(event.deltaX) === Math.max(Math.abs(event.deltaX), Math.abs(event.deltaY))) {
+ // y = this.offset.top;
+ // } else {
+ // x = this.offset.left;
+ // }
+ // }
+
+ if (x === void 0) { x = (deltaX / scaleFix) + (startP.x); }
+ if (y === void 0) { y = (deltaY / scaleFix) + (startP.y); }
+
+
+
+ this._setStylesForContImg({
+ x, y,
+ });
+ }
+ }
+
+
+ updatePosition(): void;
+ updatePosition(xOrigin: number, yOrigin: number): void;
+ updatePosition(xOrigin?: number, yOrigin?: number) {
+ const hostRect = this._cropperContainerRect();
+ const areaRect = this._areaCropperRect();
+ const areaWidth = Math.min(areaRect.width, hostRect.width);
+ const areaHeight = Math.min(areaRect.height, hostRect.height);
+ let x: number, y: number;
+ if (xOrigin == null && yOrigin == null) {
+ xOrigin = this._imgRect.xc;
+ yOrigin = this._imgRect.yc;
+ }
+ x = Math.max((areaRect.left - hostRect.left), 0);
+ y = Math.max(0, (areaRect.top - hostRect.top));
+ x -= (xOrigin! - (areaWidth / 2));
+ y -= (yOrigin! - (areaHeight / 2));
+
+ this._setStylesForContImg({
+ x, y
+ });
+ }
+
+ _slideEnd() {
+ this._cropIfAutoCrop();
+ }
+
+ private _cropIfAutoCrop() {
+ if (this.config.autoCrop) {
+ this.crop();
+ }
+ }
+
+ /** + */
+ zoomIn() {
+ const scalePercent = this._calcScalePercent(this._scal3Fix!, this._minScale!, this._maxScale!);
+ const newScale = scalePercent < 20 ? ((this._scal3Fix! * this._imgCanvas.nativeElement.width + 50)
+ / (this._imgCanvas.nativeElement.width)) : (this._scal3Fix! + .05);
+
+ if (newScale > this.minScale! && newScale <= this._maxScale!) {
+ this.setScale(newScale);
+ } else {
+ this.setScale(this._maxScale!);
+ }
+ }
+
+ private _calcScalePercent(num: number, min: number, max: number) {
+ return ((num - min) / (max - min)) * 100;
+ }
+
+
+ /** Clean the img cropper */
+ clean() {
+ // fix choosing the same image does not load
+ if (this._currentInputElement) {
+ this._currentInputElement.value = '';
+ this._currentInputElement = null!;
+ }
+ if (this.isLoaded) {
+ this._imgRect = { } as any;
+ this.startTransform = undefined;
+ this.scale = undefined as any;
+ this._scal3Fix = undefined;
+ this._rotation = 0;
+ this._minScale = undefined;
+ this._isLoadedImg = false;
+ this.isLoaded = false;
+ this.isCropped = false;
+ this._currentLoadConfig = undefined;
+ this.config = this._initialConfig;
+ const canvas = this._imgCanvas.nativeElement;
+ canvas.width = 0;
+ canvas.height = 0;
+ this.cleaned.emit(null!);
+ this.cd.markForCheck();
+ }
+ }
+
+ /** - */
+ zoomOut() {
+ const scalePercent = this._calcScalePercent(this._scal3Fix!, this._minScale!, this._maxScale!);
+ const newScale = scalePercent < 20 ? ((this._scal3Fix! * this._imgCanvas.nativeElement.width - 50)
+ / (this._imgCanvas.nativeElement.width)) : (this._scal3Fix! - .05);
+ if (newScale > this.minScale! && newScale <= this._maxScale!) {
+ this.setScale(newScale);
+ } else {
+ this.fit();
+ }
+ }
+ center() {
+ const newStyles = {
+ ...this._getCenterPoints()
+ };
+ this._setStylesForContImg(newStyles);
+ this._cropIfAutoCrop();
+ }
+ /**
+ * load an image from a given configuration,
+ * or from the result of a cropped image
+ */
+ loadImage(config: ImgCropperLoaderConfig | string, fn?: () => void) {
+ this.clean();
+ const _config = this._currentLoadConfig = typeof config === 'string'
+ ? { originalDataURL: config }
+ : { ...config };
+
+ let src = _config.originalDataURL;
+ if (!src) {
+ const err: ImgCropperErrorEvent = {
+ name: _config.name!,
+ error: ImgCropperError.Other,
+ type: _config.type!,
+ size: _config.size!
+ };
+ this.error.emit(err);
+ return;
+ }
+ this._initialAreaWidth = this._initialConfig.width;
+ this._initialAreaHeight = this._initialConfig.height;
+ if (_config.areaWidth && _config.areaHeight) {
+ this.config.width = _config.areaWidth;
+ this.config.height = _config.areaHeight;
+ }
+ if (_config.originalDataURL && isSvgImage(_config.originalDataURL)) {
+ src = normalizeSVG(_config.originalDataURL);
+ }
+
+
+ const cropEvent = { ..._config } as ImgCropperEvent;
+
+ new Promise((resolve: (value: HTMLImageElement) => void, reject) => {
+ const img = createHtmlImg(src!);
+ this._ngZone.runOutsideAngular(() => {
+ img.onerror = err => reject(err);
+ img.onabort = err => reject(err);
+ img.onload = () => resolve(img);
+ });
+ })
+ .then(
+ (img) => {
+ this._loadImageToCanvas(img);
+ this._updateMinScale(img);
+ this._updateMaxScale();
+ this._isLoadedImg = true;
+ this.imageLoaded.emit(cropEvent);
+ this.cd.markForCheck();
+ Promise.resolve(null).then(() => {
+ setTimeout(() => {
+ this._ngZone.run(() => {this._positionImg(cropEvent, fn)});
+ });
+ });
+ },
+ () => {
+ const err: ImgCropperErrorEvent = {
+ name: _config.name!,
+ error: ImgCropperError.Type,
+ type: _config.type!,
+ size: _config.size!
+ };
+ this.error.emit(err);
+ }
+ );
+ }
+
+ private _updateAreaIfNeeded() {
+ if (!this._config.responsiveArea) return;
+
+ const rootRect = this._cropperContainerRect();
+ const areaRect = this._areaCropperRect();
+ const { minWidth = 1, minHeight = 1, round } = this.config;
+
+ // Calculate maximum dimensions allowed for the area
+ const maxWidth = Math.max(rootRect.width, minWidth);
+ const maxHeight = Math.max(rootRect.height, minHeight);
+
+ let newWidth = areaRect.width;
+ let newHeight = areaRect.height;
+
+ if (round) {
+ newWidth = newHeight = Math.min(maxWidth, maxHeight); // Área cuadrada si round es true
+ } else {
+ const shouldResizeWidth = (
+ areaRect.width > maxWidth
+ || areaRect.width < this._initialAreaWidth
+ || this._areaWidthResized
+ );
+ const shouldResizeHeight = (
+ areaRect.height > maxHeight
+ || areaRect.height < this._initialAreaHeight
+ || this._areaHeightResized
+ );
+
+ let posibleWidth_w = 0;
+ let posibleWidth_h = 0;
+ let posibleHeight_h = 0;
+ let posibleHeight_w = 0;
+
+ if (shouldResizeWidth) {
+ newWidth = posibleWidth_w = Math.min(maxWidth, this._areaWidthResized || this._initialAreaWidth);
+ newHeight = posibleWidth_h = (newWidth * areaRect.height) / areaRect.width;
+ }
+
+ if (shouldResizeHeight) {
+ newHeight = posibleHeight_h = Math.min(maxHeight, this._areaHeightResized || this._initialAreaHeight);
+ newWidth = posibleHeight_w = (newHeight * areaRect.width) / areaRect.height;
+ }
+
+ if (shouldResizeHeight && shouldResizeWidth) {
+ if (Math.min(posibleWidth_w, posibleWidth_h) < Math.min(posibleHeight_h, posibleHeight_w)) {
+ newWidth = posibleWidth_w;
+ newHeight = posibleWidth_h;
+ } else {
+ newHeight = posibleHeight_h;
+ newWidth = posibleHeight_w;
+ }
+ }
+ }
+
+ // Apply changes if dimensions have changed
+ if (newWidth !== this.config.width || newHeight !== this.config.height) {
+ const newScale = (this._scal3Fix! * newWidth) / areaRect.width;
+ this.config.width = newWidth;
+ this.config.height = newHeight;
+ this._updateMinScale();
+ this._updateMaxScale();
+ this.setScale(newScale, true);
+ this._markForCheck();
+ }
+ }
+
+ /**
+ * @private
+ */
+ _updateAbsoluteScale() {
+ const scale = this._scal3Fix! / (this.config.width / this._initialAreaWidth);
+ this._absoluteScale = scale;
+ }
+
+ /**
+ * Load Image from URL
+ * @deprecated Use `loadImage` instead of `setImageUrl`
+ * @param src URL
+ * @param fn function that will be called before emit the event loaded
+ */
+ setImageUrl(src: string, fn?: () => void) {
+ this.loadImage(src, fn);
+ }
+
+ private _positionImg(cropEvent: ImgCropperEvent, fn?: () => void) {
+ const loadConfig = this._currentLoadConfig!;
+ this._updateMinScale(this._imgCanvas.nativeElement);
+ this._updateMaxScale();
+ this.isLoaded = false;
+ if (fn) {
+ fn();
+ } else {
+ if (loadConfig.scale) {
+ this.setScale(loadConfig.scale, true);
+ } else {
+ this.setScale(this.minScale, true);
+ }
+ // this.rotate(loadConfig.rotation || 0);
+ this._updateAreaIfNeeded();
+ this._markForCheck();
+ this._ngZone.runOutsideAngular(() => {
+ Promise.resolve(null).then(() => {
+ if (loadConfig.xOrigin != null && loadConfig.yOrigin != null) {
+ this.updatePosition(loadConfig.xOrigin, loadConfig.yOrigin);
+ }
+ this._updateAreaIfNeeded();
+ this.isLoaded = true;
+ this._cropIfAutoCrop();
+ this._ngZone.run(() => {
+ this._markForCheck();
+ this.ready.emit(cropEvent);
+ // tslint:disable-next-line: deprecation
+ this.loaded.emit(cropEvent);
+ });
+ });
+ });
+ }
+ }
+
+ rotate2(degrees: number) {
+ this._rotation -= degrees;
+ const newStyles = {} as any;
+ newStyles.transform = `translate(${(this._imgRect.x)}px,${(this._imgRect.y)}px)`;
+ newStyles.transform += ` scale(${this._scal3Fix})`;
+ newStyles.transform += ` rotate(${this._rotation}deg)`;
+ newStyles.transformOrigin = `${this._imgRect.xc}px ${this._imgRect.yc}px 0`;
+ for (const key in newStyles) {
+ if (newStyles.hasOwnProperty(key)) {
+ this._renderer.setStyle(this._imgContainer.nativeElement, key, newStyles[key]);
+ }
+ }
+ }
+
+ rotate(degrees: number) {
+ _normalizeDegrees(degrees)
+ const newRotation = this._rotation + degrees;
+ clearTimeout(this._rotateTimeOut);
+ this._pendingRotation += degrees;
+ this._rotation = newRotation;
+ this._rotateWithAnimation();
+ this._rotateTimeOut = setTimeout(() => {
+ this._setStylesForContImg({});
+ this._rotateCanvas();
+ }, 140);
+ }
+
+ private _rotateWithAnimation() {
+
+ const newStyles = {} as any;
+ // easeOutQuad
+ newStyles.transition = `cubic-bezier(0.250, 0.460, 0.450, 0.940) 140ms`;
+ newStyles.transform = `translate(${(this._imgRect.x)}px,${(this._imgRect.y)}px)`;
+ newStyles.transform += ` scale(${this._scal3Fix})`;
+ newStyles.transform += ` rotate(${this._pendingRotation}deg)`;
+ newStyles.transformOrigin = `${this._imgRect.xc}px ${this._imgRect.yc}px 0`;
+ for (const key in newStyles) {
+ if (newStyles.hasOwnProperty(key)) {
+ this._renderer.setStyle(this._imgContainer.nativeElement, key, newStyles[key]);
+ }
+ }
+ }
+
+
+ private _rotateCanvas() {
+ let validDegrees = this._pendingRotation;
+
+ const degreesRad = validDegrees * Math.PI / 180;
+ const canvas = this._imgCanvas.nativeElement;
+ canvas.removeAttribute('style');
+ this._pendingRotation = 0;
+ this._renderer.removeStyle(this._imgContainer.nativeElement, 'transition');
+ const canvasClon = createCanvasImg(canvas);
+ const ctx = canvas.getContext('2d')!;
+
+ // clear
+ ctx.clearRect(0, 0, canvasClon.width, canvasClon.height);
+
+ // rotate canvas image
+ const transform = `rotate(${validDegrees}deg) scale(${1 / this._scal3Fix!})`;
+ const transformOrigin = `${this._imgRect.xc}px ${this._imgRect.yc}px 0`;
+ canvas.style.transform = transform;
+ canvas.style.opacity = '0';
+ // tslint:disable-next-line: deprecation
+ canvas.style.webkitTransform = transform;
+ canvas.style.transformOrigin = transformOrigin;
+ // tslint:disable-next-line: deprecation
+ canvas.style.webkitTransformOrigin = transformOrigin;
+
+ const { left, top } = canvas.getBoundingClientRect() as DOMRect;
+
+ // save rect
+ const canvasRect = canvas.getBoundingClientRect();
+
+ // remove rotate styles
+ canvas.removeAttribute('style');
+
+ // set w & h
+ const w = canvasRect.width;
+ const h = canvasRect.height;
+ ctx.canvas.width = w;
+ ctx.canvas.height = h;
+
+ // clear
+ ctx.clearRect(0, 0, w, h);
+
+ // translate and rotate
+ ctx.translate(w / 2, h / 2);
+ ctx.rotate(degreesRad);
+ ctx.drawImage(canvasClon, -canvasClon.width / 2, -canvasClon.height / 2);
+
+ // Update min scale
+ this._updateMinScale(canvas);
+ this._updateMaxScale();
+
+ // set the minimum scale, only if necessary
+ if (this.scale! < this.minScale!) {
+ this.setScale(0, true);
+ } // ↑ no AutoCrop
+
+ const rootRect = this._cropperContainerRect();
+
+ this._setStylesForContImg({
+ x: (left - rootRect.left),
+ y: (top - rootRect.top)
+ });
+
+ // keep image inside the frame
+ const originPosition = {...this._imgRect};
+ this.startTransform = {
+ x: originPosition.x,
+ y: originPosition.y,
+ xOrigin: originPosition.xc,
+ yOrigin: originPosition.yc
+ };
+ this._setStylesForContImg({});
+ this._simulatePointerMove();
+
+ this._cropIfAutoCrop();
+ }
+
+ _updateMinScale(canvas?: HTMLCanvasElement | HTMLImageElement) {
+ if (!canvas) {
+ canvas = this._imgCanvas.nativeElement;
+ }
+ const config = this.config;
+ const minScale = (config.extraZoomOut ? Math.min : Math.max)(
+ config.width / canvas.width,
+ config.height / canvas.height);
+ this._minScale = minScale;
+ this.minScaleChange.emit(minScale!);
+ }
+
+ /**
+ * @private
+ */
+ _updateMaxScale() {
+ const maxScale = (this.config.width / this._initialAreaWidth) * 3;
+ this._maxScale = maxScale;
+ this.maxScaleChange.emit(maxScale);
+ }
+
+ /**
+ * Resize & crop image
+ */
+ crop(config?: ImgCropperConfig): ImgCropperEvent {
+ const newConfig = config
+ ? {...{ }, ...(this.config || new ImgCropperConfig()), ...config} : this.config;
+ // this._loadImageToCanvas(this._mainImage.nativeElement);
+ const cropEvent = this._imgCrop(newConfig);
+ this.cd.markForCheck();
+ return cropEvent;
+ }
+
+ /**
+ * @docs-private
+ */
+ private _imgCrop(myConfig: ImgCropperConfig) {
+ const canvasElement: HTMLCanvasElement = document.createElement('canvas');
+ const areaRect = this._areaCropperRect();
+ const canvasRect = this._canvasRect();
+ const scaleFix = this._scal3Fix!;
+ const left = (areaRect.left - canvasRect.left) / scaleFix;
+ const top = (areaRect.top - canvasRect.top) / scaleFix;
+ const { output } = myConfig;
+ const currentImageLoadConfig = this._currentLoadConfig!;
+ const area = {
+ width: myConfig.width,
+ height: myConfig.height
+ };
+ canvasElement.width = area.width / scaleFix;
+ canvasElement.height = area.height / scaleFix;
+ const ctx = canvasElement.getContext('2d')!;
+ if (myConfig.fill) {
+ ctx.fillStyle = myConfig.fill;
+ ctx.fillRect(0, 0, canvasElement.width, canvasElement.height);
+ }
+ // crop
+ ctx.drawImage(this._imgCanvas.nativeElement,
+ -(left), -(top),
+ );
+
+ const result = canvasElement;
+ // TODO: check if the image to be sized is smaller than the crop area
+ if (myConfig.output === ImgResolution.Default) {
+ const areaWidth = this._areaWidthResized ?? this._initialConfig.width;
+ const areaHeight = this._areaHeightResized ?? this._initialConfig.height;
+ resizeCanvas(
+ result,
+ areaWidth,
+ areaHeight);
+ } else if (typeof output === 'object') {
+ if (output.width && output.height) {
+ resizeCanvas(result, output.width, output.height);
+ } else if (output.width) {
+ const newHeight = area.height * output.width / area.width;
+ resizeCanvas(result, output.width, newHeight);
+ } else if (output.height) {
+ const newWidth = area.width * output.height / area.height;
+ resizeCanvas(result, newWidth, output.height);
+ }
+ }
+ const type = currentImageLoadConfig.originalDataURL?.startsWith('http')
+ ? currentImageLoadConfig.type || myConfig.type
+ : myConfig.type || currentImageLoadConfig.type!;
+ const dataURL = result.toDataURL(type);
+ const cropEvent: ImgCropperEvent = {
+ dataURL,
+ type,
+ name: currentImageLoadConfig.name!,
+ areaWidth: this._initialAreaWidth,
+ areaHeight: this._initialAreaHeight,
+ width: result.width,
+ height: result.height,
+ originalDataURL: currentImageLoadConfig.originalDataURL,
+ scale: this._absoluteScale!,
+ rotation: this._rotation,
+ left: (areaRect.left - canvasRect.left) / this._scal3Fix!,
+ top: (areaRect.top - canvasRect.top) / this._scal3Fix!,
+ size: currentImageLoadConfig.size!,
+ xOrigin: this._imgRect.xc,
+ yOrigin: this._imgRect.yc,
+ position: {
+ x: this._imgRect.xc,
+ y: this._imgRect.yc
+ }
+ };
+
+ this.isCropped = true;
+ this.cropped.emit(cropEvent);
+ return cropEvent;
+ }
+
+ // _rootRect(): DOMRect {
+ // return this._elementRef.nativeElement.getBoundingClientRect() as DOMRect;
+ // }
+
+ _cropperContainerRect(): DOMRect {
+ return this._cropperContainer.nativeElement.getBoundingClientRect() as DOMRect;
+ }
+
+ _areaCropperRect(): DOMRect {
+ return this._areaRef.nativeElement.getBoundingClientRect() as DOMRect;
+ }
+
+ _canvasRect(): DOMRect {
+ return this._imgCanvas.nativeElement.getBoundingClientRect();
+ }
+
+ // _mainImageRect(): DOMRect {
+ // return this._mainImage.nativeElement.getBoundingClientRect();
+ // }
+
+
+ /** Called when the user has lifted their pointer. */
+ private _pointerUp = (event: TouchEvent | MouseEvent) => {
+ this._isPointerUp.next(true);
+ if (this._isSliding) {
+ event.preventDefault();
+ this._removeGlobalEvents();
+ this._isSliding = false;
+ this._isMultiTouching = false;
+ this._startPointerEvent = null;
+ this._scale = this._scal3Fix;
+ this._cropIfAutoCrop();
+ }
+ }
+
+ /** Called when the window has lost focus. */
+ private _windowBlur = () => {
+ // If the window is blurred while dragging we need to stop dragging because the
+ // browser won't dispatch the `mouseup` and `touchend` events anymore.
+ if (this._lastPointerEvent) {
+ this._pointerUp(this._lastPointerEvent);
+ }
+ }
+
+ private _bindGlobalEvents(triggerEvent: TouchEvent | MouseEvent) {
+ const element = this._document;
+ const isTouch = isTouchEvent(triggerEvent);
+ const moveEventName = isTouch ? 'touchmove' : 'mousemove';
+ const endEventName = isTouch ? 'touchend' : 'mouseup';
+ element.addEventListener(moveEventName, this._pointerMove, activeEventOptions);
+ element.addEventListener(endEventName, this._pointerUp, activeEventOptions);
+
+ if (isTouch) {
+ element.addEventListener('touchcancel', this._pointerUp, activeEventOptions);
+ }
+
+ const window = this._getWindow();
+
+ if (typeof window !== 'undefined' && window) {
+ window.addEventListener('blur', this._windowBlur);
+ }
+ }
+
+ /** Removes any global event listeners that we may have added. */
+ private _removeGlobalEvents() {
+ const element = this._document;
+ element.removeEventListener('mousemove', this._pointerMove, activeEventOptions);
+ element.removeEventListener('mouseup', this._pointerUp, activeEventOptions);
+ element.removeEventListener('touchmove', this._pointerMove, activeEventOptions);
+ element.removeEventListener('touchend', this._pointerUp, activeEventOptions);
+ element.removeEventListener('touchcancel', this._pointerUp, activeEventOptions);
+
+ const window = this._getWindow();
+
+ if (typeof window !== 'undefined' && window) {
+ window.removeEventListener('blur', this._windowBlur);
+ }
+ }
+
+ private _addEventsToScaleWithScroll() {
+ this._ngZone.runOutsideAngular(() => {
+ const element = this._elementRef!.nativeElement;
+ element.addEventListener('wheel', this._onWheel, activeEventOptions);
+ });
+ }
+
+ private _onWheel = (event: WheelEvent) => {
+ if (!this.isLoaded) {
+ return;
+ }
+ event.preventDefault();
+ this._ngZone.run(() => {
+ if (Math.sign(event.deltaY) < 0) {
+ this.zoomIn();
+ } else {
+ this.zoomOut();
+ }
+ });
+
+ }
+
+ /** Use defaultView of injected document if available or fallback to global window reference */
+ private _getWindow(): Window {
+ return this._document.defaultView || window;
+ }
+
+}
+
+/**
+ * @dynamic
+ */
+@Directive()
+export class _LyCropperAreaBase implements OnDestroy {
+
+ private _isSliding: boolean;
+ /** Keeps track of the last pointer event that was captured by the crop area. */
+ private _lastPointerEvent: MouseEvent | TouchEvent | null;
+ private _startPointerEvent: {
+ x: number
+ y: number
+ } | null;
+ private _currentWidth: number | null;
+ private _currentHeight: number | null;
+ private _startAreaRect: DOMRect;
+ private _startImgRect: DOMRect;
+
+ /** Used to subscribe to global move and end events */
+ protected _document: Document;
+
+ @ViewChild('resizer') readonly _resizer?: ElementRef;
+
+ @Input()
+ set resizableArea(val: boolean) {
+ if (val !== this._resizableArea) {
+ this._resizableArea = val;
+ Promise.resolve(null).then(() => {
+ if (val) {
+ this._removeResizableArea();
+ this._addResizableArea();
+ } else {
+ this._removeResizableArea();
+ }
+ });
+ }
+ }
+ get resizableArea() {
+ return this._resizableArea;
+ }
+ private _resizableArea: boolean;
+ @Input() keepAspectRatio: boolean;
+ @Input()
+ @Input({ transform: booleanAttribute }) round: boolean;
+ // @Style(
+ // (_value, _media) => ({ after }, selectors: SelectorsFn) => {
+ // const $$ = selectors(STYLES);
+ // return lyl `{
+ // border-radius: 50%
+ // ${$$.resizer} {
+ // ${after}: ${pos}%
+ // bottom: ${pos}%
+ // transform: translate(4px, 4px)
+ // }
+ // ${$$.grid} {
+ // border-radius: 50%
+ // overflow: hidden
+ // }
+ // }`;
+ // },
+ // coerceBooleanProperty
+ // ) round: BooleanInput;
+
+ constructor(
+ readonly _elementRef: ElementRef,
+ private _ngZone: NgZone,
+ readonly _cropper: _LyImageCropperBase,
+ @Inject(DOCUMENT) _document: any,
+ ) {
+ this._document = _document;
+ }
+
+ ngOnDestroy() {
+ this._removeResizableArea();
+ }
+
+ private _addResizableArea() {
+ this._ngZone.runOutsideAngular(() => {
+ const element = this._resizer!.nativeElement;
+ element.addEventListener('mousedown', this._pointerDown, activeEventOptions);
+ element.addEventListener('touchstart', this._pointerDown, activeEventOptions);
+ });
+ }
+
+ private _removeResizableArea() {
+ const element = this._resizer?.nativeElement;
+ if (element) {
+ this._lastPointerEvent = null;
+ this._removeGlobalEvents();
+ element.removeEventListener('mousedown', this._pointerDown, activeEventOptions);
+ element.removeEventListener('touchstart', this._pointerDown, activeEventOptions);
+ }
+ }
+
+ private _pointerDown = (event: MouseEvent | TouchEvent) => {
+ this._cropper._isPointerUp.next(false);
+ // Don't do anything if the
+ // user is using anything other than the main mouse button.
+ if (this._isSliding || (!isTouchEvent(event) && event.button !== 0)) {
+ return;
+ }
+
+ event.preventDefault();
+
+ this._ngZone.run(() => {
+ this._isSliding = true;
+ this._lastPointerEvent = event;
+ this._startPointerEvent = getGesturePointFromEvent(event);
+ this._startAreaRect = this._cropper._areaCropperRect();
+ this._startImgRect = this._cropper._canvasRect();
+ this._bindGlobalEvents(event);
+ });
+
+ }
+
+ private _pointerMove = (event: MouseEvent | TouchEvent) => {
+ if (this._isSliding) {
+ event.preventDefault();
+ this._lastPointerEvent = event;
+ const element: HTMLDivElement = this._elementRef.nativeElement;
+ const { width, height, minWidth, minHeight } = this._cropper.config;
+ const point = getGesturePointFromEvent(event);
+ const deltaX = point.x - this._startPointerEvent!.x;
+ const deltaY = point.y - this._startPointerEvent!.y;
+ const startAreaRect = this._startAreaRect;
+ const startImgRect = this._startImgRect;
+ const round = this.round;
+ const keepAspectRatio = this._cropper.config.keepAspectRatio || event.shiftKey;
+ let newWidth = 0;
+ let newHeight = 0;
+ const rootRect = this._cropper._cropperContainerRect();
+
+ if (round) {
+ // The distance from the center of the cropper area to the pointer
+ const originX = ((width / 2 / Math.sqrt(2)) + deltaX);
+ const originY = ((height / 2 / Math.sqrt(2)) + deltaY);
+
+ // Leg
+ const side = Math.sqrt(originX ** 2 + originY ** 2);
+ newWidth = newHeight = side * 2;
+
+ } else if (keepAspectRatio) {
+ newWidth = width + deltaX * 2;
+ newHeight = height + deltaY * 2;
+ if (width !== height) {
+ if (width > height) {
+ newHeight = height / (width / newWidth);
+ } else if (height > width) {
+ newWidth = width / (height / newHeight);
+ }
+ } else {
+ newWidth = newHeight = Math.max(newWidth, newHeight);
+ }
+ } else {
+ newWidth = width + deltaX * 2;
+ newHeight = height + deltaY * 2;
+ }
+
+ // To min width
+ if (newWidth < minWidth!) {
+ newWidth = minWidth!;
+ }
+ // To min height
+ if (newHeight < minHeight!) {
+ newHeight = minHeight!;
+ }
+
+ // Do not overflow the cropper area
+ const centerX = startAreaRect.x + startAreaRect.width / 2;
+ const centerY = startAreaRect.y + startAreaRect.height / 2;
+ const topOverflow = startImgRect.y > centerY - (newHeight / 2);
+ const bottomOverflow = centerY + (newHeight / 2) > startImgRect.bottom;
+ const minHeightOnOverflow = Math.min((centerY - startImgRect.y) * 2, (startImgRect.bottom - centerY) * 2);
+ const leftOverflow = startImgRect.x > centerX - (newWidth / 2);
+ const rightOverflow = centerX + (newWidth / 2) > startImgRect.right;
+ const minWidthOnOverflow = Math.min((centerX - startImgRect.x) * 2, (startImgRect.right - centerX) * 2);
+ const minOnOverflow = Math.min(minWidthOnOverflow, minHeightOnOverflow);
+ if (round) {
+ if (topOverflow || bottomOverflow || leftOverflow || rightOverflow) {
+ newHeight = newWidth = minOnOverflow;
+ }
+ } else if (keepAspectRatio) {
+ const newNewWidth: number[] = [];
+ const newNewHeight: number[] = [];
+ if ((topOverflow || bottomOverflow)) {
+ newHeight = minHeightOnOverflow;
+ newNewHeight.push(newHeight);
+ newWidth = width / (height / minHeightOnOverflow);
+ newNewWidth.push(newWidth);
+ }
+ if ((leftOverflow || rightOverflow)) {
+ newWidth = minWidthOnOverflow;
+ newNewWidth.push(newWidth);
+ newHeight = height / (width / minWidthOnOverflow);
+ newNewHeight.push(newHeight);
+ }
+ if (newNewWidth.length === 2) {
+ newWidth = Math.min(...newNewWidth);
+ }
+ if (newNewHeight.length === 2) {
+ newHeight = Math.min(...newNewHeight);
+ }
+ } else {
+ if (topOverflow || bottomOverflow) {
+ newHeight = minHeightOnOverflow;
+ }
+ if (leftOverflow || rightOverflow) {
+ newWidth = minWidthOnOverflow;
+ }
+ }
+
+ // Do not overflow the container
+ if (round) {
+ const min = Math.min(rootRect.width, rootRect.height);
+ if (newWidth > min) {
+ newWidth = newHeight = min;
+ } else if (newHeight > min) {
+ newWidth = newHeight = min;
+ }
+ } else if (keepAspectRatio) {
+ if (newWidth > rootRect.width) {
+ newWidth = rootRect.width;
+ newHeight = height / (width / rootRect.width);
+ }
+ if (newHeight > rootRect.height) {
+ newWidth = width / (height / rootRect.height);
+ newHeight = rootRect.height;
+ }
+ } else {
+ if (newWidth > rootRect.width) {
+ newWidth = rootRect.width;
+ }
+ if (newHeight > rootRect.height) {
+ newHeight = rootRect.height;
+ }
+ }
+
+
+ // round values
+ const newWidthRounded = Math.round(newWidth);
+ const newHeightRounded = Math.round(newHeight);
+
+ element.style.width = `${newWidthRounded}px`;
+ element.style.height = `${newHeightRounded}px`;
+ this._currentWidth = newWidthRounded;
+ this._currentHeight = newHeightRounded;
+ }
+ }
+
+ /** Called when the user has lifted their pointer. */
+ private _pointerUp = (event: TouchEvent | MouseEvent) => {
+ this._cropper._isPointerUp.next(true);
+ const hasChange = this._currentWidth !== this._cropper._initialAreaWidth
+ || this._currentHeight !== this._cropper._initialAreaHeight;
+
+ if (this._isSliding && (this._currentWidth != null)) {
+ event.preventDefault();
+ this._removeGlobalEvents();
+ this._cropper._areaWidthResized = hasChange ? this._currentWidth : null;
+ this._cropper._areaHeightResized = hasChange ? this._currentHeight : null;
+ // this._cropper._initialAreaWidth =
+ this._cropper.config.width = this._currentWidth!;
+ // this._cropper._initialAreaHeight =
+ this._cropper.config.height = this._currentHeight!;
+ this._cropper._updateMinScale();
+ this._cropper._updateMaxScale();
+ this._cropper._updateAbsoluteScale();
+ this._isSliding = false;
+ this._startPointerEvent = null;
+ this._currentWidth = null;
+ this._currentHeight = null;
+ this._cropper._markForCheck();
+ }
+ }
+
+ /** Called when the window has lost focus. */
+ private _windowBlur = () => {
+ // If the window is blurred while dragging we need to stop dragging because the
+ // browser won't dispatch the `mouseup` and `touchend` events anymore.
+ if (this._lastPointerEvent) {
+ this._pointerUp(this._lastPointerEvent);
+ }
+ }
+
+ private _bindGlobalEvents(triggerEvent: TouchEvent | MouseEvent) {
+ const element = this._document;
+ const isTouch = isTouchEvent(triggerEvent);
+ const moveEventName = isTouch ? 'touchmove' : 'mousemove';
+ const endEventName = isTouch ? 'touchend' : 'mouseup';
+ element.addEventListener(moveEventName, this._pointerMove, activeEventOptions);
+ element.addEventListener(endEventName, this._pointerUp, activeEventOptions);
+
+ if (isTouch) {
+ element.addEventListener('touchcancel', this._pointerUp, activeEventOptions);
+ }
+
+ const window = this._getWindow();
+
+ if (typeof window !== 'undefined' && window) {
+ window.addEventListener('blur', this._windowBlur);
+ }
+ }
+
+ /** Removes any global event listeners that we may have added. */
+ private _removeGlobalEvents() {
+ const element = this._document;
+ element.removeEventListener('mousemove', this._pointerMove, activeEventOptions);
+ element.removeEventListener('mouseup', this._pointerUp, activeEventOptions);
+ element.removeEventListener('touchmove', this._pointerMove, activeEventOptions);
+ element.removeEventListener('touchend', this._pointerUp, activeEventOptions);
+ element.removeEventListener('touchcancel', this._pointerUp, activeEventOptions);
+
+ const window = this._getWindow();
+
+ if (typeof window !== 'undefined' && window) {
+ window.removeEventListener('blur', this._windowBlur);
+ }
+ }
+
+ /** Use defaultView of injected document if available or fallback to global window reference */
+ private _getWindow(): Window {
+ return this._document.defaultView || window;
+ }
+}
+
+/**
+ * Normalize degrees for cropper rotation
+ * @docs-private
+ */
+export function _normalizeDegrees(n: number) {
+ const de = n % 360;
+ if (de % 90) {
+ throw new Error(`LyCropper: Invalid \`${n}\` degree, only accepted values: 0, 90, 180, 270 & 360.`);
+ }
+ return de;
+}
+
+/**
+ * @docs-private
+ */
+function createCanvasImg(img: HTMLCanvasElement | HTMLImageElement) {
+
+ // create a new canvas
+ const newCanvas = document.createElement('canvas');
+ const context = newCanvas.getContext('2d')!;
+
+ // set dimensions
+ newCanvas.width = img.width;
+ newCanvas.height = img.height;
+
+ // apply the old canvas to the new one
+ context.drawImage(img, 0, 0);
+
+ // return the new canvas
+ return newCanvas;
+}
+
+function normalizeSVG(dataURL: string) {
+ if (window.atob && isSvgImage(dataURL)) {
+ const len = dataURL.length / 5;
+ const text = window.atob(dataURL.replace(DATA_IMAGE_SVG_PREFIX, ''));
+ const span = document.createElement('span');
+ span.innerHTML = text;
+ const svg = span.querySelector('svg')!;
+ span.setAttribute('style', 'display:none');
+ document.body.appendChild(span);
+ const width = parseFloat(getComputedStyle(svg).width!) || 1;
+ const height = parseFloat(getComputedStyle(svg).height!) || 1;
+ const max = Math.max(width, height);
+
+ svg.setAttribute('width', `${len / (width / max)}px`);
+ svg.setAttribute('height', `${len / (height / max)}px`);
+ // const result = DATA_IMAGE_SVG_PREFIX + window.btoa(span.innerHTML);
+ document.body.removeChild(span);
+ const blob = new Blob([span.innerHTML], {type: 'image/svg+xml'});
+ return URL.createObjectURL(blob);
+ }
+ return dataURL;
+}
+
+function isSvgImage(dataUrl: string) {
+ return dataUrl.startsWith(DATA_IMAGE_SVG_PREFIX);
+}
+
+function createHtmlImg(src: string) {
+ const img = new Image();
+ img.crossOrigin = 'anonymous';
+ img.src = src;
+ return img;
+}
+
+function getGesturePointFromEvent(event: TouchEvent | MouseEvent) {
+
+ // `touches` will be empty for start/end events so we have to fall back to `changedTouches`.
+ const point = isTouchEvent(event)
+ ? (event.touches[0] || event.changedTouches[0])
+ : event;
+
+ return {
+ x: point.clientX,
+ y: point.clientY
+ };
+}
+
+
+/** Returns whether an event is a touch event. */
+function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent {
+ return event.type[0] === 't';
+}
+
+// Calculate distance between two fingers
+function calDistanceFromTouchEvent(event: TouchEvent) {
+ return Math.hypot(event.touches[0].clientX - event.touches[1].clientX, event.touches[0].clientY - event.touches[1].clientY);
+};
+
+function getCenterFromTouchEvent(event: TouchEvent) {
+ const x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
+ const y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
+ return { x, y }
+}
diff --git a/src/lib/image-cropper/image-cropper-area-base.html b/src/lib/image-cropper/image-cropper-area-base.html
new file mode 100644
index 000000000..bd92d25cc
--- /dev/null
+++ b/src/lib/image-cropper/image-cropper-area-base.html
@@ -0,0 +1,7 @@
+@if (resizableArea) {
+
+
+}
+
diff --git a/src/lib/image-cropper/image-cropper-area.html b/src/lib/image-cropper/image-cropper-area.html
index d98c43907..298a3cb30 100644
--- a/src/lib/image-cropper/image-cropper-area.html
+++ b/src/lib/image-cropper/image-cropper-area.html
@@ -3,4 +3,4 @@
[class]="classes.resizer"
>
-
\ No newline at end of file
+
diff --git a/src/lib/image-cropper/image-cropper-base.html b/src/lib/image-cropper/image-cropper-base.html
new file mode 100644
index 000000000..a7c398f3e
--- /dev/null
+++ b/src/lib/image-cropper/image-cropper-base.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+ @if (_isLoadedImg) {
+
+ } @else {
+
+
+
+
+ }
+
diff --git a/src/lib/image-cropper/image-cropper-base.scss b/src/lib/image-cropper/image-cropper-base.scss
new file mode 100644
index 000000000..dfc400766
--- /dev/null
+++ b/src/lib/image-cropper/image-cropper-base.scss
@@ -0,0 +1,162 @@
+// const pos = (100 * Math.sqrt(2) - 100) / 2 / Math.sqrt(2);
+$pos: 14.64%;
+
+.ly-cropper-root {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ display: flex;
+ overflow: hidden;
+ position: relative;
+ justify-content: center;
+ align-items: center;
+}
+.ly-cropper-container {
+ position: relative;
+ margin: auto;
+ width: 80%;
+ height: 80%;
+}
+
+
+.ly-cropper-img-container {
+ cursor: move;
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: flex;
+ touch-action: none;
+}
+.ly-cropper-img-container > canvas {
+ display: block;
+}
+
+
+.ly-cropper-area {
+ pointer-events: none;
+ box-shadow: 0 0 0 20000px rgba(0, 0, 0, 0.4);
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ margin: auto;
+}
+.ly-cropper-area-round {
+ border-radius: 50%;
+ &.ly-cropper-area-resizer {
+ right: $pos;
+ bottom: $pos;
+ transform: translate(4px, 4px);
+ }
+ &.ly-cropper-area-grid {
+ border-radius: 50%;
+ overflow: hidden
+ }
+}
+
+.ly-cropper-area:before,
+.ly-cropper-area:after {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ content: '';
+}
+.ly-cropper-area:before {
+ width: 0;
+ height: 0;
+ margin: auto;
+ border-radius: 50%;
+ background: #fff;
+ border: solid 2px rgb(255, 255, 255);
+}
+.ly-cropper-area:after {
+ border: solid 2px rgb(255, 255, 255);
+ border-radius: inherit;
+}
+
+
+.ly-cropper-area-grid {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ opacity: 0;
+ transition: cubic-bezier(0.4,0.0,1,1);
+ &::before,
+ &::after {
+ content: ' ';
+ box-sizing: border-box;
+ position: absolute;
+ border: 1px solid rgb(255 255 255 / 40%);
+ }
+ &::before {
+ top: 0;
+ bottom: 0;
+ left: 33.33%;
+ right: 33.33%;
+ border-top: 0;
+ border-bottom: 0;
+ }
+ &::after {
+ top: 33.33%;
+ bottom: 33.33%;
+ left: 0;
+ right: 0;
+ border-left: 0;
+ border-right: 0;
+ }
+}
+
+.ly-cropper-area-grid-active {
+ .ly-cropper-area-grid {
+ opacity: 1
+ }
+}
+
+.ly-cropper-area-resizer {
+ width: 10px;
+ height: 10px;
+ background: #fff;
+ border-radius: 3px;
+ position: absolute;
+ touch-action: none;
+ bottom: 0;
+ right: 0;
+ pointer-events: all;
+ cursor: nwse-resize;
+ &:before {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ content: '';
+ width: 30px;
+ height: 30px;
+ transform: translate(-35%, -35%);
+ }
+}
+.ly-cropper-default-content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.ly-cropper-default-content, .ly-cropper-default-content > input {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+}
+.ly-cropper-default-content *:not(input) {
+ pointer-events: none;
+}
+.ly-cropper-default-content > input {
+ background: transparent;
+ opacity: 0;
+ width: 100%;
+ height: 100%;
+}
\ No newline at end of file
diff --git a/src/lib/image-cropper/image-cropper-base.ts b/src/lib/image-cropper/image-cropper-base.ts
new file mode 100644
index 000000000..f0c82c773
--- /dev/null
+++ b/src/lib/image-cropper/image-cropper-base.ts
@@ -0,0 +1,46 @@
+import {
+ Component,
+ ChangeDetectionStrategy,
+ OnDestroy,
+ OnInit,
+ AfterViewInit,
+ ViewEncapsulation,
+} from '@angular/core';
+import { NgIf, NgStyle } from '@angular/common';
+import { _LyCropperAreaBase, _LyImageCropperBase } from './_image-cropper-base';
+
+/**
+ * @dynamic
+ */
+@Component({
+ selector: 'ly-cropper-area-base',
+ templateUrl: './image-cropper-area-base.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ exportAs: 'lyCropperAreaBase',
+ standalone: true,
+ imports: [NgIf],
+ host: {
+ 'class': 'ly-cropper-area'
+ }
+})
+export class LyCropperAreaBase extends _LyCropperAreaBase implements OnDestroy { }
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ selector: 'ly-img-cropper-base, ly-image-cropper-base',
+ templateUrl: 'image-cropper-base.html',
+ styleUrl: 'image-cropper-base.scss',
+ providers: [
+ {provide: _LyImageCropperBase, useExisting: LyImageCropperBase},
+ ],
+ standalone: true,
+ exportAs: 'lyImageCropperBase',
+ imports: [LyCropperAreaBase, NgStyle, NgIf],
+ host: {
+ 'class': 'ly-cropper-root'
+ },
+ encapsulation: ViewEncapsulation.None
+})
+export class LyImageCropperBase extends _LyImageCropperBase implements OnInit, AfterViewInit, OnDestroy {
+ override areaGridActiveCssClass = 'ly-cropper-area-grid-active';
+}
diff --git a/src/lib/image-cropper/image-cropper.module.ts b/src/lib/image-cropper/image-cropper.module.ts
index 89b48ec17..d04c79419 100644
--- a/src/lib/image-cropper/image-cropper.module.ts
+++ b/src/lib/image-cropper/image-cropper.module.ts
@@ -1,12 +1,17 @@
import { NgModule } from '@angular/core';
-import { CommonModule } from '@angular/common';
import { LyImageCropper, LyCropperArea } from './image-cropper';
+import { LyImageCropperBase } from './image-cropper-base';
@NgModule({
- imports: [CommonModule],
- exports: [LyImageCropper],
- declarations: [LyImageCropper, LyCropperArea]
+ imports: [LyImageCropper, LyCropperArea],
+ exports: [LyImageCropper, LyCropperArea]
})
export class LyImageCropperModule { }
+
+@NgModule({
+ imports: [LyImageCropperBase],
+ exports: [LyImageCropperBase]
+})
+export class LyImageCropperBaseModule { }
diff --git a/src/lib/image-cropper/image-cropper.ts b/src/lib/image-cropper/image-cropper.ts
index 250598462..41fc246e3 100644
--- a/src/lib/image-cropper/image-cropper.ts
+++ b/src/lib/image-cropper/image-cropper.ts
@@ -1,18 +1,12 @@
import {
Component,
- ElementRef,
Input,
- Output,
ChangeDetectionStrategy,
- ChangeDetectorRef,
- ViewChild,
- EventEmitter,
- Renderer2,
OnDestroy,
- NgZone,
- Inject,
OnInit,
- AfterViewInit
+ AfterViewInit,
+ inject,
+ booleanAttribute
} from '@angular/core';
import {
LY_COMMON_STYLES,
@@ -26,14 +20,10 @@ import {
Style,
SelectorsFn,
} from '@alyle/ui';
-import { Subject, Observable, BehaviorSubject } from 'rxjs';
-import { take, takeUntil } from 'rxjs/operators';
-import { normalizePassiveListenerOptions } from '@angular/cdk/platform';
-import { DOCUMENT } from '@angular/common';
-import { resizeCanvas } from './resize-canvas';
-import { ViewportRuler } from '@angular/cdk/scrolling';
-import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
+import { NgIf, NgStyle } from '@angular/common';
+import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { color } from '@alyle/ui/color';
+import { _LyCropperAreaBase, _LyImageCropperBase } from './_image-cropper-base';
export interface LyImageCropperTheme {
/** Styles for Image Cropper Component */
@@ -45,9 +35,7 @@ export interface LyImageCropperVariables {
cropper?: LyImageCropperTheme;
}
-const activeEventOptions = normalizePassiveListenerOptions({passive: false});
const STYLE_PRIORITY = -2;
-const DATA_IMAGE_SVG_PREFIX = 'data:image/svg+xml;base64,';
const pos = (100 * Math.sqrt(2) - 100) / 2 / Math.sqrt(2);
export const STYLES = (theme: ThemeVariables & LyImageCropperVariables, selectors: SelectorsFn) => {
@@ -199,1390 +187,6 @@ export const STYLES = (theme: ThemeVariables & LyImageCropperVariables, selector
};
};
-/** Image Cropper Config */
-export class ImgCropperConfig {
- /** Cropper area width */
- width: number = 250;
- /** Cropper area height */
- height: number = 200;
- minWidth?: number = 40;
- minHeight?: number = 40;
- /** If this is not defined, the new image will be automatically defined. */
- type?: string;
- /** Background color( default: null), if is null in png is transparent but not in jpg. */
- fill?: string | null;
- /**
- * Set anti-aliased (default: true)
- * @deprecated this is not necessary as the cropper will automatically resize the image
- * to the best quality
- */
- antiAliased?: boolean = true;
- autoCrop?: boolean;
- output?: ImgOutput | ImgResolution = ImgResolution.Default;
- /**
- * Zoom out until the entire image fits into the cropping area.
- * default: false
- */
- extraZoomOut?: boolean;
- /**
- * Zoom in until the entire image fits into the cropping area.
- * default: false
- */
- extraZoomIn?: boolean;
- /**
- * Emit event `error` if the file size in bytes for the limit.
- * Note: It only works when the image is received from the `
` event.
- */
- maxFileSize?: number | null;
- /**
- * Whether the cropper area will be round.
- * This implies that the cropper area will maintain its aspect ratio.
- * default: false
- */
- round?: boolean;
- /**
- * Whether the cropper area is resizable.
- * default: false
- */
- resizableArea?: boolean;
- /**
- * Keep the width and height of the growing area the same according
- * to `ImgCropperConfig.width` and `ImgCropperConfig.height`
- * default: false
- */
- keepAspectRatio?: boolean;
- /**
- * Whether the cropper area is responsive.
- * By default, the width and height of the cropper area is fixed,
- * so can use when the cropper area is larger than its container,
- * otherwise this will bring problems when cropping.
- */
- responsiveArea?: boolean;
-
-}
-
-/**
- * The output image
- * With this option you can resize the output image.
- * If `width` or `height` are 0, this will be set automatically.
- * Both cannot be 0.
- */
-export interface ImgOutput {
- /**
- * The cropped image will be resized to this `width`.
- */
- width: number;
- /**
- * Cropped image will be resized to this `height`.
- */
- height: number;
-}
-
-/** Image output */
-export enum ImgResolution {
- /**
- * The output image will be equal to the initial size of the cropper area.
- */
- Default,
- /** Just crop the image without resizing */
- OriginalImage
-}
-
-export enum PointerChange {
- Down,
- Up
-}
-
-/** Image output */
-export enum ImgCropperError {
- /** The loaded image exceeds the size limit set. */
- Size,
- /** The file loaded is not image. */
- Type,
- /** When the image has not been loaded. */
- Other
-}
-
-export interface ImgCropperEvent {
- /** Cropped image data URL */
- dataURL?: string;
- name: string | null;
- /** Filetype */
- type?: string;
- /** Cropped area width */
- areaWidth: number;
- /** Cropped area height */
- areaHeight: number;
- /** Cropped image width */
- width: number;
- /** Cropped image height */
- height: number;
- /** Original Image data URL */
- originalDataURL?: string;
- scale: number;
- /** Current rotation in degrees */
- rotation: number;
- /** Size of the image in bytes */
- size: number;
- /** Scaled offset from the left edge of the image */
- left: number;
- /** Scaled offset from the top edge of the image */
- top: number;
- /**
- * Scaled offset from the left edge of the image to center of area
- * Can be used to set image position
- */
- xOrigin: number;
- /**
- * Scaled offset from the top edge of the image to center of area
- * Can be used to set image position
- */
- yOrigin: number;
- /** @deprecated Use `xOrigin & yOrigin` instead. */
- position?: {
- x: number
- y: number
- };
-}
-
-export interface ImgCropperErrorEvent {
- name?: string;
- /** Size of the image in bytes */
- size: number;
- /** Filetype */
- type: string;
- /** Type of error */
- error: ImgCropperError;
- errorMsg?: string;
-}
-
-interface ImgRect {
- x: number;
- y: number;
- xc: number;
- yc: number;
- /** transform with */
- wt: number;
- ht: number;
-}
-
-export interface ImgCropperLoaderConfig {
- name?: string | null;
- /** Filetype */
- type?: string;
- /** Cropped area width */
- areaWidth?: number;
- /** Cropped area height */
- areaHeight?: number;
- /** Cropped image width */
- width?: number;
- /** Cropped image height */
- height?: number;
- /** Original Image data URL */
- originalDataURL?: string;
- /** Accept File or Blob */
- file?: File;
- scale?: number;
- /** Current rotation in degrees */
- rotation?: number;
- /** Size of the image in bytes */
- size?: number;
- /** Offset from the left edge of the image to center of area */
- xOrigin?: number;
- /** Offset from the top edge of the image to center of area */
- yOrigin?: number;
-}
-
-@Component({
- changeDetection: ChangeDetectionStrategy.OnPush,
- preserveWhitespaces: false,
- selector: 'ly-img-cropper, ly-image-cropper',
- templateUrl: 'image-cropper.html',
- providers: [
- StyleRenderer
- ],
-})
-export class LyImageCropper implements OnInit, AfterViewInit, OnDestroy {
- static readonly и = 'LyImageCropper';
- /**
- * styles
- * @docs-private
- */
- readonly classes = this.sRenderer.renderSheet(STYLES, true);
- private _currentLoadConfig?: ImgCropperLoaderConfig;
- // _originalImgBase64?: string;
- // private _fileName: string | null;
-
- /** Original image */
- private _img: HTMLImageElement;
- private startTransform?: {
- x: number
- y: number
- xOrigin: number
- yOrigin: number
- };
- private _scale?: number;
- private _scal3Fix?: number;
- private _minScale?: number;
- private _maxScale?: number;
- /** Initial config */
- private _initialConfig: ImgCropperConfig;
- private _config: ImgCropperConfig;
- private _imgRect: ImgRect = {} as any;
- private _rotation: number = 0;
- // private _sizeInBytes: number | null;
- private _isSliding: boolean;
- private _isMultiTouching: boolean;
- private _startCenter: { x: number, y: number };
- private _startDistance: number;
- /** Keeps track of the last pointer event that was captured by the crop area. */
- private _lastPointerEvent: MouseEvent | TouchEvent | null;
- private _startPointerEvent: {
- x: number
- y: number
- } | null;
- _initialAreaWidth: number;
- _initialAreaHeight: number;
- _areaWidthResized: number | null;
- _areaHeightResized: number | null;
-
- _absoluteScale: number;
-
- /**
- * When is loaded image
- * @internal
- */
- _isLoadedImg: boolean;
- _urlsToRevoke: string[] = [];
-
- /** When is loaded image & ready for crop */
- isLoaded: boolean;
- /** When the cropper is ready to be interacted */
- isReady: boolean;
- isCropped: boolean;
- /** @private */
- readonly _isPointerUp = new BehaviorSubject
(true);
-
- @ViewChild('_cropperContainer', { static: true }) _cropperContainer: ElementRef;
- @ViewChild('_imgContainer', { static: true }) _imgContainer: ElementRef;
- @ViewChild('_area', {
- read: ElementRef
- }) _areaRef: ElementRef;
- @ViewChild('_imgCanvas', { static: true }) _imgCanvas: ElementRef;
- // @ViewChild('_mainImage', { static: false }) _mainImage: ElementRef;
- @Input()
- get config(): ImgCropperConfig {
- return this._config;
- }
- set config(val: ImgCropperConfig) {
- this._config = {...{}, ...new ImgCropperConfig(), ...val};
- this._initialConfig = {...{}, ...this._config};
- this._initialAreaWidth = this.config.width;
- this._initialAreaHeight = this.config.height;
- if (
- this._config.round
- && this.config.width !== this.config.height
- ) {
- throw new Error(`${LyImageCropper.и}: Both width and height must be equal when using \`ImgCropperConfig.round = true\``);
- }
- const maxFileSize = this._config.maxFileSize;
- if (maxFileSize) {
- this.maxFileSize = maxFileSize;
- }
- }
-
- /** Set scale */
- @Input()
- get scale(): number | undefined {
- return this._scale;
- }
- set scale(val: number | undefined) {
- this.setScale(val);
- }
-
- /**
- * Emit event `error` if the file size for the limit.
- * Note: It only works when the image is received from the ` ` event.
- */
- @Input() maxFileSize: number;
-
- /** Get min scale */
- get minScale(): number | undefined {
- return this._minScale;
- }
-
- @Output() readonly scaleChange = new EventEmitter();
-
- /** Emits minimum supported image scale */
- @Output('minScale') readonly minScaleChange = new EventEmitter();
-
- /** Emits maximum supported image scale */
- @Output('maxScale') readonly maxScaleChange = new EventEmitter();
-
- /** @deprecated Emits when the image is loaded, instead use `ready` */
- @Output() readonly loaded = new EventEmitter();
-
- /** Emits when the image is loaded */
- @Output() readonly imageLoaded = new EventEmitter();
-
- /** Emits when the cropper is ready to be interacted */
- @Output() readonly ready = new EventEmitter();
-
- /** On crop new image */
- @Output() readonly cropped = new EventEmitter();
-
- /** Emits when the cropper is cleaned */
- @Output() readonly cleaned = new EventEmitter();
-
- /** Emit an error when the loaded image is not valid */
- // tslint:disable-next-line: no-output-native
- @Output() readonly error = new EventEmitter();
-
- private _currentInputElement?: HTMLInputElement;
-
- /** Emits whenever the component is destroyed. */
- private readonly _destroy = new Subject();
-
- /** Used to subscribe to global move and end events */
- protected _document: Document;
- private _rotateTimeOut: any;
- private _pendingRotation = 0;
-
- constructor(
- readonly sRenderer: StyleRenderer,
- private _renderer: Renderer2,
- readonly _elementRef: ElementRef,
- private cd: ChangeDetectorRef,
- private _ngZone: NgZone,
- @Inject(DOCUMENT) _document: any,
- viewPortRuler: ViewportRuler
- ) {
- this._document = _document;
- viewPortRuler.change()
- .pipe(takeUntil(this._destroy))
- .subscribe(() =>
- this._ngZone.run(() => this.updateCropperPosition())
- );
- this._isPointerUp.subscribe((UP) => {
- if (UP) {
- sRenderer.removeClass(this.classes.showGrid);
- sRenderer.addClass(this.classes.isPointerUp);
- } else {
- sRenderer.addClass(this.classes.showGrid);
- sRenderer.removeClass(this.classes.isPointerUp);
- }
- });
- }
-
- ngOnInit() {
- this._ngZone.runOutsideAngular(() => {
- const element = this._imgContainer.nativeElement;
- element.addEventListener('mousedown', this._pointerDown, activeEventOptions);
- element.addEventListener('touchstart', this._pointerDown, activeEventOptions);
- });
- }
-
- ngAfterViewInit() {
- this._addEventsToScaleWithScroll();
- }
-
- ngOnDestroy() {
- this._destroy.next();
- this._destroy.complete();
- const element = this._imgContainer.nativeElement;
- const host = this._elementRef.nativeElement;
- this._lastPointerEvent = null;
- this._removeGlobalEvents();
- element.removeEventListener('mousedown', this._pointerDown, activeEventOptions);
- element.removeEventListener('touchstart', this._pointerDown, activeEventOptions);
- host.removeEventListener('wheel', this._onWheel, activeEventOptions);
- }
-
- /** Load image with canvas */
- // private _imgLoaded(imgElement: HTMLImageElement) {
- // if (imgElement) {
- // this._img = imgElement;
- // const canvas = this._imgCanvas.nativeElement;
- // canvas.width = imgElement.width;
- // canvas.height = imgElement.height;
- // const ctx = canvas.getContext('2d')!;
- // ctx.clearRect(0, 0, imgElement.width, imgElement.height);
- // ctx.drawImage(imgElement, 0, 0);
-
- // /** set min scale */
- // this._updateMinScale(canvas);
- // this._updateMaxScale();
- // }
- // }
-
- /** Load image with canvas */
- private _loadImageToCanvas(imgElement: HTMLImageElement) {
- if (imgElement) {
- this._img = imgElement;
- const canvas = this._imgCanvas.nativeElement;
- canvas.width = imgElement.width;
- canvas.height = imgElement.height;
- const ctx = canvas.getContext('2d')!;
- ctx.clearRect(0, 0, imgElement.width, imgElement.height);
- ctx.drawImage(imgElement, 0, 0);
- }
- }
-
-
- private _setStylesForContImg(values: {
- x?: number, y?: number
- }) {
- const newStyles = { } as any;
- if (values.x != null && values.y != null) {
- const rootRect = this._cropperContainerRect();
- const x = rootRect.width / 2 - (values.x);
- const y = rootRect.height / 2 - (values.y);
-
- this._imgRect.x = (values.x);
- this._imgRect.y = (values.y);
- this._imgRect.xc = (x);
- this._imgRect.yc = (y);
-
- }
-
- newStyles.transform = `translate3d(${(this._imgRect.x)}px,${(this._imgRect.y)}px, 0)`;
- newStyles.transform += ` scale(${this._scal3Fix})`;
- newStyles.transform += ` rotate(0)`;
- newStyles.transformOrigin = `${this._imgRect.xc}px ${this._imgRect.yc}px 0`;
- for (const key in newStyles) {
- if (newStyles.hasOwnProperty(key)) {
- this._renderer.setStyle(this._imgContainer.nativeElement, key, newStyles[key]);
- }
- }
- }
-
- /**
- * Update area and image position only if needed,
- * this is used when window resize
- */
- updateCropperPosition() {
- if (this.isLoaded) {
- this.updatePosition();
- this._updateAreaIfNeeded();
- }
- }
-
- /** Load Image from input event */
- selectInputEvent(img: Event) {
- this.clean();
- this._currentInputElement = img.target as HTMLInputElement;
- const _img = img.target as HTMLInputElement;
- if (_img.files && _img.files.length !== 1) {
- return;
- }
- const fileSize = _img.files![0].size;
- const fileName = _img.value.replace(/.*(\/|\\)/, '');
-
- if (this.maxFileSize && fileSize > this.maxFileSize) {
- const cropEvent: ImgCropperErrorEvent = {
- name: fileName,
- type: _img.files![0].type,
- size: fileSize,
- error: ImgCropperError.Size
- };
- this.clean();
- this.error.emit(cropEvent as ImgCropperErrorEvent);
- return;
- }
- new Observable(observer => {
-
- const reader = new FileReader();
-
- reader.onerror = err => observer.error(err);
- reader.onabort = err => observer.error(err);
- reader.onload = (ev) => setTimeout(() => {
- observer.next(ev);
- observer.complete();
- });
-
- reader.readAsDataURL(_img.files![0]);
- })
- .pipe(take(1), takeUntil(this._destroy))
- .subscribe(
- (loadEvent) => {
- const originalDataURL = (loadEvent.target as FileReader).result as string;
- this.loadImage({
- name: fileName,
- size: _img.files![0].size, // size in bytes
- type: this.config.type || _img.files![0].type,
- originalDataURL,
- file: _img.files![0]
- });
-
- this.cd.markForCheck();
- },
- () => {
- const cropEvent: ImgCropperErrorEvent = {
- name: fileName,
- size: fileSize,
- error: ImgCropperError.Other,
- errorMsg: 'The File could not be loaded.',
- type: _img.files![0].type
- };
- this.clean();
- this.error.emit(cropEvent as ImgCropperErrorEvent);
- }
- );
-
- }
-
- /** Set the size of the image, the values can be 0 between 1, where 1 is the original size */
- setScale(size?: number, noAutoCrop?: boolean) {
- // fix min scale
- // const newSize = size! >= this.minScale! && size! <= 1 ? size : this.minScale;
- const newSize = (size! >= this.minScale!) ? size : this.minScale;
-
-
- // check
- const changed = size != null && size !== this.scale && newSize !== this.scale;
- this._scale = size;
- if (!changed) {
- return;
- }
- this._scal3Fix = newSize;
- this._updateAbsoluteScale();
- // this._updateMinScale();
- // this._updateMaxScale(); // TODO: check this
- if (this.isLoaded) {
- if (changed) {
- const originPosition = {...this._imgRect};
- this.startTransform = {
- x: originPosition.x,
- y: originPosition.y,
- xOrigin: originPosition.xc,
- yOrigin: originPosition.yc
- };
- this._setStylesForContImg({});
- this._simulatePointerMove();
- } else {
- return;
- }
- } else if (this.minScale) {
- this._setStylesForContImg({
- ...this._getCenterPoints()
- });
- } else {
- return;
- }
-
- this.scaleChange.emit(size);
- if (!noAutoCrop) {
- this._cropIfAutoCrop();
- }
-
- }
-
- private _getCenterPoints() {
- const root = this._cropperContainer.nativeElement as HTMLElement;
- const img = this._imgCanvas.nativeElement;
- const x = (root.offsetWidth - (img.width)) / 2;
- const y = (root.offsetHeight - (img.height)) / 2;
- return {
- x,
- y
- };
- }
-
- /**
- * Fit to screen
- */
- fitToScreen() {
- const container = this._cropperContainer.nativeElement as HTMLElement;
- const min = {
- width: container.offsetWidth,
- height: container.offsetHeight
- };
- const { width, height } = this._img;
- const minScale = {
- width: min.width / width,
- height: min.height / height
- };
- const result = Math.max(minScale.width, minScale.height);
- this.setScale(result);
- }
-
- fit() {
- this.setScale(this.minScale);
- }
-
- private _pointerDown = (event: TouchEvent | MouseEvent) => {
- this._isPointerUp.next(false);
- if (isTouchEvent(event)) {
- if (event.touches.length === 2) {
- this._isMultiTouching = isTouchEvent(event);
-
- // Calculate where the fingers have started on the X and Y axis
- this._startCenter = getCenterFromTouchEvent(event);
- this._startDistance = calDistanceFromTouchEvent(event);
- }
- }
- // Don't do anything if the
- // user is using anything other than the main mouse button.
- if (this._isSliding || (!isTouchEvent(event) && event.button !== 0)) {
- return;
- }
-
- this._ngZone.run(() => {
-
-
- this._isSliding = true;
-
- this.startTransform = {
- x: this._imgRect.x,
- y: this._imgRect.y,
- xOrigin: this._imgRect.xc,
- yOrigin: this._imgRect.yc
- };
- this._lastPointerEvent = event;
- this._startPointerEvent = getGesturePointFromEvent(event);
- event.preventDefault(); // Prevent page scroll
- this._bindGlobalEvents(event);
- });
-
- }
-
- /**
- * Simulate pointerMove with clientX = 0 and clientY = 0,
- * this is used by `setScale` and `rotate`
- */
- private _simulatePointerMove() {
- this._isSliding = true;
- this._startPointerEvent = {
- x: 0,
- y: 0
- };
- this._pointerMove({
- clientX: 0,
- clientY: 0,
- type: 'n',
- preventDefault: () => {}
- } as MouseEvent);
- this._isSliding = false;
- this._startPointerEvent = null;
- }
-
- _markForCheck() {
- this.cd.markForCheck();
- }
-
- /**
- * Called when the user has moved their pointer after
- * starting to drag.
- */
- private _pointerMove = (event: TouchEvent | MouseEvent) => {
- if (this._isSliding) {
- event.preventDefault();
- this._lastPointerEvent = event;
- let x: number | undefined, y: number | undefined;
- let scaleFix = this._scal3Fix;
- const config = this.config;
- const startP = this.startTransform;
- if (!scaleFix || !startP) {
- return;
- }
- const point = getGesturePointFromEvent(event);
- let deltaX = 0;
- let deltaY = 0;
- const imgContainer = this._imgCanvas.nativeElement;
- /** Main image width */
- const imgWidth = imgContainer.width;
- /** Main image height */
- const imgHeight = imgContainer.height;
-
- if (this._isMultiTouching) {
- const newScale = calDistanceFromTouchEvent(event as TouchEvent) / this._startDistance * this._scale!;
- scaleFix = this._scal3Fix = Math.max(newScale, this.minScale!);
- const center = getCenterFromTouchEvent(event as TouchEvent);
- deltaX = center.x - this._startCenter.x;
- deltaY = center.y - this._startCenter.y;
- } else {
- deltaX = point.x - this._startPointerEvent!.x;
- deltaY = point.y - this._startPointerEvent!.y;
- }
-
- if (!scaleFix || !startP) {
- return;
- }
-
- const isMinScaleX = imgWidth * scaleFix < config.width && config.extraZoomOut;
- const isMinScaleY = imgHeight * scaleFix < config.height && config.extraZoomOut;
-
- const limitLeft = (config.width / 2 / scaleFix) >= startP.xOrigin - (deltaX / scaleFix);
- const limitRight = (config.width / 2 / scaleFix) + (imgWidth) - (startP.xOrigin - (deltaX / scaleFix)) <= config.width / scaleFix;
- const limitTop = ((config.height / 2 / scaleFix) >= (startP.yOrigin - (deltaY / scaleFix)));
- const limitBottom = (
- ((config.height / 2 / scaleFix) + (imgHeight) - (startP.yOrigin - (deltaY / scaleFix))) <= (config.height / scaleFix)
- );
-
- // Limit for left
- if ((limitLeft && !isMinScaleX) || (!limitLeft && isMinScaleX)) {
- x = startP.x + (startP.xOrigin) - (config.width / 2 / scaleFix);
- }
-
- // Limit for right
- if ((limitRight && !isMinScaleX) || (!limitRight && isMinScaleX)) {
- x = startP.x + (startP.xOrigin) + (config.width / 2 / scaleFix) - imgWidth;
- }
-
- // Limit for top
- if ((limitTop && !isMinScaleY) || (!limitTop && isMinScaleY)) {
- y = startP.y + (startP.yOrigin) - (config.height / 2 / scaleFix);
- }
-
- // Limit for bottom
- if ((limitBottom && !isMinScaleY) || (!limitBottom && isMinScaleY)) {
- y = startP.y + (startP.yOrigin) + (config.height / 2 / scaleFix) - imgHeight;
- }
-
- // When press shiftKey, deprecated
- // if (event.srcEvent && event.srcEvent.shiftKey) {
- // if (Math.abs(event.deltaX) === Math.max(Math.abs(event.deltaX), Math.abs(event.deltaY))) {
- // y = this.offset.top;
- // } else {
- // x = this.offset.left;
- // }
- // }
-
- if (x === void 0) { x = (deltaX / scaleFix) + (startP.x); }
- if (y === void 0) { y = (deltaY / scaleFix) + (startP.y); }
-
-
-
- this._setStylesForContImg({
- x, y,
- });
- }
- }
-
-
- updatePosition(): void;
- updatePosition(xOrigin: number, yOrigin: number): void;
- updatePosition(xOrigin?: number, yOrigin?: number) {
- const hostRect = this._cropperContainerRect();
- const areaRect = this._areaCropperRect();
- const areaWidth = Math.min(areaRect.width, hostRect.width);
- const areaHeight = Math.min(areaRect.height, hostRect.height);
- let x: number, y: number;
- if (xOrigin == null && yOrigin == null) {
- xOrigin = this._imgRect.xc;
- yOrigin = this._imgRect.yc;
- }
- x = Math.max((areaRect.left - hostRect.left), 0);
- y = Math.max(0, (areaRect.top - hostRect.top));
- x -= (xOrigin! - (areaWidth / 2));
- y -= (yOrigin! - (areaHeight / 2));
-
- this._setStylesForContImg({
- x, y
- });
- }
-
- _slideEnd() {
- this._cropIfAutoCrop();
- }
-
- private _cropIfAutoCrop() {
- if (this.config.autoCrop) {
- this.crop();
- }
- }
-
- /** + */
- zoomIn() {
- const scalePercent = this._calcScalePercent(this._scal3Fix!, this._minScale!, this._maxScale!);
- const newScale = scalePercent < 20 ? ((this._scal3Fix! * this._imgCanvas.nativeElement.width + 50)
- / (this._imgCanvas.nativeElement.width)) : (this._scal3Fix! + .05);
-
- if (newScale > this.minScale! && newScale <= this._maxScale!) {
- this.setScale(newScale);
- } else {
- this.setScale(this._maxScale!);
- }
- }
-
- private _calcScalePercent(num: number, min: number, max: number) {
- return ((num - min) / (max - min)) * 100;
- }
-
-
- /** Clean the img cropper */
- clean() {
- // fix choosing the same image does not load
- if (this._currentInputElement) {
- this._currentInputElement.value = '';
- this._currentInputElement = null!;
- }
- if (this.isLoaded) {
- this._imgRect = { } as any;
- this.startTransform = undefined;
- this.scale = undefined as any;
- this._scal3Fix = undefined;
- this._rotation = 0;
- this._minScale = undefined;
- this._isLoadedImg = false;
- this.isLoaded = false;
- this.isCropped = false;
- this._currentLoadConfig = undefined;
- this.config = this._initialConfig;
- const canvas = this._imgCanvas.nativeElement;
- canvas.width = 0;
- canvas.height = 0;
- this.cleaned.emit(null!);
- this.cd.markForCheck();
- }
- }
-
- /** - */
- zoomOut() {
- const scalePercent = this._calcScalePercent(this._scal3Fix!, this._minScale!, this._maxScale!);
- const newScale = scalePercent < 20 ? ((this._scal3Fix! * this._imgCanvas.nativeElement.width - 50)
- / (this._imgCanvas.nativeElement.width)) : (this._scal3Fix! - .05);
- if (newScale > this.minScale! && newScale <= this._maxScale!) {
- this.setScale(newScale);
- } else {
- this.fit();
- }
- }
- center() {
- const newStyles = {
- ...this._getCenterPoints()
- };
- this._setStylesForContImg(newStyles);
- this._cropIfAutoCrop();
- }
- /**
- * load an image from a given configuration,
- * or from the result of a cropped image
- */
- loadImage(config: ImgCropperLoaderConfig | string, fn?: () => void) {
- this.clean();
- const _config = this._currentLoadConfig = typeof config === 'string'
- ? { originalDataURL: config }
- : { ...config };
-
- let src = _config.originalDataURL;
- if (!src) {
- const err: ImgCropperErrorEvent = {
- name: _config.name!,
- error: ImgCropperError.Other,
- type: _config.type!,
- size: _config.size!
- };
- this.error.emit(err);
- return;
- }
- this._initialAreaWidth = this._initialConfig.width;
- this._initialAreaHeight = this._initialConfig.height;
- if (_config.areaWidth && _config.areaHeight) {
- this.config.width = _config.areaWidth;
- this.config.height = _config.areaHeight;
- }
- if (_config.originalDataURL && isSvgImage(_config.originalDataURL)) {
- src = normalizeSVG(_config.originalDataURL);
- }
-
-
- const cropEvent = { ..._config } as ImgCropperEvent;
-
- new Promise((resolve: (value: HTMLImageElement) => void, reject) => {
- const img = createHtmlImg(src!);
- this._ngZone.runOutsideAngular(() => {
- img.onerror = err => reject(err);
- img.onabort = err => reject(err);
- img.onload = () => resolve(img);
- });
- })
- .then(
- (img) => {
- this._loadImageToCanvas(img);
- this._updateMinScale(img);
- this._updateMaxScale();
- this._isLoadedImg = true;
- this.imageLoaded.emit(cropEvent);
- this.cd.markForCheck();
- Promise.resolve(null).then(() => {
- setTimeout(() => {
- this._ngZone.run(() => {this._positionImg(cropEvent, fn)});
- });
- });
- },
- () => {
- const err: ImgCropperErrorEvent = {
- name: _config.name!,
- error: ImgCropperError.Type,
- type: _config.type!,
- size: _config.size!
- };
- this.error.emit(err);
- }
- );
- }
-
- private _updateAreaIfNeeded() {
- if (!this._config.responsiveArea) return;
-
- const rootRect = this._cropperContainerRect();
- const areaRect = this._areaCropperRect();
- const { minWidth = 1, minHeight = 1, round } = this.config;
-
- // Calculate maximum dimensions allowed for the area
- const maxWidth = Math.max(rootRect.width, minWidth);
- const maxHeight = Math.max(rootRect.height, minHeight);
-
- let newWidth = areaRect.width;
- let newHeight = areaRect.height;
-
- if (round) {
- newWidth = newHeight = Math.min(maxWidth, maxHeight); // Área cuadrada si round es true
- } else {
- const shouldResizeWidth = (
- areaRect.width > maxWidth
- || areaRect.width < this._initialAreaWidth
- || this._areaWidthResized
- );
- const shouldResizeHeight = (
- areaRect.height > maxHeight
- || areaRect.height < this._initialAreaHeight
- || this._areaHeightResized
- );
-
- let posibleWidth_w = 0;
- let posibleWidth_h = 0;
- let posibleHeight_h = 0;
- let posibleHeight_w = 0;
-
- if (shouldResizeWidth) {
- newWidth = posibleWidth_w = Math.min(maxWidth, this._areaWidthResized || this._initialAreaWidth);
- newHeight = posibleWidth_h = (newWidth * areaRect.height) / areaRect.width;
- }
-
- if (shouldResizeHeight) {
- newHeight = posibleHeight_h = Math.min(maxHeight, this._areaHeightResized || this._initialAreaHeight);
- newWidth = posibleHeight_w = (newHeight * areaRect.width) / areaRect.height;
- }
-
- if (shouldResizeHeight && shouldResizeWidth) {
- if (Math.min(posibleWidth_w, posibleWidth_h) < Math.min(posibleHeight_h, posibleHeight_w)) {
- newWidth = posibleWidth_w;
- newHeight = posibleWidth_h;
- } else {
- newHeight = posibleHeight_h;
- newWidth = posibleHeight_w;
- }
- }
- }
-
- // Apply changes if dimensions have changed
- if (newWidth !== this.config.width || newHeight !== this.config.height) {
- const newScale = (this._scal3Fix! * newWidth) / areaRect.width;
- this.config.width = newWidth;
- this.config.height = newHeight;
- this._updateMinScale();
- this._updateMaxScale();
- this.setScale(newScale, true);
- this._markForCheck();
- }
- }
-
- /**
- * @private
- */
- _updateAbsoluteScale() {
- const scale = this._scal3Fix! / (this.config.width / this._initialAreaWidth);
- this._absoluteScale = scale;
- }
-
- /**
- * Load Image from URL
- * @deprecated Use `loadImage` instead of `setImageUrl`
- * @param src URL
- * @param fn function that will be called before emit the event loaded
- */
- setImageUrl(src: string, fn?: () => void) {
- this.loadImage(src, fn);
- }
-
- private _positionImg(cropEvent: ImgCropperEvent, fn?: () => void) {
- const loadConfig = this._currentLoadConfig!;
- this._updateMinScale(this._imgCanvas.nativeElement);
- this._updateMaxScale();
- this.isLoaded = false;
- if (fn) {
- fn();
- } else {
- if (loadConfig.scale) {
- this.setScale(loadConfig.scale, true);
- } else {
- this.setScale(this.minScale, true);
- }
- // this.rotate(loadConfig.rotation || 0);
- this._updateAreaIfNeeded();
- this._markForCheck();
- this._ngZone.runOutsideAngular(() => {
- Promise.resolve(null).then(() => {
- if (loadConfig.xOrigin != null && loadConfig.yOrigin != null) {
- this.updatePosition(loadConfig.xOrigin, loadConfig.yOrigin);
- }
- this._updateAreaIfNeeded();
- this.isLoaded = true;
- this._cropIfAutoCrop();
- this._ngZone.run(() => {
- this._markForCheck();
- this.ready.emit(cropEvent);
- // tslint:disable-next-line: deprecation
- this.loaded.emit(cropEvent);
- });
- });
- });
- }
- }
-
- rotate2(degrees: number) {
- this._rotation -= degrees;
- const newStyles = {} as any;
- newStyles.transform = `translate(${(this._imgRect.x)}px,${(this._imgRect.y)}px)`;
- newStyles.transform += ` scale(${this._scal3Fix})`;
- newStyles.transform += ` rotate(${this._rotation}deg)`;
- newStyles.transformOrigin = `${this._imgRect.xc}px ${this._imgRect.yc}px 0`;
- for (const key in newStyles) {
- if (newStyles.hasOwnProperty(key)) {
- this._renderer.setStyle(this._imgContainer.nativeElement, key, newStyles[key]);
- }
- }
- }
-
- rotate(degrees: number) {
- _normalizeDegrees(degrees)
- const newRotation = this._rotation + degrees;
- clearTimeout(this._rotateTimeOut);
- this._pendingRotation += degrees;
- this._rotation = newRotation;
- this._rotateWithAnimation();
- this._rotateTimeOut = setTimeout(() => {
- this._setStylesForContImg({});
- this._rotateCanvas();
- }, 140);
- }
-
- private _rotateWithAnimation() {
-
- const newStyles = {} as any;
- // easeOutQuad
- newStyles.transition = `cubic-bezier(0.250, 0.460, 0.450, 0.940) 140ms`;
- newStyles.transform = `translate(${(this._imgRect.x)}px,${(this._imgRect.y)}px)`;
- newStyles.transform += ` scale(${this._scal3Fix})`;
- newStyles.transform += ` rotate(${this._pendingRotation}deg)`;
- newStyles.transformOrigin = `${this._imgRect.xc}px ${this._imgRect.yc}px 0`;
- for (const key in newStyles) {
- if (newStyles.hasOwnProperty(key)) {
- this._renderer.setStyle(this._imgContainer.nativeElement, key, newStyles[key]);
- }
- }
- }
-
-
- private _rotateCanvas() {
- let validDegrees = this._pendingRotation;
-
- const degreesRad = validDegrees * Math.PI / 180;
- const canvas = this._imgCanvas.nativeElement;
- canvas.removeAttribute('style');
- this._pendingRotation = 0;
- this._renderer.removeStyle(this._imgContainer.nativeElement, 'transition');
- const canvasClon = createCanvasImg(canvas);
- const ctx = canvas.getContext('2d')!;
-
- // clear
- ctx.clearRect(0, 0, canvasClon.width, canvasClon.height);
-
- // rotate canvas image
- const transform = `rotate(${validDegrees}deg) scale(${1 / this._scal3Fix!})`;
- const transformOrigin = `${this._imgRect.xc}px ${this._imgRect.yc}px 0`;
- canvas.style.transform = transform;
- canvas.style.opacity = '0';
- // tslint:disable-next-line: deprecation
- canvas.style.webkitTransform = transform;
- canvas.style.transformOrigin = transformOrigin;
- // tslint:disable-next-line: deprecation
- canvas.style.webkitTransformOrigin = transformOrigin;
-
- const { left, top } = canvas.getBoundingClientRect() as DOMRect;
-
- // save rect
- const canvasRect = canvas.getBoundingClientRect();
-
- // remove rotate styles
- canvas.removeAttribute('style');
-
- // set w & h
- const w = canvasRect.width;
- const h = canvasRect.height;
- ctx.canvas.width = w;
- ctx.canvas.height = h;
-
- // clear
- ctx.clearRect(0, 0, w, h);
-
- // translate and rotate
- ctx.translate(w / 2, h / 2);
- ctx.rotate(degreesRad);
- ctx.drawImage(canvasClon, -canvasClon.width / 2, -canvasClon.height / 2);
-
- // Update min scale
- this._updateMinScale(canvas);
- this._updateMaxScale();
-
- // set the minimum scale, only if necessary
- if (this.scale! < this.minScale!) {
- this.setScale(0, true);
- } // ↑ no AutoCrop
-
- const rootRect = this._cropperContainerRect();
-
- this._setStylesForContImg({
- x: (left - rootRect.left),
- y: (top - rootRect.top)
- });
-
- // keep image inside the frame
- const originPosition = {...this._imgRect};
- this.startTransform = {
- x: originPosition.x,
- y: originPosition.y,
- xOrigin: originPosition.xc,
- yOrigin: originPosition.yc
- };
- this._setStylesForContImg({});
- this._simulatePointerMove();
-
- this._cropIfAutoCrop();
- }
-
- _updateMinScale(canvas?: HTMLCanvasElement | HTMLImageElement) {
- if (!canvas) {
- canvas = this._imgCanvas.nativeElement;
- }
- const config = this.config;
- const minScale = (config.extraZoomOut ? Math.min : Math.max)(
- config.width / canvas.width,
- config.height / canvas.height);
- this._minScale = minScale;
- this.minScaleChange.emit(minScale!);
- }
-
- /**
- * @private
- */
- _updateMaxScale() {
- const maxScale = (this.config.width / this._initialAreaWidth) * 3;
- this._maxScale = maxScale;
- this.maxScaleChange.emit(maxScale);
- }
-
- /**
- * Resize & crop image
- */
- crop(config?: ImgCropperConfig): ImgCropperEvent {
- const newConfig = config
- ? {...{ }, ...(this.config || new ImgCropperConfig()), ...config} : this.config;
- // this._loadImageToCanvas(this._mainImage.nativeElement);
- const cropEvent = this._imgCrop(newConfig);
- this.cd.markForCheck();
- return cropEvent;
- }
-
- /**
- * @docs-private
- */
- private _imgCrop(myConfig: ImgCropperConfig) {
- const canvasElement: HTMLCanvasElement = document.createElement('canvas');
- const areaRect = this._areaCropperRect();
- const canvasRect = this._canvasRect();
- const scaleFix = this._scal3Fix!;
- const left = (areaRect.left - canvasRect.left) / scaleFix;
- const top = (areaRect.top - canvasRect.top) / scaleFix;
- const { output } = myConfig;
- const currentImageLoadConfig = this._currentLoadConfig!;
- const area = {
- width: myConfig.width,
- height: myConfig.height
- };
- canvasElement.width = area.width / scaleFix;
- canvasElement.height = area.height / scaleFix;
- const ctx = canvasElement.getContext('2d')!;
- if (myConfig.fill) {
- ctx.fillStyle = myConfig.fill;
- ctx.fillRect(0, 0, canvasElement.width, canvasElement.height);
- }
- // crop
- ctx.drawImage(this._imgCanvas.nativeElement,
- -(left), -(top),
- );
-
- const result = canvasElement;
- // TODO: check if the image to be sized is smaller than the crop area
- if (myConfig.output === ImgResolution.Default) {
- const areaWidth = this._areaWidthResized ?? this._initialConfig.width;
- const areaHeight = this._areaHeightResized ?? this._initialConfig.height;
- resizeCanvas(
- result,
- areaWidth,
- areaHeight);
- } else if (typeof output === 'object') {
- if (output.width && output.height) {
- resizeCanvas(result, output.width, output.height);
- } else if (output.width) {
- const newHeight = area.height * output.width / area.width;
- resizeCanvas(result, output.width, newHeight);
- } else if (output.height) {
- const newWidth = area.width * output.height / area.height;
- resizeCanvas(result, newWidth, output.height);
- }
- }
- const type = currentImageLoadConfig.originalDataURL?.startsWith('http')
- ? currentImageLoadConfig.type || myConfig.type
- : myConfig.type || currentImageLoadConfig.type!;
- const dataURL = result.toDataURL(type);
- const cropEvent: ImgCropperEvent = {
- dataURL,
- type,
- name: currentImageLoadConfig.name!,
- areaWidth: this._initialAreaWidth,
- areaHeight: this._initialAreaHeight,
- width: result.width,
- height: result.height,
- originalDataURL: currentImageLoadConfig.originalDataURL,
- scale: this._absoluteScale!,
- rotation: this._rotation,
- left: (areaRect.left - canvasRect.left) / this._scal3Fix!,
- top: (areaRect.top - canvasRect.top) / this._scal3Fix!,
- size: currentImageLoadConfig.size!,
- xOrigin: this._imgRect.xc,
- yOrigin: this._imgRect.yc,
- position: {
- x: this._imgRect.xc,
- y: this._imgRect.yc
- }
- };
-
- this.isCropped = true;
- this.cropped.emit(cropEvent);
- return cropEvent;
- }
-
- // _rootRect(): DOMRect {
- // return this._elementRef.nativeElement.getBoundingClientRect() as DOMRect;
- // }
-
- _cropperContainerRect(): DOMRect {
- return this._cropperContainer.nativeElement.getBoundingClientRect() as DOMRect;
- }
-
- _areaCropperRect(): DOMRect {
- return this._areaRef.nativeElement.getBoundingClientRect() as DOMRect;
- }
-
- _canvasRect(): DOMRect {
- return this._imgCanvas.nativeElement.getBoundingClientRect();
- }
-
- // _mainImageRect(): DOMRect {
- // return this._mainImage.nativeElement.getBoundingClientRect();
- // }
-
-
- /** Called when the user has lifted their pointer. */
- private _pointerUp = (event: TouchEvent | MouseEvent) => {
- this._isPointerUp.next(true);
- if (this._isSliding) {
- event.preventDefault();
- this._removeGlobalEvents();
- this._isSliding = false;
- this._isMultiTouching = false;
- this._startPointerEvent = null;
- this._scale = this._scal3Fix;
- this._cropIfAutoCrop();
- }
- }
-
- /** Called when the window has lost focus. */
- private _windowBlur = () => {
- // If the window is blurred while dragging we need to stop dragging because the
- // browser won't dispatch the `mouseup` and `touchend` events anymore.
- if (this._lastPointerEvent) {
- this._pointerUp(this._lastPointerEvent);
- }
- }
-
- private _bindGlobalEvents(triggerEvent: TouchEvent | MouseEvent) {
- const element = this._document;
- const isTouch = isTouchEvent(triggerEvent);
- const moveEventName = isTouch ? 'touchmove' : 'mousemove';
- const endEventName = isTouch ? 'touchend' : 'mouseup';
- element.addEventListener(moveEventName, this._pointerMove, activeEventOptions);
- element.addEventListener(endEventName, this._pointerUp, activeEventOptions);
-
- if (isTouch) {
- element.addEventListener('touchcancel', this._pointerUp, activeEventOptions);
- }
-
- const window = this._getWindow();
-
- if (typeof window !== 'undefined' && window) {
- window.addEventListener('blur', this._windowBlur);
- }
- }
-
- /** Removes any global event listeners that we may have added. */
- private _removeGlobalEvents() {
- const element = this._document;
- element.removeEventListener('mousemove', this._pointerMove, activeEventOptions);
- element.removeEventListener('mouseup', this._pointerUp, activeEventOptions);
- element.removeEventListener('touchmove', this._pointerMove, activeEventOptions);
- element.removeEventListener('touchend', this._pointerUp, activeEventOptions);
- element.removeEventListener('touchcancel', this._pointerUp, activeEventOptions);
-
- const window = this._getWindow();
-
- if (typeof window !== 'undefined' && window) {
- window.removeEventListener('blur', this._windowBlur);
- }
- }
-
- private _addEventsToScaleWithScroll() {
- this._ngZone.runOutsideAngular(() => {
- const element = this._elementRef!.nativeElement;
- element.addEventListener('wheel', this._onWheel, activeEventOptions);
- });
- }
-
- private _onWheel = (event: WheelEvent) => {
- if (!this.isLoaded) {
- return;
- }
- event.preventDefault();
- this._ngZone.run(() => {
- if (Math.sign(event.deltaY) < 0) {
- this.zoomIn();
- } else {
- this.zoomOut();
- }
- });
-
- }
-
- /** Use defaultView of injected document if available or fallback to global window reference */
- private _getWindow(): Window {
- return this._document.defaultView || window;
- }
-
-}
-
/**
* @dynamic
*/
@@ -1593,48 +197,17 @@ export class LyImageCropper implements OnInit, AfterViewInit, OnDestroy {
StyleRenderer
],
changeDetection: ChangeDetectionStrategy.OnPush,
- exportAs: 'lyCropperArea'
+ exportAs: 'lyCropperArea',
+ standalone: true,
+ imports: [NgIf],
})
-export class LyCropperArea implements WithStyles, OnDestroy {
+export class LyCropperArea extends _LyCropperAreaBase implements WithStyles, OnDestroy {
+ readonly sRenderer = inject(StyleRenderer);
readonly classes = this.sRenderer.renderSheet(STYLES, 'area');
- private _isSliding: boolean;
- /** Keeps track of the last pointer event that was captured by the crop area. */
- private _lastPointerEvent: MouseEvent | TouchEvent | null;
- private _startPointerEvent: {
- x: number
- y: number
- } | null;
- private _currentWidth: number | null;
- private _currentHeight: number | null;
- private _startAreaRect: DOMRect;
- private _startImgRect: DOMRect;
-
- /** Used to subscribe to global move and end events */
- protected _document: Document;
-
- @ViewChild('resizer') readonly _resizer?: ElementRef;
-
- @Input()
- set resizableArea(val: boolean) {
- if (val !== this._resizableArea) {
- this._resizableArea = val;
- Promise.resolve(null).then(() => {
- if (val) {
- this._removeResizableArea();
- this._addResizableArea();
- } else {
- this._removeResizableArea();
- }
- });
- }
- }
- get resizableArea() {
- return this._resizableArea;
- }
- private _resizableArea: boolean;
- @Input() keepAspectRatio: boolean;
- @Input()
+ @Input({
+ transform: booleanAttribute
+ })
@Style(
(_value, _media) => ({ after }, selectors: SelectorsFn) => {
const $$ = selectors(STYLES);
@@ -1652,363 +225,24 @@ export class LyCropperArea implements WithStyles, OnDestroy {
}`;
},
coerceBooleanProperty
- ) round: BooleanInput;
-
- constructor(
- readonly sRenderer: StyleRenderer,
- readonly _elementRef: ElementRef,
- private _ngZone: NgZone,
- readonly _cropper: LyImageCropper,
- @Inject(DOCUMENT) _document: any,
- ) {
- this._document = _document;
- }
-
- ngOnDestroy() {
- this._removeResizableArea();
- }
-
- private _addResizableArea() {
- this._ngZone.runOutsideAngular(() => {
- const element = this._resizer!.nativeElement;
- element.addEventListener('mousedown', this._pointerDown, activeEventOptions);
- element.addEventListener('touchstart', this._pointerDown, activeEventOptions);
- });
- }
-
- private _removeResizableArea() {
- const element = this._resizer?.nativeElement;
- if (element) {
- this._lastPointerEvent = null;
- this._removeGlobalEvents();
- element.removeEventListener('mousedown', this._pointerDown, activeEventOptions);
- element.removeEventListener('touchstart', this._pointerDown, activeEventOptions);
- }
- }
-
- private _pointerDown = (event: MouseEvent | TouchEvent) => {
- this._cropper._isPointerUp.next(false);
- // Don't do anything if the
- // user is using anything other than the main mouse button.
- if (this._isSliding || (!isTouchEvent(event) && event.button !== 0)) {
- return;
- }
-
- event.preventDefault();
-
- this._ngZone.run(() => {
- this._isSliding = true;
- this._lastPointerEvent = event;
- this._startPointerEvent = getGesturePointFromEvent(event);
- this._startAreaRect = this._cropper._areaCropperRect();
- this._startImgRect = this._cropper._canvasRect();
- this._bindGlobalEvents(event);
- });
-
- }
-
- private _pointerMove = (event: MouseEvent | TouchEvent) => {
- if (this._isSliding) {
- event.preventDefault();
- this._lastPointerEvent = event;
- const element: HTMLDivElement = this._elementRef.nativeElement;
- const { width, height, minWidth, minHeight } = this._cropper.config;
- const point = getGesturePointFromEvent(event);
- const deltaX = point.x - this._startPointerEvent!.x;
- const deltaY = point.y - this._startPointerEvent!.y;
- const startAreaRect = this._startAreaRect;
- const startImgRect = this._startImgRect;
- const round = this.round;
- const keepAspectRatio = this._cropper.config.keepAspectRatio || event.shiftKey;
- let newWidth = 0;
- let newHeight = 0;
- const rootRect = this._cropper._cropperContainerRect();
-
- if (round) {
- // The distance from the center of the cropper area to the pointer
- const originX = ((width / 2 / Math.sqrt(2)) + deltaX);
- const originY = ((height / 2 / Math.sqrt(2)) + deltaY);
-
- // Leg
- const side = Math.sqrt(originX ** 2 + originY ** 2);
- newWidth = newHeight = side * 2;
-
- } else if (keepAspectRatio) {
- newWidth = width + deltaX * 2;
- newHeight = height + deltaY * 2;
- if (width !== height) {
- if (width > height) {
- newHeight = height / (width / newWidth);
- } else if (height > width) {
- newWidth = width / (height / newHeight);
- }
- } else {
- newWidth = newHeight = Math.max(newWidth, newHeight);
- }
- } else {
- newWidth = width + deltaX * 2;
- newHeight = height + deltaY * 2;
- }
-
- // To min width
- if (newWidth < minWidth!) {
- newWidth = minWidth!;
- }
- // To min height
- if (newHeight < minHeight!) {
- newHeight = minHeight!;
- }
-
- // Do not overflow the cropper area
- const centerX = startAreaRect.x + startAreaRect.width / 2;
- const centerY = startAreaRect.y + startAreaRect.height / 2;
- const topOverflow = startImgRect.y > centerY - (newHeight / 2);
- const bottomOverflow = centerY + (newHeight / 2) > startImgRect.bottom;
- const minHeightOnOverflow = Math.min((centerY - startImgRect.y) * 2, (startImgRect.bottom - centerY) * 2);
- const leftOverflow = startImgRect.x > centerX - (newWidth / 2);
- const rightOverflow = centerX + (newWidth / 2) > startImgRect.right;
- const minWidthOnOverflow = Math.min((centerX - startImgRect.x) * 2, (startImgRect.right - centerX) * 2);
- const minOnOverflow = Math.min(minWidthOnOverflow, minHeightOnOverflow);
- if (round) {
- if (topOverflow || bottomOverflow || leftOverflow || rightOverflow) {
- newHeight = newWidth = minOnOverflow;
- }
- } else if (keepAspectRatio) {
- const newNewWidth: number[] = [];
- const newNewHeight: number[] = [];
- if ((topOverflow || bottomOverflow)) {
- newHeight = minHeightOnOverflow;
- newNewHeight.push(newHeight);
- newWidth = width / (height / minHeightOnOverflow);
- newNewWidth.push(newWidth);
- }
- if ((leftOverflow || rightOverflow)) {
- newWidth = minWidthOnOverflow;
- newNewWidth.push(newWidth);
- newHeight = height / (width / minWidthOnOverflow);
- newNewHeight.push(newHeight);
- }
- if (newNewWidth.length === 2) {
- newWidth = Math.min(...newNewWidth);
- }
- if (newNewHeight.length === 2) {
- newHeight = Math.min(...newNewHeight);
- }
- } else {
- if (topOverflow || bottomOverflow) {
- newHeight = minHeightOnOverflow;
- }
- if (leftOverflow || rightOverflow) {
- newWidth = minWidthOnOverflow;
- }
- }
-
- // Do not overflow the container
- if (round) {
- const min = Math.min(rootRect.width, rootRect.height);
- if (newWidth > min) {
- newWidth = newHeight = min;
- } else if (newHeight > min) {
- newWidth = newHeight = min;
- }
- } else if (keepAspectRatio) {
- if (newWidth > rootRect.width) {
- newWidth = rootRect.width;
- newHeight = height / (width / rootRect.width);
- }
- if (newHeight > rootRect.height) {
- newWidth = width / (height / rootRect.height);
- newHeight = rootRect.height;
- }
- } else {
- if (newWidth > rootRect.width) {
- newWidth = rootRect.width;
- }
- if (newHeight > rootRect.height) {
- newHeight = rootRect.height;
- }
- }
-
-
- // round values
- const newWidthRounded = Math.round(newWidth);
- const newHeightRounded = Math.round(newHeight);
-
- element.style.width = `${newWidthRounded}px`;
- element.style.height = `${newHeightRounded}px`;
- this._currentWidth = newWidthRounded;
- this._currentHeight = newHeightRounded;
- }
- }
-
- /** Called when the user has lifted their pointer. */
- private _pointerUp = (event: TouchEvent | MouseEvent) => {
- this._cropper._isPointerUp.next(true);
- const hasChange = this._currentWidth !== this._cropper._initialAreaWidth
- || this._currentHeight !== this._cropper._initialAreaHeight;
-
- if (this._isSliding && (this._currentWidth != null)) {
- event.preventDefault();
- this._removeGlobalEvents();
- this._cropper._areaWidthResized = hasChange ? this._currentWidth : null;
- this._cropper._areaHeightResized = hasChange ? this._currentHeight : null;
- // this._cropper._initialAreaWidth =
- this._cropper.config.width = this._currentWidth!;
- // this._cropper._initialAreaHeight =
- this._cropper.config.height = this._currentHeight!;
- this._cropper._updateMinScale();
- this._cropper._updateMaxScale();
- this._cropper._updateAbsoluteScale();
- this._isSliding = false;
- this._startPointerEvent = null;
- this._currentWidth = null;
- this._currentHeight = null;
- this._cropper._markForCheck();
- }
- }
-
- /** Called when the window has lost focus. */
- private _windowBlur = () => {
- // If the window is blurred while dragging we need to stop dragging because the
- // browser won't dispatch the `mouseup` and `touchend` events anymore.
- if (this._lastPointerEvent) {
- this._pointerUp(this._lastPointerEvent);
- }
- }
-
- private _bindGlobalEvents(triggerEvent: TouchEvent | MouseEvent) {
- const element = this._document;
- const isTouch = isTouchEvent(triggerEvent);
- const moveEventName = isTouch ? 'touchmove' : 'mousemove';
- const endEventName = isTouch ? 'touchend' : 'mouseup';
- element.addEventListener(moveEventName, this._pointerMove, activeEventOptions);
- element.addEventListener(endEventName, this._pointerUp, activeEventOptions);
-
- if (isTouch) {
- element.addEventListener('touchcancel', this._pointerUp, activeEventOptions);
- }
-
- const window = this._getWindow();
-
- if (typeof window !== 'undefined' && window) {
- window.addEventListener('blur', this._windowBlur);
- }
- }
-
- /** Removes any global event listeners that we may have added. */
- private _removeGlobalEvents() {
- const element = this._document;
- element.removeEventListener('mousemove', this._pointerMove, activeEventOptions);
- element.removeEventListener('mouseup', this._pointerUp, activeEventOptions);
- element.removeEventListener('touchmove', this._pointerMove, activeEventOptions);
- element.removeEventListener('touchend', this._pointerUp, activeEventOptions);
- element.removeEventListener('touchcancel', this._pointerUp, activeEventOptions);
-
- const window = this._getWindow();
-
- if (typeof window !== 'undefined' && window) {
- window.removeEventListener('blur', this._windowBlur);
- }
- }
-
- /** Use defaultView of injected document if available or fallback to global window reference */
- private _getWindow(): Window {
- return this._document.defaultView || window;
- }
+ ) round: boolean;
}
-/**
- * Normalize degrees for cropper rotation
- * @docs-private
- */
-export function _normalizeDegrees(n: number) {
- const de = n % 360;
- if (de % 90) {
- throw new Error(`LyCropper: Invalid \`${n}\` degree, only accepted values: 0, 90, 180, 270 & 360.`);
- }
- return de;
-}
-
-/**
- * @docs-private
- */
-function createCanvasImg(img: HTMLCanvasElement | HTMLImageElement) {
-
- // create a new canvas
- const newCanvas = document.createElement('canvas');
- const context = newCanvas.getContext('2d')!;
-
- // set dimensions
- newCanvas.width = img.width;
- newCanvas.height = img.height;
-
- // apply the old canvas to the new one
- context.drawImage(img, 0, 0);
-
- // return the new canvas
- return newCanvas;
-}
-
-function normalizeSVG(dataURL: string) {
- if (window.atob && isSvgImage(dataURL)) {
- const len = dataURL.length / 5;
- const text = window.atob(dataURL.replace(DATA_IMAGE_SVG_PREFIX, ''));
- const span = document.createElement('span');
- span.innerHTML = text;
- const svg = span.querySelector('svg')!;
- span.setAttribute('style', 'display:none');
- document.body.appendChild(span);
- const width = parseFloat(getComputedStyle(svg).width!) || 1;
- const height = parseFloat(getComputedStyle(svg).height!) || 1;
- const max = Math.max(width, height);
-
- svg.setAttribute('width', `${len / (width / max)}px`);
- svg.setAttribute('height', `${len / (height / max)}px`);
- // const result = DATA_IMAGE_SVG_PREFIX + window.btoa(span.innerHTML);
- document.body.removeChild(span);
- const blob = new Blob([span.innerHTML], {type: 'image/svg+xml'});
- return URL.createObjectURL(blob);
- }
- return dataURL;
-}
-
-function isSvgImage(dataUrl: string) {
- return dataUrl.startsWith(DATA_IMAGE_SVG_PREFIX);
-}
-
-function createHtmlImg(src: string) {
- const img = new Image();
- img.crossOrigin = 'anonymous';
- img.src = src;
- return img;
-}
-
-function getGesturePointFromEvent(event: TouchEvent | MouseEvent) {
-
- // `touches` will be empty for start/end events so we have to fall back to `changedTouches`.
- const point = isTouchEvent(event)
- ? (event.touches[0] || event.changedTouches[0])
- : event;
-
- return {
- x: point.clientX,
- y: point.clientY
- };
-}
-
-
-/** Returns whether an event is a touch event. */
-function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent {
- return event.type[0] === 't';
-}
-
-// Calculate distance between two fingers
-function calDistanceFromTouchEvent(event: TouchEvent) {
- return Math.hypot(event.touches[0].clientX - event.touches[1].clientX, event.touches[0].clientY - event.touches[1].clientY);
-};
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ preserveWhitespaces: false,
+ selector: 'ly-img-cropper, ly-image-cropper',
+ templateUrl: 'image-cropper.html',
+ providers: [
+ StyleRenderer,
+ {provide: _LyImageCropperBase, useExisting: LyImageCropper},
+ ],
+ standalone: true,
+ imports: [LyCropperArea, NgStyle, NgIf]
+})
+export class LyImageCropper extends _LyImageCropperBase implements OnInit, AfterViewInit, OnDestroy {
-function getCenterFromTouchEvent(event: TouchEvent) {
- const x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
- const y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
- return { x, y }
+ readonly sRenderer = inject(StyleRenderer);
+ readonly classes = this.sRenderer.renderSheet(STYLES, 'root');
+ override areaGridActiveCssClass = this.classes.showGrid;
}
diff --git a/src/lib/image-cropper/public_api.ts b/src/lib/image-cropper/public_api.ts
index 7c31baa40..33205ba58 100644
--- a/src/lib/image-cropper/public_api.ts
+++ b/src/lib/image-cropper/public_api.ts
@@ -1,2 +1,4 @@
export * from './image-cropper';
+export * from './_image-cropper-base';
+export * from './image-cropper-base';
export * from './image-cropper.module';