-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into feature/ux-improvements
* main: feat: IIIf image edit component (#1534) chore(main): release 11.7.5 (#1536) fix: initialize pendo on app start (#1535)
- Loading branch information
Showing
11 changed files
with
308 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
apps/dsp-app/src/app/workspace/resource/values/third-party-iiif/iiif-url-validator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { AbstractControl, AsyncValidatorFn, ValidationErrors, ValidatorFn } from '@angular/forms'; | ||
import { from, Observable, of } from 'rxjs'; | ||
import { catchError, map } from 'rxjs/operators'; | ||
import { IIIFUrl } from './third-party-iiif'; | ||
|
||
export function iiifUrlValidator(): ValidatorFn { | ||
return (control: AbstractControl): ValidationErrors | null => { | ||
if (!IIIFUrl.createUrl(control.value)?.isValidIiifUrl) { | ||
return { invalidIiifUrl: true }; | ||
} | ||
return null; | ||
}; | ||
} | ||
|
||
function fetchUrl(url: string): Promise<void> { | ||
return fetch(url, { method: 'HEAD' }).then(response => { | ||
if (!response.ok) { | ||
throw new Error(`HTTP Error: ${response.status}`); | ||
} | ||
}); | ||
} | ||
|
||
export function infoJsonUrlValidatorAsync(): AsyncValidatorFn { | ||
return (control: AbstractControl): Observable<ValidationErrors | null> => { | ||
const iiifUrl = IIIFUrl.createUrl(control.value); | ||
if (!iiifUrl) { | ||
return of(null); | ||
} | ||
|
||
return from(fetchUrl(iiifUrl.infoJsonUrl)).pipe( | ||
map(() => null), | ||
catchError(() => of({ infoJsonError: true })) | ||
); | ||
}; | ||
} | ||
|
||
export function previewImageUrlValidatorAsync(): AsyncValidatorFn { | ||
return (control: AbstractControl): Observable<ValidationErrors | null> => { | ||
const iiifUrl = IIIFUrl.createUrl(control.value); | ||
if (!iiifUrl) { | ||
return of(null); | ||
} | ||
|
||
return from(fetchUrl(iiifUrl?.previewImageUrl)).pipe( | ||
map(() => null), | ||
catchError(() => of({ previewImageError: true })) | ||
); | ||
}; | ||
} |
45 changes: 45 additions & 0 deletions
45
...sp-app/src/app/workspace/resource/values/third-party-iiif/third-party-iiif.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<div class="third-party-iiif-preview"> | ||
<div | ||
*ngIf="(!previewImageUrl || !iiifUrlControl.valid) && formStatus === 'IDLE'" | ||
class="third-party-iiif-preview-placeholder"> | ||
<mat-icon class="iiif-preview-icon">preview</mat-icon> | ||
<div>[ preview ]</div> | ||
<div class="iiif-url-explanation mat-subtitle-2"> | ||
<div>The url must point to be a valid iiif image (jpg, tif, jp2, png).</div> | ||
<div>Example: "{{exampleString}}"</div> | ||
</div> | ||
</div> | ||
<dasch-swiss-app-progress-indicator *ngIf="formStatus !=='IDLE'"></dasch-swiss-app-progress-indicator> | ||
<img | ||
*ngIf="previewImageUrl && iiifUrlControl.valid" | ||
[src]="previewImageUrl" | ||
(loadstart)="formStatus='LOADING'" | ||
(loadeddata)="formStatus='IDLE'" | ||
(error)="formStatus='IDLE'" | ||
alt="IIIF Preview" | ||
height="240" /> | ||
</div> | ||
<mat-form-field class="third-party-iiif-field"> | ||
<mat-label>IIIF Image URL</mat-label> | ||
<input matInput [formControl]="iiifUrlControl" placeholder="Enter IIIF image URL" /> | ||
<mat-error *ngIf="iiifUrlControl.errors as errors"> {{ errors | humanReadableError: validatorErrors }} </mat-error> | ||
<button | ||
*ngIf="iiifUrlControl.value" | ||
[disabled]="iiifUrlControl.valid" | ||
(click)="resetIfInvalid()" | ||
mat-icon-button | ||
matSuffix | ||
type="button" | ||
color="primary"> | ||
<mat-icon>{{ iiifUrlControl.valid ? 'check' : 'close'}}</mat-icon> | ||
</button> | ||
<button | ||
type="button" | ||
color="primary" | ||
mat-icon-button | ||
matSuffix | ||
matTooltip="Paste from clipboard" | ||
(click)="pasteFromClipboard()"> | ||
<mat-icon>content_paste</mat-icon> | ||
</button> | ||
</mat-form-field> |
36 changes: 36 additions & 0 deletions
36
...sp-app/src/app/workspace/resource/values/third-party-iiif/third-party-iiif.component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
.third-party-iiif-preview { | ||
width: 100%; | ||
min-height: 248px; | ||
border: 2px solid #000; | ||
border-radius: 2px; | ||
display: flex; | ||
flex-direction: row; | ||
justify-content: center; | ||
align-items: center; | ||
background-color: #f2f2f2; | ||
} | ||
|
||
.third-party-iiif-preview-placeholder { | ||
height: 100%; | ||
width: 100%; | ||
justify-content: space-between; | ||
display: flex; | ||
flex-direction: column; | ||
} | ||
|
||
.iiif-preview-icon { | ||
margin-top: 1em; | ||
height:auto; | ||
width: auto; | ||
font-size: 48px; | ||
} | ||
|
||
.iiif-url-explanation > div { | ||
margin-top: 1em; | ||
} | ||
|
||
.third-party-iiif-field { | ||
width: 100%; | ||
} | ||
|
||
|
77 changes: 77 additions & 0 deletions
77
.../dsp-app/src/app/workspace/resource/values/third-party-iiif/third-party-iiif.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; | ||
import { FormBuilder, FormControl, Validators } from '@angular/forms'; | ||
import { Subject } from 'rxjs'; | ||
import { takeUntil } from 'rxjs/operators'; | ||
import { iiifUrlValidator, infoJsonUrlValidatorAsync, previewImageUrlValidatorAsync } from './iiif-url-validator'; | ||
import { IIIFUrl } from './third-party-iiif'; | ||
|
||
@Component({ | ||
selector: 'app-third-part-iiif', | ||
templateUrl: './third-party-iiif.component.html', | ||
styleUrls: ['./third-party-iiif.component.scss'], | ||
}) | ||
export class ThirdPartyIiifComponent implements OnInit, OnDestroy { | ||
@Input() iiifUrlValue = ''; | ||
@Output() afterControlInit = new EventEmitter<FormControl<string>>(); | ||
|
||
iiifUrlControl: FormControl<string>; | ||
|
||
previewImageUrl: string | null; | ||
formStatus: 'VALIDATING' | 'LOADING' | 'IDLE' = 'IDLE'; | ||
|
||
private _destroy$ = new Subject<void>(); | ||
|
||
readonly validatorErrors = [ | ||
{ errorKey: 'invalidIiifUrl', message: 'The provided URL is not a valid IIIF image URL' }, | ||
{ errorKey: 'previewImageError', message: 'The image cannot be loaded from the third party server' }, | ||
{ errorKey: 'infoJsonError', message: 'The iiif info json cannot be loaded from the third party server' }, | ||
]; | ||
|
||
readonly templateString = '{scheme}://{server}{/prefix}/{identifier}/{region}/{size}/{rotation}/{quality}.{format}'; | ||
readonly exampleString = 'https://example.org/image-service/abcd1234/full/max/0/default.jpg'; | ||
|
||
constructor( | ||
private _cdr: ChangeDetectorRef, | ||
private _fb: FormBuilder | ||
) {} | ||
|
||
ngOnInit() { | ||
this.iiifUrlControl = new FormControl(this.iiifUrlValue, { | ||
validators: [Validators.required, iiifUrlValidator()], | ||
asyncValidators: [previewImageUrlValidatorAsync(), infoJsonUrlValidatorAsync()], | ||
}); | ||
|
||
this.iiifUrlControl.valueChanges.pipe(takeUntil(this._destroy$)).subscribe(urlStr => { | ||
const iiifUrl = IIIFUrl.createUrl(urlStr); | ||
this.previewImageUrl = iiifUrl?.previewImageUrl; | ||
}); | ||
|
||
this.iiifUrlControl.statusChanges.pipe(takeUntil(this._destroy$)).subscribe(state => { | ||
this.formStatus = state === 'PENDING' ? 'VALIDATING' : 'IDLE'; | ||
this._cdr.detectChanges(); | ||
}); | ||
|
||
this.afterControlInit.emit(this.iiifUrlControl); | ||
} | ||
|
||
pasteFromClipboard() { | ||
if (navigator.clipboard && navigator.clipboard.readText) { | ||
navigator.clipboard.readText().then(text => { | ||
this.iiifUrlControl.setValue(text); | ||
this.iiifUrlControl.markAsTouched(); | ||
this.iiifUrlControl.updateValueAndValidity(); | ||
}); | ||
} | ||
} | ||
|
||
resetIfInvalid() { | ||
if (this.iiifUrlControl.invalid) { | ||
this.iiifUrlControl.reset(); | ||
} | ||
} | ||
|
||
ngOnDestroy(): void { | ||
this._destroy$.next(); | ||
this._destroy$.complete(); | ||
} | ||
} |
85 changes: 85 additions & 0 deletions
85
apps/dsp-app/src/app/workspace/resource/values/third-party-iiif/third-party-iiif.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
export class IIIFUrl { | ||
private _url: URL; | ||
|
||
private readonly qualform_ex = /^(color|gray|bitonal|default)\.(jpg|tif|png|jp2)$/; | ||
private readonly rotation_ex = /^[-+]?[0-9]*\.?[0-9]+$|^![-+]?[0-9]*\.?[0-9]+$/; | ||
private readonly size_ex = /^(\^?max)|(\^?pct:[0-9]*\.?[0-9]*)|(\^?[0-9]*,)|(\^?,[0-9]*)|(\^?!?[0-9]*,[0-9]*)$/; | ||
private readonly region_ex = | ||
/^(full)|(square)|([0-9]+,[0-9]+,[0-9]+,[0-9]+)|(pct:[0-9]*\.?[0-9]*,[0-9]*\.?[0-9]*,[0-9]*\.?[0-9]*,[0-9]*\.?[0-9]*)$/; | ||
|
||
private constructor(url: string) { | ||
this._url = new URL(url); | ||
} | ||
|
||
static createUrl(url: string): IIIFUrl | null { | ||
try { | ||
return new IIIFUrl(url); | ||
} catch (error) { | ||
return null; | ||
} | ||
} | ||
|
||
private get _baseUrl(): string { | ||
return `${this._url.protocol}//${this._url.host}`; | ||
} | ||
|
||
private get _segments(): string[] { | ||
return this._url.pathname.split('/').slice(1); | ||
} | ||
|
||
private get _qualitySegment(): string { | ||
return this._segments[this._segments.length - 1]; | ||
} | ||
|
||
private get _quality(): string { | ||
return this._qualitySegment.split('.')[0]; | ||
} | ||
|
||
private get _rotationSegment(): string { | ||
return this._segments[this._segments.length - 2]; | ||
} | ||
|
||
private get _sizeSegment(): string { | ||
return this._segments[this._segments.length - 3]; | ||
} | ||
|
||
private get _regionSegment(): string { | ||
return this._segments[this._segments.length - 4]; | ||
} | ||
|
||
get isValidIiifUrl(): boolean { | ||
if (this._segments.length < 5) { | ||
return false; | ||
} | ||
|
||
if (!this.qualform_ex.test(this._qualitySegment)) { | ||
return false; | ||
} | ||
|
||
if (!this.rotation_ex.test(this._rotationSegment)) { | ||
return false; | ||
} | ||
|
||
if (!this.size_ex.test(this._sizeSegment)) { | ||
return false; | ||
} | ||
|
||
if (!this.region_ex.test(this._regionSegment)) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
get iiifImageInstanceUrl(): string { | ||
return `${this._baseUrl}/${this._segments.slice(0, this._segments.length - 4).join('/')}`; | ||
} | ||
|
||
get infoJsonUrl() { | ||
return `${this.iiifImageInstanceUrl}/info.json`; | ||
} | ||
|
||
get previewImageUrl() { | ||
return `${this.iiifImageInstanceUrl}/${this._regionSegment}/${this._sizeSegment}/${this._rotationSegment}/${this._quality}.jpg`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters