@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.
- package/fesm2022/theseam-ui-common-signature-input.mjs +743 -0
- package/fesm2022/theseam-ui-common-signature-input.mjs.map +1 -0
- package/fesm2022/theseam-ui-common-story-helpers.mjs +118 -55
- package/fesm2022/theseam-ui-common-story-helpers.mjs.map +1 -1
- package/package.json +5 -1
- package/signature-input/index.d.ts +241 -0
- package/signature-input/package.json +3 -0
- package/story-helpers/index.d.ts +60 -23
|
@@ -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
|