@rdlabo/ionic-angular-photo-editor 21.0.1 → 21.0.3-3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +0,0 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
2
- import { PhotoEditorPage } from './photo-editor.page';
3
- import { testConfig } from '../../../../../util/test.config';
4
-
5
- describe('PhotoEditorPage', () => {
6
- let component: PhotoEditorPage;
7
- let fixture: ComponentFixture<PhotoEditorPage>;
8
-
9
- beforeEach(() => {
10
- TestBed.configureTestingModule({
11
- providers: testConfig.providers,
12
- });
13
- fixture = TestBed.createComponent(PhotoEditorPage);
14
- component = fixture.componentInstance;
15
- fixture.detectChanges();
16
- });
17
-
18
- it('should create', () => {
19
- expect(component).toBeTruthy();
20
- });
21
- });
@@ -1,221 +0,0 @@
1
- import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
2
- import {
3
- ChangeDetectionStrategy,
4
- Component,
5
- computed,
6
- effect,
7
- ElementRef,
8
- inject,
9
- input,
10
- OnDestroy,
11
- OnInit,
12
- signal,
13
- viewChild,
14
- } from '@angular/core';
15
- import { CommonModule } from '@angular/common';
16
- import { FormsModule } from '@angular/forms';
17
- import { IonContent, ModalController, RangeCustomEvent, ViewDidEnter, ViewDidLeave } from '@ionic/angular/standalone';
18
- import ImageEditor from 'tui-image-editor';
19
- import { filterPreset } from '../../filter-preset';
20
- import { Subscription } from 'rxjs';
21
- import { toObservable } from '@angular/core/rxjs-interop';
22
- import { IDictionaryForEditor, IFilter, IPhotoEditorDismiss, ISize } from '../../types';
23
- import { ionComponents } from '../../ion-components';
24
- import { dictionaryForEditor } from '../../dictionaries';
25
- import { initializeEditorIcons, waitToFindDom } from '../util';
26
-
27
- @Component({
28
- selector: 'app-editor-image',
29
- templateUrl: './photo-editor.page.html',
30
- styleUrls: ['../core.scss', './photo-editor.page.scss'],
31
- imports: [CommonModule, FormsModule, ...ionComponents],
32
- changeDetection: ChangeDetectionStrategy.OnPush,
33
- })
34
- export class PhotoEditorPage implements OnInit, OnDestroy, ViewDidEnter, ViewDidLeave {
35
- readonly modalCtrl = inject(ModalController);
36
-
37
- protected readonly dictionary = signal<IDictionaryForEditor>(dictionaryForEditor());
38
-
39
- readonly requireSquare = input<boolean, BooleanInput>(false, {
40
- transform: coerceBooleanProperty,
41
- });
42
- readonly labels = input<Partial<IDictionaryForEditor> | undefined>(undefined);
43
- readonly setLabels = effect(() => {
44
- if (this.labels()) {
45
- this.dictionary.update((value) => ({ ...value, ...this.labels() }));
46
- }
47
- });
48
- readonly value = input.required<string>();
49
-
50
- readonly editorRef = viewChild.required<ElementRef>('imageEditor');
51
- readonly ionContent = viewChild.required(IonContent, { read: ElementRef });
52
-
53
- readonly filters = signal<IFilter[]>([]);
54
- readonly footerMenu = signal<'filter' | 'menu' | 'crop' | 'brightness'>('menu');
55
- readonly currentCrop = signal<'cover' | '16/9' | '1' | 'auto'>('cover');
56
- readonly currentRotate = signal<number>(0);
57
- readonly photoCrop = signal<ISize>({
58
- width: 0,
59
- height: 0,
60
- });
61
- readonly isCropped = signal<boolean>(false);
62
- private readonly adoptFilter = signal<IFilter | undefined>(undefined);
63
- protected readonly filterPreset = computed(() => filterPreset(this.dictionary()));
64
-
65
- private readonly footerMenu$ = toObservable(this.footerMenu);
66
-
67
- private readonly initSubscription$: Subscription[] = [];
68
- private readonly filterImageSize = 240;
69
- private editorInstance!: ImageEditor;
70
-
71
- private canvasContainerObserver: MutationObserver = new MutationObserver((mutationsList: MutationRecord[]) => {
72
- if (mutationsList.find((mutation) => mutation.type === 'attributes' && mutation.attributeName === 'style')) {
73
- // Cover the image editor with the parent element
74
- const editorRef = this.editorRef();
75
- editorRef.nativeElement.style.minWidth = mutationsList[0].target.parentElement!.style.maxWidth;
76
- editorRef.nativeElement.style.minHeight = mutationsList[0].target.parentElement!.style.maxHeight;
77
-
78
- this.photoCrop.set({
79
- width: mutationsList[0].target.parentElement!.querySelector('canvas')!.width,
80
- height: mutationsList[0].target.parentElement!.querySelector('canvas')!.height,
81
- });
82
- }
83
- });
84
-
85
- constructor() {
86
- initializeEditorIcons();
87
- }
88
-
89
- ngOnInit() {
90
- this.initSubscription$.push(
91
- this.footerMenu$.subscribe((value) => {
92
- if (value === 'filter') {
93
- this.initializeFilterMenu().then();
94
- }
95
- if (value === 'crop') {
96
- this.editorInstance.startDrawingMode('CROPPER');
97
- this.changeCrop(this.requireSquare() ? '1' : 'cover');
98
- }
99
- }),
100
- );
101
- }
102
-
103
- ngOnDestroy() {
104
- this.initSubscription$.forEach((subscription) => subscription.unsubscribe());
105
- }
106
-
107
- async ionViewDidEnter() {
108
- this.editorInstance = new ImageEditor(this.editorRef().nativeElement, {
109
- cssMaxWidth: this.ionContent().nativeElement.clientWidth - 32,
110
- cssMaxHeight: this.ionContent().nativeElement.clientHeight - 32,
111
- });
112
- waitToFindDom(this.editorRef().nativeElement, '.tui-image-editor-canvas-container').then(() => {
113
- this.canvasContainerObserver.observe(this.editorRef().nativeElement.querySelector('.tui-image-editor-canvas-container'), {
114
- attributes: true,
115
- childList: false,
116
- subtree: true,
117
- });
118
- });
119
- const blob = await fetch(this.value()).then((res) => res.blob());
120
- await this.editorInstance.loadImageFromFile(new File([blob], 'data.png', { type: blob.type }));
121
- this.footerMenu.set(this.requireSquare() ? 'crop' : 'menu');
122
- }
123
-
124
- ionViewDidLeave() {
125
- this.editorInstance.destroy();
126
- this.canvasContainerObserver.disconnect();
127
- }
128
-
129
- changeCrop(crop: 'cover' | '16/9' | '1' | 'auto') {
130
- const rect = crop === 'cover' ? this.photoCrop().width / this.photoCrop().height : crop === '16/9' ? 16 / 9 : 1;
131
- this.editorInstance.setCropzoneRect(crop !== 'auto' ? rect : undefined);
132
- this.currentCrop.set(crop);
133
- }
134
-
135
- async rotate() {
136
- this.editorInstance.stopDrawingMode();
137
- await this.editorInstance.rotate(90);
138
- this.currentRotate.update((value) => value + 90);
139
- this.editorInstance.startDrawingMode('CROPPER');
140
- requestAnimationFrame(() => this.changeCrop(this.currentCrop()));
141
- }
142
-
143
- async closeCrop(type: 'cancel' | 'apply') {
144
- if (this.footerMenu() === 'crop') {
145
- if (type === 'cancel') {
146
- await this.editorInstance.rotate(this.currentRotate() * -1);
147
- } else {
148
- await this.editorInstance.crop(this.editorInstance.getCropzoneRect());
149
- this.isCropped.set(true);
150
- }
151
- this.currentRotate.set(0);
152
- this.currentCrop.set('cover');
153
- this.editorInstance.stopDrawingMode();
154
- } else if (this.footerMenu() === 'brightness') {
155
- if (type === 'cancel' && this.editorInstance.hasFilter('brightness')) {
156
- await this.editorInstance.removeFilter('brightness');
157
- }
158
- }
159
- this.footerMenu.set('menu');
160
- }
161
-
162
- async changeRange(event: RangeCustomEvent) {
163
- if (this.editorInstance.hasFilter('brightness')) {
164
- await this.editorInstance.removeFilter('brightness');
165
- }
166
- await this.editorInstance.applyFilter('brightness', {
167
- brightness: Number(event.detail.value) / 255,
168
- });
169
- }
170
-
171
- imageSave() {
172
- const value = this.editorInstance.toDataURL();
173
- this.modalCtrl.dismiss({ value } as IPhotoEditorDismiss);
174
- }
175
-
176
- private async initializeFilterMenu() {
177
- const filters: IFilter[] = [];
178
-
179
- const defaultInstance = new ImageEditor(document.createElement('div'), {
180
- cssMaxWidth: this.filterImageSize,
181
- cssMaxHeight: (this.photoCrop().height * this.filterImageSize) / this.photoCrop().width,
182
- });
183
- const blob = await fetch(
184
- this.editorInstance.toDataURL({
185
- multiplier: this.filterImageSize / this.photoCrop().width,
186
- }),
187
- ).then((res) => res.blob());
188
- await defaultInstance.loadImageFromFile(new File([blob], 'defaultInstance.png', { type: blob.type }));
189
-
190
- for (const filter of this.filterPreset()) {
191
- if (filter.type !== 'Default') {
192
- await defaultInstance.applyFilter(filter.type, filter.option);
193
- }
194
- filters.push({
195
- name: filter.name,
196
- type: filter.type,
197
- option: filter.option,
198
- data: defaultInstance.toDataURL(),
199
- width: this.filterImageSize,
200
- height: (this.photoCrop().height * this.filterImageSize) / this.photoCrop().width,
201
- });
202
- if (filter.type !== 'Default') {
203
- await defaultInstance.removeFilter(filter.type);
204
- }
205
- }
206
- this.filters.set(filters);
207
- defaultInstance.destroy();
208
- }
209
-
210
- async filterImage(filter: IFilter) {
211
- if (this.adoptFilter()) {
212
- await this.editorInstance.removeFilter(this.adoptFilter()!.type);
213
- }
214
- if (filter.type === 'Default') {
215
- this.adoptFilter.set(undefined);
216
- return;
217
- }
218
- await this.editorInstance.applyFilter(filter.type, filter.option);
219
- this.adoptFilter.set(filter);
220
- }
221
- }
@@ -1,31 +0,0 @@
1
- <ion-header [translucent]="true">
2
- <ion-toolbar>
3
- <ion-buttons slot="start">
4
- <ion-button (click)="modalCtrl.dismiss()" class="viewer-close-button">
5
- <ion-icon name="close-outline" slot="icon-only"></ion-icon>
6
- </ion-button>
7
- </ion-buttons>
8
- @if (enableDelete()) {
9
- <ion-buttons slot="end">
10
- <ion-button color="photo-editor-danger" fill="solid" type="submit" (click)="remove()">{{ dictionary().delete }}</ion-button>
11
- </ion-buttons>
12
- }
13
- </ion-toolbar>
14
- </ion-header>
15
-
16
- <ion-content [fullscreen]="true">
17
- <swiper-container #swiper>
18
- @for (url of imageUrls(); track url) {
19
- <swiper-slide>
20
- <div class="swiper-zoom-container"><img [src]="url" alt="" [class.circle]="isCircle()" /></div>
21
- </swiper-slide>
22
- }
23
- </swiper-container>
24
- </ion-content>
25
-
26
- @if (enableFooterSafeArea()) {
27
- <!-- use for safe area-->
28
- <ion-footer>
29
- <ion-toolbar style="--border-width: 0; --min-height: 0"></ion-toolbar>
30
- </ion-footer>
31
- }
@@ -1,29 +0,0 @@
1
- ion-button.viewer-close-button {
2
- --color: var(--editor-color) !important;
3
- }
4
-
5
- img.circle {
6
- border-radius: 50%;
7
- display: block;
8
- padding: 16px;
9
- }
10
-
11
- swiper-container {
12
- height: 100%;
13
- max-height: 100%;
14
- }
15
-
16
- swiper-slide {
17
- height: 100%;
18
- max-height: 100%;
19
- display: flex;
20
- justify-content: center;
21
- align-items: center;
22
- }
23
-
24
- swiper-slide img {
25
- display: block;
26
- object-fit: contain;
27
- max-width: 100%;
28
- max-height: 100%;
29
- }
@@ -1,25 +0,0 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
2
- import { ComponentRef } from '@angular/core';
3
- import { PhotoViewerPage } from './photo-viewer.page';
4
- import { testConfig } from '../../../../../util/test.config';
5
-
6
- describe('PhotoViewerPage', () => {
7
- let component: PhotoViewerPage;
8
- let fixture: ComponentFixture<PhotoViewerPage>;
9
- let componentRef: ComponentRef<PhotoViewerPage>;
10
-
11
- beforeEach(() => {
12
- TestBed.configureTestingModule({
13
- providers: testConfig.providers,
14
- });
15
- fixture = TestBed.createComponent(PhotoViewerPage);
16
- component = fixture.componentInstance;
17
- componentRef = fixture.componentRef;
18
- componentRef.setInput('imageUrls', []);
19
- fixture.detectChanges();
20
- });
21
-
22
- it('should create', () => {
23
- expect(component).toBeTruthy();
24
- });
25
- });
@@ -1,134 +0,0 @@
1
- import {
2
- ChangeDetectionStrategy,
3
- Component,
4
- CUSTOM_ELEMENTS_SCHEMA,
5
- effect,
6
- ElementRef,
7
- inject,
8
- input,
9
- OnDestroy,
10
- OnInit,
11
- signal,
12
- viewChild,
13
- } from '@angular/core';
14
- import { IonicSlides, ModalController } from '@ionic/angular/standalone';
15
- import { Navigation, Zoom } from 'swiper/modules';
16
- import { fromEvent, Subscription, throttleTime, withLatestFrom, zipWith } from 'rxjs';
17
- import { SwiperContainer } from 'swiper/element';
18
- import { ionComponents } from '../../ion-components';
19
- import { IDictionaryForViewer, IPhotoViewerDismiss } from '../../types';
20
- import { register } from 'swiper/element/bundle';
21
- import { dictionaryForViewer } from '../../dictionaries';
22
- import { BooleanInput, coerceBooleanProperty, coerceNumberProperty, NumberInput } from '@angular/cdk/coercion';
23
- import { initializeViewerIcons, waitToFindDom } from '../util';
24
-
25
- @Component({
26
- selector: 'app-photo-image',
27
- templateUrl: './photo-viewer.page.html',
28
- styleUrls: ['../core.scss', './photo-viewer.page.scss'],
29
- imports: [...ionComponents],
30
- schemas: [CUSTOM_ELEMENTS_SCHEMA],
31
- changeDetection: ChangeDetectionStrategy.OnPush,
32
- })
33
- export class PhotoViewerPage implements OnInit, OnDestroy {
34
- readonly imageUrls = input.required<string[]>();
35
- readonly index = input<number, NumberInput>(0, {
36
- transform: coerceNumberProperty,
37
- });
38
- readonly isCircle = input<boolean, BooleanInput>(false, {
39
- transform: coerceBooleanProperty,
40
- });
41
- readonly enableDelete = input<boolean, BooleanInput>(false, {
42
- transform: coerceBooleanProperty,
43
- });
44
- readonly enableFooterSafeArea = input<boolean, BooleanInput>(false, {
45
- transform: coerceBooleanProperty,
46
- });
47
- readonly labels = input<Partial<IDictionaryForViewer>>();
48
- readonly setLabels = effect(() => {
49
- if (this.labels()) {
50
- this.dictionary.update((value) => ({ ...value, ...this.labels() }));
51
- }
52
- });
53
-
54
- readonly swiper = viewChild.required<ElementRef<SwiperContainer>>('swiper');
55
- protected readonly dictionary = signal<IDictionaryForViewer>(dictionaryForViewer());
56
-
57
- readonly watchSwipe$ = new Subscription();
58
- readonly modalCtrl = inject(ModalController);
59
- private readonly el = inject(ElementRef);
60
-
61
- constructor() {
62
- register();
63
- initializeViewerIcons();
64
- }
65
-
66
- async ngOnInit() {
67
- waitToFindDom(this.el.nativeElement, 'swiper-container').then(() => {
68
- const index = this.index();
69
- const swiper = this.swiper();
70
- Object.assign(swiper.nativeElement, {
71
- modules: [Navigation, Zoom, IonicSlides],
72
- initialSlide: index,
73
- slidesPerView: 1,
74
- pagination: {
75
- enabled: true,
76
- clickable: true,
77
- },
78
- zoom: true,
79
- });
80
- swiper.nativeElement.initialize();
81
- swiper.nativeElement.swiper.zoom.enable();
82
-
83
- swiper.nativeElement.swiper.activeIndex = index;
84
- swiper.nativeElement.swiper.update();
85
- });
86
-
87
- this.watchSwipe$.add(
88
- fromEvent<TouchEvent>(this.el.nativeElement, 'touchstart')
89
- .pipe(
90
- zipWith(
91
- fromEvent<TouchEvent>(this.el.nativeElement, 'touchend').pipe(
92
- withLatestFrom(fromEvent<TouchEvent>(this.el.nativeElement, 'touchmove')),
93
- ),
94
- ),
95
- throttleTime(1),
96
- )
97
- .subscribe(([touchstart, [_, touchmove]]) => {
98
- const touchstartClientX = touchstart.touches ? touchstart.touches[0].clientX : (touchstart as any).detail[1].clientX;
99
- const touchmoveClientX = touchmove.touches ? touchmove.touches[0].clientX : (touchmove as any).detail[1].clientX;
100
-
101
- const touchstartClientY = touchstart.touches ? touchstart.touches[0].clientY : (touchstart as any).detail[1].clientY;
102
- const touchmoveClientY = touchmove.touches ? touchmove.touches[0].clientY : (touchmove as any).detail[1].clientY;
103
-
104
- const xDiff = touchstartClientX - touchmoveClientX;
105
- const yDiff = touchstartClientY - touchmoveClientY;
106
-
107
- const slides = (this.swiper() as any).nativeElement.querySelectorAll('swiper-slide') as HTMLElement[];
108
- const isZoomed = Array.from(slides).find((slide: HTMLElement) => {
109
- return ['swiper-slide-zoomed', 'swiper-slide-active'].every((c) => slide.classList.contains(c));
110
- });
111
-
112
- const threshold = touchmove.touches ? -50 : -5;
113
-
114
- if (!isZoomed && Math.abs(xDiff) < Math.abs(threshold) && yDiff < threshold && touchstart.timeStamp <= touchmove.timeStamp) {
115
- this.watchSwipe$.unsubscribe();
116
- this.modalCtrl.dismiss();
117
- }
118
- }),
119
- );
120
- }
121
-
122
- ngOnDestroy() {
123
- this.watchSwipe$.unsubscribe();
124
- }
125
-
126
- remove() {
127
- this.modalCtrl.dismiss({
128
- delete: {
129
- index: this.swiper().nativeElement.swiper.activeIndex,
130
- value: this.imageUrls()[this.swiper().nativeElement.swiper.activeIndex],
131
- },
132
- } as IPhotoViewerDismiss);
133
- }
134
- }
@@ -1,48 +0,0 @@
1
- import { addIcons } from 'ionicons';
2
- import {
3
- checkmarkOutline,
4
- closeOutline,
5
- colorFilterOutline,
6
- cropOutline,
7
- expandOutline,
8
- refreshOutline,
9
- removeOutline,
10
- send,
11
- squareOutline,
12
- sunnyOutline,
13
- tabletLandscapeOutline,
14
- } from 'ionicons/icons';
15
-
16
- export const initializeViewerIcons = (): void => {
17
- addIcons({
18
- closeOutline,
19
- removeOutline,
20
- });
21
- };
22
-
23
- export const initializeEditorIcons = (): void => {
24
- addIcons({
25
- closeOutline,
26
- send,
27
- cropOutline,
28
- colorFilterOutline,
29
- sunnyOutline,
30
- expandOutline,
31
- tabletLandscapeOutline,
32
- squareOutline,
33
- refreshOutline,
34
- checkmarkOutline,
35
- });
36
- };
37
-
38
- export const waitToFindDom = (nativeElement: HTMLElement, selector: string): Promise<void> => {
39
- return new Promise<void>((resolve) => {
40
- const interval = setInterval(() => {
41
- const find = nativeElement.querySelector(selector);
42
- if (find) {
43
- clearInterval(interval);
44
- resolve();
45
- }
46
- });
47
- });
48
- };
@@ -1,5 +0,0 @@
1
- export enum PhotoEditorErrors {
2
- initialize = 'input dom is not found',
3
- cancel = 'user canceled',
4
- type = 'upload file is not image.',
5
- }
@@ -1,19 +0,0 @@
1
- import { TestBed } from '@angular/core/testing';
2
-
3
- import { PhotoFileService } from './photo-file.service';
4
- import { testConfig } from '../../../../util/test.config';
5
-
6
- describe('PhotoService', () => {
7
- let service: PhotoFileService;
8
-
9
- beforeEach(() => {
10
- TestBed.configureTestingModule({
11
- providers: [...testConfig.providers],
12
- });
13
- service = TestBed.inject(PhotoFileService);
14
- });
15
-
16
- it('should be created', () => {
17
- expect(service).toBeTruthy();
18
- });
19
- });