@theseam/ui-common 1.0.2-beta.58 → 1.0.2-beta.59

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.
@@ -0,0 +1,743 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, DestroyRef, computed, signal, ChangeDetectionStrategy, Component, input, output, viewChild, afterNextRender, effect, isDevMode } from '@angular/core';
3
+ import * as i1$2 from '@angular/cdk/a11y';
4
+ import { A11yModule } from '@angular/cdk/a11y';
5
+ import { toSignal, takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
6
+ import * as i2 from '@angular/forms';
7
+ import { FormControl, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormGroup } from '@angular/forms';
8
+ import { of, from, map, switchMap as switchMap$1, combineLatest } from 'rxjs';
9
+ import { faSignature, faUpload, faKeyboard } from '@fortawesome/free-solid-svg-icons';
10
+ import * as i3 from '@theseam/ui-common/buttons';
11
+ import { TheSeamButtonsModule } from '@theseam/ui-common/buttons';
12
+ import * as i4 from '@theseam/ui-common/icon';
13
+ import { TheSeamIconModule } from '@theseam/ui-common/icon';
14
+ import { TheSeamLayoutService } from '@theseam/ui-common/layout';
15
+ import { ModalRef } from '@theseam/ui-common/modal';
16
+ import { TheSeamAutoFocusDirective, TheSeamDisableControlDirective } from '@theseam/ui-common/shared';
17
+ import { readFileAsDataUrlAsync, observeControlValue, observeControlValid } from '@theseam/ui-common/utils';
18
+ import { switchMap, debounceTime } from 'rxjs/operators';
19
+ import * as i1 from 'ngx-file-drop';
20
+ import { NgxFileDropModule } from 'ngx-file-drop';
21
+ import * as i1$1 from '@almothafar/angular-signature-pad';
22
+ import { AngularSignaturePadModule } from '@almothafar/angular-signature-pad';
23
+ import { TheSeamFontLoaderService } from '@theseam/ui-common/services';
24
+ import * as i2$1 from '@theseam/ui-common/form-field';
25
+ import { TheSeamFormFieldModule } from '@theseam/ui-common/form-field';
26
+ import { ComponentHarness } from '@angular/cdk/testing';
27
+
28
+ const THESEAM_SIGNATURE_INPUT_CONTAINER = new InjectionToken('THESEAM_SIGNATURE_INPUT_CONTAINER');
29
+
30
+ const MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024;
31
+ const maxFileSizeValidator = (control) => {
32
+ const value = control.value;
33
+ if (!(value instanceof File)) {
34
+ return null;
35
+ }
36
+ return value.size > MAX_FILE_SIZE_BYTES ? { maxFileSize: {} } : null;
37
+ };
38
+ class TheSeamSignatureInputImgComponent {
39
+ static MAX_FILE_SIZE = MAX_FILE_SIZE_BYTES;
40
+ _container = inject(THESEAM_SIGNATURE_INPUT_CONTAINER, {
41
+ optional: true,
42
+ });
43
+ _destroyRef = inject(DestroyRef);
44
+ /**
45
+ * The File is only needed for validation at selection time. Once it's been
46
+ * converted to a data URL and stored in the form value, we don't need the
47
+ * File again — so there's no point trying to round-trip it through
48
+ * writeValue / form state. The preview renders off the current form value.
49
+ */
50
+ _fileControl = new FormControl(null, {
51
+ validators: [maxFileSizeValidator],
52
+ });
53
+ _fileStatus = toSignal(this._fileControl.statusChanges, {
54
+ initialValue: this._fileControl.status,
55
+ });
56
+ _sizeError = computed(() => {
57
+ // Touch the status signal so this re-runs on validity changes.
58
+ this._fileStatus();
59
+ return this._fileControl.getError('maxFileSize')
60
+ ? 'File size has exceeded 2MB.'
61
+ : null;
62
+ }, ...(ngDevMode ? [{ debugName: "_sizeError" }] : []));
63
+ /**
64
+ * Single source of truth for both the form value and the preview image.
65
+ * External writes (writeValue) and successful uploads both funnel through
66
+ * here, so switching tabs and coming back always shows the last committed
67
+ * signature.
68
+ */
69
+ _value = signal(null, ...(ngDevMode ? [{ debugName: "_value" }] : []));
70
+ _previewDataUrl = computed(() => this._value(), ...(ngDevMode ? [{ debugName: "_previewDataUrl" }] : []));
71
+ _previewBackgroundImage = computed(() => {
72
+ const url = this._value();
73
+ return url ? `url("${url}")` : null;
74
+ }, ...(ngDevMode ? [{ debugName: "_previewBackgroundImage" }] : []));
75
+ _onChange = () => undefined;
76
+ _onTouched = () => undefined;
77
+ constructor() {
78
+ if (this._container) {
79
+ this._container.registerInputItem('img', this);
80
+ this._destroyRef.onDestroy(() => this._container?.unregisterInputItem('img', this));
81
+ }
82
+ // Valid file uploads convert to a data URL and become both the preview
83
+ // and the form value. Invalid (too large) files clear both.
84
+ this._fileControl.valueChanges
85
+ .pipe(switchMap(() => {
86
+ const file = this._fileControl.value;
87
+ if (!file || this._fileControl.invalid) {
88
+ return of(null);
89
+ }
90
+ return from(readFileAsDataUrlAsync(file));
91
+ }), takeUntilDestroyed())
92
+ .subscribe((dataUrl) => this._setValue(dataUrl));
93
+ }
94
+ writeValue(value) {
95
+ this._value.set(value);
96
+ }
97
+ registerOnChange(fn) {
98
+ this._onChange = fn;
99
+ }
100
+ registerOnTouched(fn) {
101
+ this._onTouched = fn;
102
+ }
103
+ setDisabledState(isDisabled) {
104
+ if (this._fileControl.disabled === isDisabled)
105
+ return;
106
+ if (isDisabled)
107
+ this._fileControl.disable();
108
+ else
109
+ this._fileControl.enable();
110
+ }
111
+ clear() {
112
+ // valueChanges subscription propagates this to `_value` (null) and to the
113
+ // form value.
114
+ this._fileControl.setValue(null);
115
+ }
116
+ openFileBrowse() {
117
+ const fileInput = document.createElement('input');
118
+ fileInput.setAttribute('type', 'file');
119
+ const cleanup = () => {
120
+ // Give the 'change' event a moment to fire before assuming the user
121
+ // canceled the dialog.
122
+ setTimeout(() => {
123
+ fileInput.removeEventListener('change', onFileChange);
124
+ document.body.removeEventListener('focus', onFocusReturned);
125
+ window.removeEventListener('focus', onFocusReturned);
126
+ }, 1000);
127
+ };
128
+ const onFileChange = (event) => {
129
+ const input = event.target;
130
+ if (input.files && input.files.length > 0) {
131
+ this._fileControl.setValue(input.files[0]);
132
+ }
133
+ cleanup();
134
+ };
135
+ fileInput.addEventListener('change', onFileChange);
136
+ // Detect file browser canceled without making a selection.
137
+ const onFocusReturned = () => cleanup();
138
+ document.body.addEventListener('focus', onFocusReturned);
139
+ window.addEventListener('focus', onFocusReturned);
140
+ fileInput.click();
141
+ }
142
+ _onFileDropped(files) {
143
+ for (const droppedFile of files) {
144
+ if (droppedFile.fileEntry.isFile) {
145
+ const fileEntry = droppedFile.fileEntry;
146
+ fileEntry.file((file) => this._fileControl.setValue(file));
147
+ break;
148
+ }
149
+ }
150
+ }
151
+ _setValue(value) {
152
+ this._value.set(value);
153
+ this._onChange(value);
154
+ this._onTouched();
155
+ }
156
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamSignatureInputImgComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
157
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: TheSeamSignatureInputImgComponent, isStandalone: true, selector: "seam-signature-input-img", providers: [
158
+ {
159
+ provide: NG_VALUE_ACCESSOR,
160
+ useExisting: TheSeamSignatureInputImgComponent,
161
+ multi: true,
162
+ },
163
+ ], ngImport: i0, template: "<div class=\"seam-signature-input-img\">\n <div class=\"seam-signature-input-img__upload-container\">\n <div class=\"seam-signature-input-img__header h-100\">\n <div\n class=\"seam-signature-input-img__upload-box h-100\"\n [class.has-preview]=\"!!_previewDataUrl()\"\n [style.background-image]=\"_previewBackgroundImage()\"\n tabindex=\"0\"\n (click)=\"openFileBrowse()\"\n (keydown.enter)=\"openFileBrowse()\"\n >\n <ngx-file-drop\n contentClassName=\"border-0\"\n dropZoneClassName=\"border-0\"\n (onFileDrop)=\"_onFileDropped($event)\"\n >\n <ng-template ngx-file-drop-content-tmp>\n @if (!_previewDataUrl()) {\n <div class=\"seam-signature-input-img__drop-prompt\">\n <strong>Choose a file</strong> or drag it here\n </div>\n }\n </ng-template>\n </ngx-file-drop>\n </div>\n\n @if (_sizeError(); as err) {\n <div class=\"seam-signature-input-img__size-error\">{{ err }}</div>\n }\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.seam-signature-input-img{border-bottom:1px solid black}.seam-signature-input-img__header{font-weight:500}.seam-signature-input-img__upload-container{height:174px;position:relative}.seam-signature-input-img__upload-box{position:relative;text-align:center;cursor:pointer;background-repeat:no-repeat;background-position:center;background-size:contain}.seam-signature-input-img__upload-box.has-preview{background-color:#fff}.seam-signature-input-img__size-error{color:var(--bs-danger, #dc3545)}ngx-file-drop ::ng-deep>div{height:100%}ngx-file-drop ::ng-deep .content{height:100%}ngx-file-drop ::ng-deep #dropZone{min-height:125px}\n"], dependencies: [{ kind: "ngmodule", type: NgxFileDropModule }, { kind: "component", type: i1.NgxFileDropComponent, selector: "ngx-file-drop", inputs: ["accept", "directory", "multiple", "dropZoneLabel", "dropZoneClassName", "useDragEnter", "contentClassName", "showBrowseBtn", "browseBtnClassName", "browseBtnLabel", "disabled"], outputs: ["onFileDrop", "onFileOver", "onFileLeave"] }, { kind: "directive", type: i1.NgxFileDropContentTemplateDirective, selector: "[ngx-file-drop-content-tmp]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
164
+ }
165
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamSignatureInputImgComponent, decorators: [{
166
+ type: Component,
167
+ args: [{ selector: 'seam-signature-input-img', imports: [NgxFileDropModule], providers: [
168
+ {
169
+ provide: NG_VALUE_ACCESSOR,
170
+ useExisting: TheSeamSignatureInputImgComponent,
171
+ multi: true,
172
+ },
173
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"seam-signature-input-img\">\n <div class=\"seam-signature-input-img__upload-container\">\n <div class=\"seam-signature-input-img__header h-100\">\n <div\n class=\"seam-signature-input-img__upload-box h-100\"\n [class.has-preview]=\"!!_previewDataUrl()\"\n [style.background-image]=\"_previewBackgroundImage()\"\n tabindex=\"0\"\n (click)=\"openFileBrowse()\"\n (keydown.enter)=\"openFileBrowse()\"\n >\n <ngx-file-drop\n contentClassName=\"border-0\"\n dropZoneClassName=\"border-0\"\n (onFileDrop)=\"_onFileDropped($event)\"\n >\n <ng-template ngx-file-drop-content-tmp>\n @if (!_previewDataUrl()) {\n <div class=\"seam-signature-input-img__drop-prompt\">\n <strong>Choose a file</strong> or drag it here\n </div>\n }\n </ng-template>\n </ngx-file-drop>\n </div>\n\n @if (_sizeError(); as err) {\n <div class=\"seam-signature-input-img__size-error\">{{ err }}</div>\n }\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.seam-signature-input-img{border-bottom:1px solid black}.seam-signature-input-img__header{font-weight:500}.seam-signature-input-img__upload-container{height:174px;position:relative}.seam-signature-input-img__upload-box{position:relative;text-align:center;cursor:pointer;background-repeat:no-repeat;background-position:center;background-size:contain}.seam-signature-input-img__upload-box.has-preview{background-color:#fff}.seam-signature-input-img__size-error{color:var(--bs-danger, #dc3545)}ngx-file-drop ::ng-deep>div{height:100%}ngx-file-drop ::ng-deep .content{height:100%}ngx-file-drop ::ng-deep #dropZone{min-height:125px}\n"] }]
174
+ }], ctorParameters: () => [] });
175
+
176
+ const DEFAULT_OPTIONS = {
177
+ canvasWidth: 500,
178
+ canvasHeight: 150,
179
+ };
180
+ class TheSeamSignatureInputPenComponent {
181
+ _container = inject(THESEAM_SIGNATURE_INPUT_CONTAINER, {
182
+ optional: true,
183
+ });
184
+ _destroyRef = inject(DestroyRef);
185
+ options = input(DEFAULT_OPTIONS, ...(ngDevMode ? [{ debugName: "options", transform: (value) => ({ ...DEFAULT_OPTIONS, ...value }) }] : [{ transform: (value) => ({ ...DEFAULT_OPTIONS, ...value }) }]));
186
+ beginDrawing = output();
187
+ endDrawing = output();
188
+ // Optional (not required) because writeValue may be called before the view
189
+ // is rendered — callers must null-check when reading.
190
+ _signaturePad = viewChild('sigPad', ...(ngDevMode ? [{ debugName: "_signaturePad" }] : []));
191
+ _value = signal(null, ...(ngDevMode ? [{ debugName: "_value" }] : []));
192
+ _disabled = signal(false, ...(ngDevMode ? [{ debugName: "_disabled" }] : []));
193
+ _canvasWidth = computed(() => this.options().canvasWidth ?? DEFAULT_OPTIONS.canvasWidth, ...(ngDevMode ? [{ debugName: "_canvasWidth" }] : []));
194
+ _canvasHeight = computed(() => this.options().canvasHeight ?? DEFAULT_OPTIONS.canvasHeight, ...(ngDevMode ? [{ debugName: "_canvasHeight" }] : []));
195
+ _onChange = () => undefined;
196
+ _onTouched = () => undefined;
197
+ constructor() {
198
+ if (this._container) {
199
+ this._container.registerInputItem('pen', this);
200
+ this._destroyRef.onDestroy(() => this._container?.unregisterInputItem('pen', this));
201
+ }
202
+ // Restore any value written before the canvas was ready (the common case
203
+ // when this component mounts via @switch with an existing form value).
204
+ afterNextRender(() => this._applyValueToPad());
205
+ }
206
+ writeValue(value) {
207
+ this._value.set(value);
208
+ this._applyValueToPad();
209
+ }
210
+ registerOnChange(fn) {
211
+ this._onChange = fn;
212
+ }
213
+ registerOnTouched(fn) {
214
+ this._onTouched = fn;
215
+ }
216
+ setDisabledState(isDisabled) {
217
+ this._disabled.set(isDisabled);
218
+ }
219
+ clear() {
220
+ this._signaturePad()?.clear();
221
+ this._setValue(null);
222
+ }
223
+ _drawStart(event) {
224
+ this.beginDrawing.emit(event);
225
+ }
226
+ _drawComplete(event) {
227
+ this._setValue(this._getDataURL());
228
+ this.endDrawing.emit(event);
229
+ }
230
+ _getDataURL() {
231
+ const pad = this._signaturePad();
232
+ if (!pad || pad.isEmpty()) {
233
+ return null;
234
+ }
235
+ return pad.toDataURL();
236
+ }
237
+ _applyValueToPad() {
238
+ const pad = this._signaturePad();
239
+ if (!pad) {
240
+ return;
241
+ }
242
+ try {
243
+ const value = this._value();
244
+ if (value) {
245
+ // fromDataURL is async and rejects on malformed input. Swallow errors
246
+ // so a corrupted stored value can't crash the component — the form
247
+ // control still retains the raw string.
248
+ pad.fromDataURL(value).catch(() => undefined);
249
+ }
250
+ else {
251
+ pad.clear();
252
+ }
253
+ }
254
+ catch {
255
+ // The SignaturePadComponent instance is available as soon as the view
256
+ // query resolves, but its internal signature_pad isn't constructed
257
+ // until ngAfterContentInit. writeValue can be called before that
258
+ // (during form-control wire-up). afterNextRender re-invokes this once
259
+ // the canvas is fully initialized.
260
+ }
261
+ }
262
+ _setValue(value) {
263
+ this._value.set(value);
264
+ this._onChange(value);
265
+ this._onTouched();
266
+ }
267
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamSignatureInputPenComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
268
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.3.15", type: TheSeamSignatureInputPenComponent, isStandalone: true, selector: "seam-signature-input-pen", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { beginDrawing: "beginDrawing", endDrawing: "endDrawing" }, providers: [
269
+ {
270
+ provide: NG_VALUE_ACCESSOR,
271
+ useExisting: TheSeamSignatureInputPenComponent,
272
+ multi: true,
273
+ },
274
+ ], viewQueries: [{ propertyName: "_signaturePad", first: true, predicate: ["sigPad"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"seam-signature-input-pen__canvas-wrap\"\n [style.width.px]=\"_canvasWidth()\"\n [style.height.px]=\"_canvasHeight()\"\n>\n <signature-pad\n #sigPad\n [options]=\"options()\"\n (drawStart)=\"_drawStart($event)\"\n (drawEnd)=\"_drawComplete($event)\"\n ></signature-pad>\n</div>\n", styles: [":host{display:block;overflow:hidden;position:relative}.seam-signature-input-pen__canvas-wrap{border-bottom:1px solid black}signature-pad{background:none!important}signature-pad ::ng-deep .signature-pad-canvas{border:none!important}\n"], dependencies: [{ kind: "ngmodule", type: AngularSignaturePadModule }, { kind: "component", type: i1$1.SignaturePadComponent, selector: "signature-pad", inputs: ["options"], outputs: ["drawStart", "drawBeforeUpdate", "drawAfterUpdate", "drawEnd"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
275
+ }
276
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamSignatureInputPenComponent, decorators: [{
277
+ type: Component,
278
+ args: [{ selector: 'seam-signature-input-pen', imports: [AngularSignaturePadModule], providers: [
279
+ {
280
+ provide: NG_VALUE_ACCESSOR,
281
+ useExisting: TheSeamSignatureInputPenComponent,
282
+ multi: true,
283
+ },
284
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"seam-signature-input-pen__canvas-wrap\"\n [style.width.px]=\"_canvasWidth()\"\n [style.height.px]=\"_canvasHeight()\"\n>\n <signature-pad\n #sigPad\n [options]=\"options()\"\n (drawStart)=\"_drawStart($event)\"\n (drawEnd)=\"_drawComplete($event)\"\n ></signature-pad>\n</div>\n", styles: [":host{display:block;overflow:hidden;position:relative}.seam-signature-input-pen__canvas-wrap{border-bottom:1px solid black}signature-pad{background:none!important}signature-pad ::ng-deep .signature-pad-canvas{border:none!important}\n"] }]
285
+ }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], beginDrawing: [{ type: i0.Output, args: ["beginDrawing"] }], endDrawing: [{ type: i0.Output, args: ["endDrawing"] }], _signaturePad: [{ type: i0.ViewChild, args: ['sigPad', { isSignal: true }] }] } });
286
+
287
+ const SIGNATURE_FONT_FAMILY = 'Homemade Apple';
288
+ const CANVAS_WIDTH = 500;
289
+ const CANVAS_HEIGHT = 127;
290
+ class TheSeamSignatureInputTextComponent {
291
+ _fontLoader = inject(TheSeamFontLoaderService);
292
+ _container = inject(THESEAM_SIGNATURE_INPUT_CONTAINER, {
293
+ optional: true,
294
+ });
295
+ _destroyRef = inject(DestroyRef);
296
+ // Optional (not required) because writeValue may be called before the
297
+ // canvas is in the DOM — callers must null-check when reading.
298
+ _typeCanvas = viewChild('typeCanvas', ...(ngDevMode ? [{ debugName: "_typeCanvas" }] : []));
299
+ _nameControl = new FormControl(null);
300
+ _fontState = signal('loading', ...(ngDevMode ? [{ debugName: "_fontState" }] : []));
301
+ _fontLoading = computed(() => this._fontState() === 'loading', ...(ngDevMode ? [{ debugName: "_fontLoading" }] : []));
302
+ _fontInactive = computed(() => this._fontState() === 'inactive', ...(ngDevMode ? [{ debugName: "_fontInactive" }] : []));
303
+ _fontNotActive = computed(() => this._fontState() !== 'active', ...(ngDevMode ? [{ debugName: "_fontNotActive" }] : []));
304
+ _disabled = signal(false, ...(ngDevMode ? [{ debugName: "_disabled" }] : []));
305
+ _disabledOrFontNotActive = computed(() => this._disabled() || this._fontNotActive(), ...(ngDevMode ? [{ debugName: "_disabledOrFontNotActive" }] : []));
306
+ _canvasWidth = CANVAS_WIDTH;
307
+ _canvasHeight = CANVAS_HEIGHT;
308
+ /** Current form value (rendered bitmap data URL). */
309
+ _value = null;
310
+ /**
311
+ * When the user last typed a name (if any) on this instance. `null` means
312
+ * there is no user-typed signature on this instance — the canvas should
313
+ * keep whatever image was restored from `_value`.
314
+ */
315
+ _renderedText = null;
316
+ _onChange = () => undefined;
317
+ _onTouched = () => undefined;
318
+ constructor() {
319
+ if (this._container) {
320
+ this._container.registerInputItem('text', this);
321
+ this._destroyRef.onDestroy(() => this._container?.unregisterInputItem('text', this));
322
+ }
323
+ this._fontLoader
324
+ .load({ google: { families: [SIGNATURE_FONT_FAMILY] } })
325
+ .pipe(takeUntilDestroyed())
326
+ .subscribe((e) => {
327
+ if (e.type === 'loading' ||
328
+ e.type === 'inactive' ||
329
+ e.type === 'active') {
330
+ this._fontState.set(e.type);
331
+ }
332
+ });
333
+ // Debounced redraw of the user-typed name.
334
+ this._nameControl.valueChanges
335
+ .pipe(debounceTime(100), takeUntilDestroyed())
336
+ .subscribe(() => this._drawTextToCanvas());
337
+ // When the font flips to active and a name has been typed on this
338
+ // instance, redraw the text so the now-loaded font replaces the fallback.
339
+ // We do NOT touch the canvas otherwise — if there's a restored image but
340
+ // no typed name, this effect must leave the image alone.
341
+ effect(() => {
342
+ if (this._fontState() === 'active' && this._renderedText) {
343
+ this._drawTextToCanvas();
344
+ }
345
+ });
346
+ }
347
+ ngAfterViewInit() {
348
+ // Paint any previously-written form value onto the canvas now that the
349
+ // view is in place. writeValue may have been invoked earlier as part of
350
+ // form-control wire-up, before the canvas existed.
351
+ if (this._value) {
352
+ this._drawImageToCanvas(this._value);
353
+ }
354
+ }
355
+ writeValue(value) {
356
+ this._value = value;
357
+ this._renderedText = null;
358
+ // emitEvent: false so the debounced valueChanges subscriber doesn't fire
359
+ // and redraw as text (which would clobber the restored image).
360
+ this._nameControl.setValue(null, { emitEvent: false });
361
+ this._drawImageToCanvas(value);
362
+ }
363
+ registerOnChange(fn) {
364
+ this._onChange = fn;
365
+ }
366
+ registerOnTouched(fn) {
367
+ this._onTouched = fn;
368
+ }
369
+ setDisabledState(isDisabled) {
370
+ this._disabled.set(isDisabled);
371
+ }
372
+ clear() {
373
+ this._clearCanvas();
374
+ this._renderedText = null;
375
+ this._nameControl.setValue(null, { emitEvent: false });
376
+ this._setValue(null);
377
+ }
378
+ _onKeyDownEnter() {
379
+ this._drawTextToCanvas();
380
+ }
381
+ _onNameInputBlur() {
382
+ this._drawTextToCanvas();
383
+ }
384
+ /**
385
+ * Paint the given data URL onto the canvas as an image. Used when the
386
+ * component mounts with a pre-existing form value — the user's typed name
387
+ * isn't recoverable from the rendered bitmap, only the bitmap itself.
388
+ */
389
+ _drawImageToCanvas(dataUrl) {
390
+ const canvas = this._typeCanvas()?.nativeElement;
391
+ if (!canvas) {
392
+ return;
393
+ }
394
+ const context = canvas.getContext('2d');
395
+ if (!context) {
396
+ return;
397
+ }
398
+ context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
399
+ if (!dataUrl) {
400
+ return;
401
+ }
402
+ const img = new Image();
403
+ img.onload = () => {
404
+ context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
405
+ context.drawImage(img, 0, 0);
406
+ };
407
+ img.src = dataUrl;
408
+ }
409
+ /**
410
+ * Render `_nameControl.value` as text onto the canvas and publish the
411
+ * result as the form value. A null/empty name clears both.
412
+ */
413
+ _drawTextToCanvas() {
414
+ const canvas = this._typeCanvas()?.nativeElement;
415
+ if (!canvas) {
416
+ return;
417
+ }
418
+ const context = canvas.getContext('2d');
419
+ if (!context) {
420
+ return;
421
+ }
422
+ const text = this._nameControl.value;
423
+ if (!text || text.trim().length === 0) {
424
+ // Only clear if the user had previously rendered text on this instance.
425
+ // Otherwise we'd wipe a restored image that writeValue just painted.
426
+ if (this._renderedText !== null) {
427
+ context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
428
+ this._renderedText = null;
429
+ this._setValue(null);
430
+ }
431
+ return;
432
+ }
433
+ context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
434
+ context.fillStyle = 'black';
435
+ context.textBaseline = 'bottom';
436
+ context.textAlign = 'center';
437
+ let fontsize = 60;
438
+ do {
439
+ fontsize--;
440
+ context.font = `${fontsize}px ${SIGNATURE_FONT_FAMILY}, cursive`;
441
+ } while (context.measureText(text).width > CANVAS_WIDTH);
442
+ context.fillText(text, CANVAS_WIDTH / 2, 100);
443
+ this._renderedText = text;
444
+ this._setValue(canvas.toDataURL());
445
+ }
446
+ _clearCanvas() {
447
+ const context = this._typeCanvas()?.nativeElement.getContext('2d');
448
+ context?.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
449
+ }
450
+ _setValue(value) {
451
+ if (this._value === value) {
452
+ return;
453
+ }
454
+ this._value = value;
455
+ this._onChange(value);
456
+ this._onTouched();
457
+ }
458
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamSignatureInputTextComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
459
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: TheSeamSignatureInputTextComponent, isStandalone: true, selector: "seam-signature-input-text", providers: [
460
+ {
461
+ provide: NG_VALUE_ACCESSOR,
462
+ useExisting: TheSeamSignatureInputTextComponent,
463
+ multi: true,
464
+ },
465
+ ], viewQueries: [{ propertyName: "_typeCanvas", first: true, predicate: ["typeCanvas"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"seam-signature-input-text\">\n <div class=\"seam-signature-input-text__header d-flex flex-row\">\n <seam-form-field\n [inline]=\"true\"\n class=\"flex-grow-1 mr-2\"\n [numPaddingErrors]=\"0\"\n >\n <span *seamFormFieldLabelTpl class=\"text-nowrap mr-2\"\n >Enter Full Name:</span\n >\n <input\n seamInput\n [formControl]=\"_nameControl\"\n class=\"pl-1 flex-grow-1\"\n autocomplete=\"off\"\n (keydown.enter)=\"_onKeyDownEnter()\"\n (blur)=\"_onNameInputBlur()\"\n [seamDisableControl]=\"_disabledOrFontNotActive()\"\n seamAutoFocus\n />\n </seam-form-field>\n </div>\n\n <div\n class=\"seam-signature-input-text__canvas-wrap overflow-hidden position-relative\"\n [style.height.px]=\"_canvasHeight\"\n >\n @if (_fontLoading()) {\n <div class=\"seam-signature-input-text__status\">Font Loading</div>\n }\n\n @if (_fontInactive()) {\n <div class=\"seam-signature-input-text__status text-warning\">\n <div class=\"seam-signature-input-text__status-headline\">\n Font failed to load.\n </div>\n <div>\n To use the typed signature, try refreshing the page or using another\n web browser. If you still have issues please contact support and let\n us know.\n </div>\n </div>\n }\n\n <canvas\n #typeCanvas\n class=\"position-absolute\"\n [attr.width]=\"_canvasWidth\"\n [attr.height]=\"_canvasHeight\"\n [style.top.px]=\"0\"\n [style.left.px]=\"0\"\n ></canvas>\n </div>\n</div>\n", styles: [":host{display:block}.seam-signature-input-text{border-bottom:1px solid black}.seam-signature-input-text__header{font-weight:500}.seam-signature-input-text__status{display:block;position:absolute;width:100%;padding-top:1rem;text-align:center;font-size:20px}.seam-signature-input-text__status-headline{font-size:20px}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: TheSeamFormFieldModule }, { kind: "component", type: i2$1.TheSeamFormFieldComponent, selector: "seam-form-field", inputs: ["inline", "label", "labelPosition", "labelClass", "maxErrors", "numPaddingErrors", "labelId", "helpText", "helpTextId"] }, { kind: "directive", type: i2$1.InputDirective, selector: "input[seamInput], textarea[seamInput], ng-select[seamInput], seam-tel-input[seamInput], quill-editor[seamInput], seam-google-maps[seamInput], seam-rich-text[seamInput]", inputs: ["seamInputSize", "id", "type", "placeholder", "required", "disabled", "readonly"], exportAs: ["seamInput"] }, { kind: "directive", type: i2$1.FormFieldLabelTplDirective, selector: "[seamFormFieldLabelTpl]" }, { kind: "directive", type: TheSeamAutoFocusDirective, selector: "[seamAutoFocus]", inputs: ["seamAutoFocus"], exportAs: ["seamAutoFocus"] }, { kind: "directive", type: TheSeamDisableControlDirective, selector: "[seamDisableControl]", inputs: ["seamDisableControl"], exportAs: ["seamDisableControl"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
466
+ }
467
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamSignatureInputTextComponent, decorators: [{
468
+ type: Component,
469
+ args: [{ selector: 'seam-signature-input-text', imports: [
470
+ ReactiveFormsModule,
471
+ TheSeamFormFieldModule,
472
+ TheSeamAutoFocusDirective,
473
+ TheSeamDisableControlDirective,
474
+ ], providers: [
475
+ {
476
+ provide: NG_VALUE_ACCESSOR,
477
+ useExisting: TheSeamSignatureInputTextComponent,
478
+ multi: true,
479
+ },
480
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"seam-signature-input-text\">\n <div class=\"seam-signature-input-text__header d-flex flex-row\">\n <seam-form-field\n [inline]=\"true\"\n class=\"flex-grow-1 mr-2\"\n [numPaddingErrors]=\"0\"\n >\n <span *seamFormFieldLabelTpl class=\"text-nowrap mr-2\"\n >Enter Full Name:</span\n >\n <input\n seamInput\n [formControl]=\"_nameControl\"\n class=\"pl-1 flex-grow-1\"\n autocomplete=\"off\"\n (keydown.enter)=\"_onKeyDownEnter()\"\n (blur)=\"_onNameInputBlur()\"\n [seamDisableControl]=\"_disabledOrFontNotActive()\"\n seamAutoFocus\n />\n </seam-form-field>\n </div>\n\n <div\n class=\"seam-signature-input-text__canvas-wrap overflow-hidden position-relative\"\n [style.height.px]=\"_canvasHeight\"\n >\n @if (_fontLoading()) {\n <div class=\"seam-signature-input-text__status\">Font Loading</div>\n }\n\n @if (_fontInactive()) {\n <div class=\"seam-signature-input-text__status text-warning\">\n <div class=\"seam-signature-input-text__status-headline\">\n Font failed to load.\n </div>\n <div>\n To use the typed signature, try refreshing the page or using another\n web browser. If you still have issues please contact support and let\n us know.\n </div>\n </div>\n }\n\n <canvas\n #typeCanvas\n class=\"position-absolute\"\n [attr.width]=\"_canvasWidth\"\n [attr.height]=\"_canvasHeight\"\n [style.top.px]=\"0\"\n [style.left.px]=\"0\"\n ></canvas>\n </div>\n</div>\n", styles: [":host{display:block}.seam-signature-input-text{border-bottom:1px solid black}.seam-signature-input-text__header{font-weight:500}.seam-signature-input-text__status{display:block;position:absolute;width:100%;padding-top:1rem;text-align:center;font-size:20px}.seam-signature-input-text__status-headline{font-size:20px}\n"] }]
481
+ }], ctorParameters: () => [], propDecorators: { _typeCanvas: [{ type: i0.ViewChild, args: ['typeCanvas', { isSignal: true }] }] } });
482
+
483
+ const isValueEmpty = (value) => typeof value !== 'string' || value.trim().length === 0;
484
+ class TheSeamSignatureInputPanelComponent {
485
+ _layout = inject(TheSeamLayoutService);
486
+ _modalRef = inject(ModalRef, { optional: true });
487
+ /** Emitted when the panel is submitted or canceled. */
488
+ result = output();
489
+ _faSignature = faSignature;
490
+ _faUpload = faUpload;
491
+ _faKeyboard = faKeyboard;
492
+ _activeType = signal('pen', ...(ngDevMode ? [{ debugName: "_activeType" }] : []));
493
+ _resetType = computed(() => this._activeType() === 'img' ? 'delete' : 'cancel', ...(ngDevMode ? [{ debugName: "_resetType" }] : []));
494
+ _form = new FormGroup({
495
+ pen: new FormControl(null),
496
+ text: new FormControl(null),
497
+ img: new FormControl(null),
498
+ });
499
+ _registeredInputItems = new Map();
500
+ // Observables are used here because the active control changes, which makes
501
+ // a pure-signal approach awkward (signals don't naturally switch between
502
+ // observable sources). We derive the active control, its value, and its
503
+ // validity via rxjs, then expose the results as signals for the template.
504
+ _activeControl$ = toObservable(this._activeType).pipe(map((type) => this._form.controls[type]));
505
+ _activeValue$ = this._activeControl$.pipe(switchMap$1((control) => observeControlValue(control)), map((value) => (isValueEmpty(value) ? null : value)));
506
+ _canSubmit$ = combineLatest([
507
+ this._activeControl$.pipe(switchMap$1((c) => observeControlValid(c))),
508
+ this._activeValue$,
509
+ ]).pipe(map(([valid, value]) => valid && value !== null));
510
+ _value = toSignal(this._activeValue$, {
511
+ initialValue: null,
512
+ });
513
+ _valueEmpty = computed(() => this._value() === null, ...(ngDevMode ? [{ debugName: "_valueEmpty" }] : []));
514
+ _canSubmit = toSignal(this._canSubmit$, {
515
+ initialValue: false,
516
+ });
517
+ _isSm = toSignal(this._layout.observe('sm'), {
518
+ initialValue: false,
519
+ });
520
+ showType(type) {
521
+ this._activeType.set(type);
522
+ }
523
+ registerInputItem(type, item) {
524
+ if (this._registeredInputItems.has(type)) {
525
+ if (isDevMode()) {
526
+ // eslint-disable-next-line no-console
527
+ console.warn(`[TheSeamSignatureInputPanelComponent] Input item '${type}' is already registered.`);
528
+ }
529
+ return false;
530
+ }
531
+ this._registeredInputItems.set(type, item);
532
+ return true;
533
+ }
534
+ unregisterInputItem(type, item) {
535
+ const registered = this._registeredInputItems.get(type);
536
+ if (!registered) {
537
+ if (isDevMode()) {
538
+ // eslint-disable-next-line no-console
539
+ console.warn(`[TheSeamSignatureInputPanelComponent] Input item '${type}' can't be unregistered.`);
540
+ }
541
+ return false;
542
+ }
543
+ if (isDevMode() && registered !== item) {
544
+ // eslint-disable-next-line no-console
545
+ console.warn(`[TheSeamSignatureInputPanelComponent] Registered item for type '${type}' doesn't match the item being unregistered.`);
546
+ }
547
+ this._registeredInputItems.delete(type);
548
+ return true;
549
+ }
550
+ reset() {
551
+ this._form.reset({ pen: null, text: null, img: null });
552
+ }
553
+ _onClearBtnClick(event) {
554
+ event.preventDefault();
555
+ event.stopPropagation();
556
+ this._registeredInputItems.get(this._activeType())?.clear();
557
+ }
558
+ _onCancelBtnClick(event) {
559
+ event.preventDefault();
560
+ event.stopPropagation();
561
+ this.reset();
562
+ this._emit({ type: 'cancel' });
563
+ }
564
+ _onSubmitBtnClick(event) {
565
+ event.preventDefault();
566
+ event.stopPropagation();
567
+ if (!this._canSubmit()) {
568
+ return;
569
+ }
570
+ const value = this._value();
571
+ if (value === null) {
572
+ return;
573
+ }
574
+ this._emit({ type: 'submit', value });
575
+ }
576
+ _emit(result) {
577
+ this.result.emit(result);
578
+ this._modalRef?.close(result);
579
+ }
580
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamSignatureInputPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
581
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: TheSeamSignatureInputPanelComponent, isStandalone: true, selector: "seam-signature-input-panel", outputs: { result: "result" }, host: { properties: { "attr.tabIndex": "-1" } }, providers: [
582
+ {
583
+ provide: THESEAM_SIGNATURE_INPUT_CONTAINER,
584
+ useExisting: TheSeamSignatureInputPanelComponent,
585
+ },
586
+ ], ngImport: i0, template: "<div class=\"seam-signature-input-panel\">\n <div class=\"seam-signature-input-panel__content\" cdkTrapFocus>\n <div class=\"seam-signature-input-panel__options pb-2 mb-1\">\n <button\n seamButton\n theme=\"primary\"\n [class.btn-sm]=\"_isSm()\"\n class=\"text-nowrap mr-1\"\n [class.active]=\"_activeType() === 'pen'\"\n (click)=\"showType('pen')\"\n >\n <seam-icon [icon]=\"_faSignature\" size=\"sm\"></seam-icon> Draw\n </button>\n <button\n seamButton\n theme=\"primary\"\n [class.btn-sm]=\"_isSm()\"\n class=\"text-nowrap mx-1\"\n [class.active]=\"_activeType() === 'text'\"\n (click)=\"showType('text')\"\n >\n <seam-icon [icon]=\"_faKeyboard\" size=\"sm\"></seam-icon> Type\n </button>\n <button\n seamButton\n theme=\"primary\"\n [class.btn-sm]=\"_isSm()\"\n class=\"text-nowrap ml-1\"\n [class.active]=\"_activeType() === 'img'\"\n (click)=\"showType('img')\"\n >\n <seam-icon [icon]=\"_faUpload\" size=\"sm\"></seam-icon> Upload\n </button>\n </div>\n\n <form [formGroup]=\"_form\">\n @switch (_activeType()) {\n @case ('pen') {\n <seam-signature-input-pen\n formControlName=\"pen\"\n ></seam-signature-input-pen>\n }\n @case ('text') {\n <seam-signature-input-text\n formControlName=\"text\"\n ></seam-signature-input-text>\n }\n @case ('img') {\n <seam-signature-input-img\n formControlName=\"img\"\n ></seam-signature-input-img>\n }\n }\n </form>\n\n <div class=\"seam-signature-input-panel__footer mt-1\">\n <div>\n @if (_resetType() === 'delete') {\n <button\n seamButton\n theme=\"danger\"\n [class.btn-sm]=\"_isSm()\"\n [disabled]=\"_valueEmpty()\"\n (click)=\"_onClearBtnClick($event)\"\n >\n Delete\n </button>\n } @else {\n <button\n seamButton\n theme=\"lightgray\"\n [class.btn-sm]=\"_isSm()\"\n [disabled]=\"_valueEmpty()\"\n (click)=\"_onClearBtnClick($event)\"\n >\n Clear\n </button>\n }\n </div>\n <div>\n <button\n seamButton\n theme=\"lightgray\"\n [class.btn-sm]=\"_isSm()\"\n seamAutoFocus\n (click)=\"_onCancelBtnClick($event)\"\n >\n Cancel\n </button>\n <button\n seamButton\n theme=\"primary\"\n [class.btn-sm]=\"_isSm()\"\n [disabled]=\"!_canSubmit()\"\n (click)=\"_onSubmitBtnClick($event)\"\n >\n Apply Signature\n </button>\n </div>\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.seam-signature-input-panel{background-color:#fff;border-radius:4px}.seam-signature-input-panel__options{padding:8px 8px 0;border-bottom:1px solid black;display:flex;flex-direction:row;justify-content:flex-end}.seam-signature-input-panel__footer{padding:0 8px 8px;display:flex;flex-direction:row;justify-content:space-between;margin-top:2px}.seam-signature-input-panel__footer>div>*:not(:last-child){margin-right:6px}.seam-signature-input-panel__footer button{height:36px;margin-top:6px}\n"], dependencies: [{ kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i1$2.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: TheSeamButtonsModule }, { kind: "component", type: i3.TheSeamButtonComponent, selector: "button[seamButton]", inputs: ["disabled", "theme", "size", "type"], exportAs: ["seamButton"] }, { kind: "ngmodule", type: TheSeamIconModule }, { kind: "component", type: i4.IconComponent, selector: "seam-icon", inputs: ["grayscaleOnDisable", "disabled", "iconClass", "icon", "size", "showDefaultOnError", "defaultIcon", "iconType"] }, { kind: "directive", type: TheSeamAutoFocusDirective, selector: "[seamAutoFocus]", inputs: ["seamAutoFocus"], exportAs: ["seamAutoFocus"] }, { kind: "component", type: TheSeamSignatureInputPenComponent, selector: "seam-signature-input-pen", inputs: ["options"], outputs: ["beginDrawing", "endDrawing"] }, { kind: "component", type: TheSeamSignatureInputTextComponent, selector: "seam-signature-input-text" }, { kind: "component", type: TheSeamSignatureInputImgComponent, selector: "seam-signature-input-img" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
587
+ }
588
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamSignatureInputPanelComponent, decorators: [{
589
+ type: Component,
590
+ args: [{ selector: 'seam-signature-input-panel', imports: [
591
+ A11yModule,
592
+ ReactiveFormsModule,
593
+ TheSeamButtonsModule,
594
+ TheSeamIconModule,
595
+ TheSeamAutoFocusDirective,
596
+ TheSeamSignatureInputPenComponent,
597
+ TheSeamSignatureInputTextComponent,
598
+ TheSeamSignatureInputImgComponent,
599
+ ], providers: [
600
+ {
601
+ provide: THESEAM_SIGNATURE_INPUT_CONTAINER,
602
+ useExisting: TheSeamSignatureInputPanelComponent,
603
+ },
604
+ ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
605
+ /**
606
+ * Allow focusing the host element via `focus()` without it being part of the
607
+ * tab order (the inner inputs handle keyboard navigation).
608
+ */
609
+ '[attr.tabIndex]': '-1',
610
+ }, template: "<div class=\"seam-signature-input-panel\">\n <div class=\"seam-signature-input-panel__content\" cdkTrapFocus>\n <div class=\"seam-signature-input-panel__options pb-2 mb-1\">\n <button\n seamButton\n theme=\"primary\"\n [class.btn-sm]=\"_isSm()\"\n class=\"text-nowrap mr-1\"\n [class.active]=\"_activeType() === 'pen'\"\n (click)=\"showType('pen')\"\n >\n <seam-icon [icon]=\"_faSignature\" size=\"sm\"></seam-icon> Draw\n </button>\n <button\n seamButton\n theme=\"primary\"\n [class.btn-sm]=\"_isSm()\"\n class=\"text-nowrap mx-1\"\n [class.active]=\"_activeType() === 'text'\"\n (click)=\"showType('text')\"\n >\n <seam-icon [icon]=\"_faKeyboard\" size=\"sm\"></seam-icon> Type\n </button>\n <button\n seamButton\n theme=\"primary\"\n [class.btn-sm]=\"_isSm()\"\n class=\"text-nowrap ml-1\"\n [class.active]=\"_activeType() === 'img'\"\n (click)=\"showType('img')\"\n >\n <seam-icon [icon]=\"_faUpload\" size=\"sm\"></seam-icon> Upload\n </button>\n </div>\n\n <form [formGroup]=\"_form\">\n @switch (_activeType()) {\n @case ('pen') {\n <seam-signature-input-pen\n formControlName=\"pen\"\n ></seam-signature-input-pen>\n }\n @case ('text') {\n <seam-signature-input-text\n formControlName=\"text\"\n ></seam-signature-input-text>\n }\n @case ('img') {\n <seam-signature-input-img\n formControlName=\"img\"\n ></seam-signature-input-img>\n }\n }\n </form>\n\n <div class=\"seam-signature-input-panel__footer mt-1\">\n <div>\n @if (_resetType() === 'delete') {\n <button\n seamButton\n theme=\"danger\"\n [class.btn-sm]=\"_isSm()\"\n [disabled]=\"_valueEmpty()\"\n (click)=\"_onClearBtnClick($event)\"\n >\n Delete\n </button>\n } @else {\n <button\n seamButton\n theme=\"lightgray\"\n [class.btn-sm]=\"_isSm()\"\n [disabled]=\"_valueEmpty()\"\n (click)=\"_onClearBtnClick($event)\"\n >\n Clear\n </button>\n }\n </div>\n <div>\n <button\n seamButton\n theme=\"lightgray\"\n [class.btn-sm]=\"_isSm()\"\n seamAutoFocus\n (click)=\"_onCancelBtnClick($event)\"\n >\n Cancel\n </button>\n <button\n seamButton\n theme=\"primary\"\n [class.btn-sm]=\"_isSm()\"\n [disabled]=\"!_canSubmit()\"\n (click)=\"_onSubmitBtnClick($event)\"\n >\n Apply Signature\n </button>\n </div>\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.seam-signature-input-panel{background-color:#fff;border-radius:4px}.seam-signature-input-panel__options{padding:8px 8px 0;border-bottom:1px solid black;display:flex;flex-direction:row;justify-content:flex-end}.seam-signature-input-panel__footer{padding:0 8px 8px;display:flex;flex-direction:row;justify-content:space-between;margin-top:2px}.seam-signature-input-panel__footer>div>*:not(:last-child){margin-right:6px}.seam-signature-input-panel__footer button{height:36px;margin-top:6px}\n"] }]
611
+ }], propDecorators: { result: [{ type: i0.Output, args: ["result"] }] } });
612
+
613
+ class TheSeamSignatureInputImgHarness extends ComponentHarness {
614
+ static hostSelector = 'seam-signature-input-img';
615
+ _fileDrop = this.locatorFor('ngx-file-drop');
616
+ _sizeError = this.locatorForOptional('.seam-signature-input-img__size-error');
617
+ _preview = this.locatorForOptional('.seam-signature-input-img__preview');
618
+ async getSizeError() {
619
+ const el = await this._sizeError();
620
+ return el ? (await el.text()).trim() : null;
621
+ }
622
+ async hasPreview() {
623
+ return (await this._preview()) !== null;
624
+ }
625
+ async getPreviewSrc() {
626
+ const el = await this._preview();
627
+ return el ? el.getAttribute('src') : null;
628
+ }
629
+ async getFileDrop() {
630
+ return this._fileDrop();
631
+ }
632
+ }
633
+
634
+ class TheSeamSignatureInputPenHarness extends ComponentHarness {
635
+ static hostSelector = 'seam-signature-input-pen';
636
+ _canvas = this.locatorFor('canvas');
637
+ async getCanvas() {
638
+ return this._canvas();
639
+ }
640
+ async getCanvasWidth() {
641
+ const canvas = await this._canvas();
642
+ const attr = await canvas.getAttribute('width');
643
+ return attr ? Number(attr) : null;
644
+ }
645
+ async getCanvasHeight() {
646
+ const canvas = await this._canvas();
647
+ const attr = await canvas.getAttribute('height');
648
+ return attr ? Number(attr) : null;
649
+ }
650
+ }
651
+
652
+ class TheSeamSignatureInputTextHarness extends ComponentHarness {
653
+ static hostSelector = 'seam-signature-input-text';
654
+ _input = this.locatorFor('input');
655
+ _canvas = this.locatorFor('canvas');
656
+ async getInput() {
657
+ return this._input();
658
+ }
659
+ async enterName(text) {
660
+ const input = await this._input();
661
+ await input.clear();
662
+ await input.sendKeys(text);
663
+ }
664
+ async getInputValue() {
665
+ const input = await this._input();
666
+ return input.getProperty('value');
667
+ }
668
+ async isInputDisabled() {
669
+ const input = await this._input();
670
+ return (await input.getAttribute('disabled')) !== null;
671
+ }
672
+ async getCanvas() {
673
+ return this._canvas();
674
+ }
675
+ }
676
+
677
+ class TheSeamSignatureInputPanelHarness extends ComponentHarness {
678
+ static hostSelector = 'seam-signature-input-panel';
679
+ _drawBtn = this.locatorFor('button:nth-of-type(1)');
680
+ _typeBtn = this.locatorFor('button:nth-of-type(2)');
681
+ _uploadBtn = this.locatorFor('button:nth-of-type(3)');
682
+ _footerBtns = this.locatorForAll('.seam-signature-input-panel__footer button');
683
+ _penHarness = this.locatorForOptional(TheSeamSignatureInputPenHarness);
684
+ _textHarness = this.locatorForOptional(TheSeamSignatureInputTextHarness);
685
+ _imgHarness = this.locatorForOptional(TheSeamSignatureInputImgHarness);
686
+ async showType(type) {
687
+ const btn = type === 'pen'
688
+ ? await this._drawBtn()
689
+ : type === 'text'
690
+ ? await this._typeBtn()
691
+ : await this._uploadBtn();
692
+ await btn.click();
693
+ }
694
+ async getActiveType() {
695
+ if (await this._penHarness())
696
+ return 'pen';
697
+ if (await this._textHarness())
698
+ return 'text';
699
+ if (await this._imgHarness())
700
+ return 'img';
701
+ return null;
702
+ }
703
+ async getPen() {
704
+ return this._penHarness();
705
+ }
706
+ async getText() {
707
+ return this._textHarness();
708
+ }
709
+ async getImg() {
710
+ return this._imgHarness();
711
+ }
712
+ async getClearOrDeleteButton() {
713
+ const [btn] = await this._footerBtns();
714
+ return btn;
715
+ }
716
+ async getCancelButton() {
717
+ const buttons = await this._footerBtns();
718
+ return buttons[buttons.length - 2];
719
+ }
720
+ async getSubmitButton() {
721
+ const buttons = await this._footerBtns();
722
+ return buttons[buttons.length - 1];
723
+ }
724
+ async isSubmitDisabled() {
725
+ const btn = await this.getSubmitButton();
726
+ return (await btn.getAttribute('disabled')) !== null;
727
+ }
728
+ async cancel() {
729
+ const btn = await this.getCancelButton();
730
+ await btn.click();
731
+ }
732
+ async submit() {
733
+ const btn = await this.getSubmitButton();
734
+ await btn.click();
735
+ }
736
+ }
737
+
738
+ /**
739
+ * Generated bundle index. Do not edit.
740
+ */
741
+
742
+ export { THESEAM_SIGNATURE_INPUT_CONTAINER, TheSeamSignatureInputImgComponent, TheSeamSignatureInputImgHarness, TheSeamSignatureInputPanelComponent, TheSeamSignatureInputPanelHarness, TheSeamSignatureInputPenComponent, TheSeamSignatureInputPenHarness, TheSeamSignatureInputTextComponent, TheSeamSignatureInputTextHarness };
743
+ //# sourceMappingURL=theseam-ui-common-signature-input.mjs.map