ngx-form-draft 2.2.11 → 2.2.13
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/LICENSE +21 -21
- package/README.md +206 -206
- package/esm2020/form-draft-banner.component.mjs +84 -84
- package/esm2020/form-draft.directive.mjs +267 -267
- package/esm2020/form-draft.service.mjs +92 -92
- package/esm2020/index.mjs +4 -4
- package/esm2020/ngx-form-draft.mjs +4 -4
- package/esm2020/ngx-form-draft.module.mjs +19 -19
- package/fesm2015/ngx-form-draft.mjs +446 -446
- package/fesm2015/ngx-form-draft.mjs.map +1 -1
- package/fesm2020/ngx-form-draft.mjs +439 -439
- package/fesm2020/ngx-form-draft.mjs.map +1 -1
- package/form-draft-banner.component.d.ts +14 -14
- package/form-draft.directive.d.ts +56 -56
- package/form-draft.service.d.ts +35 -35
- package/index.d.ts +4 -4
- package/ngx-form-draft.module.d.ts +9 -9
- package/package.json +1 -1
|
@@ -7,460 +7,460 @@ import { filter, debounceTime, takeUntil } from 'rxjs/operators';
|
|
|
7
7
|
import * as i1 from '@angular/common';
|
|
8
8
|
import { CommonModule } from '@angular/common';
|
|
9
9
|
|
|
10
|
-
class FormDraftBannerComponent {
|
|
11
|
-
constructor() {
|
|
12
|
-
this.visible = false;
|
|
13
|
-
this.timeLabel = '';
|
|
14
|
-
this.isRestored = false;
|
|
15
|
-
this.restoredText = 'Draft restored';
|
|
16
|
-
this.savedText = 'Draft saved';
|
|
17
|
-
this.savedLabel = 'saved';
|
|
18
|
-
this.discardText = 'Discard';
|
|
19
|
-
this.discard = new EventEmitter();
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
FormDraftBannerComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftBannerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
23
|
-
FormDraftBannerComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: FormDraftBannerComponent, selector: "ngx-form-draft-banner", inputs: { visible: "visible", timeLabel: "timeLabel", isRestored: "isRestored", restoredText: "restoredText", savedText: "savedText", savedLabel: "savedLabel", discardText: "discardText" }, outputs: { discard: "discard" }, ngImport: i0, template: `
|
|
24
|
-
<div class="form-draft-banner" *ngIf="visible">
|
|
25
|
-
<div class="form-draft-banner__icon">
|
|
26
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
27
|
-
<path d="M12 8V12L14.5 14.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
28
|
-
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
|
|
29
|
-
</svg>
|
|
30
|
-
</div>
|
|
31
|
-
<div class="form-draft-banner__content">
|
|
32
|
-
<span class="form-draft-banner__text">
|
|
33
|
-
<span *ngIf="isRestored">{{ restoredText }}</span>
|
|
34
|
-
<span *ngIf="!isRestored">{{ savedText }}</span>
|
|
35
|
-
<span class="form-draft-banner__time" *ngIf="timeLabel && isRestored">
|
|
36
|
-
· {{ savedLabel }} {{ timeLabel }}
|
|
37
|
-
</span>
|
|
38
|
-
</span>
|
|
39
|
-
</div>
|
|
40
|
-
<div class="form-draft-banner__actions">
|
|
41
|
-
<button class="form-draft-banner__btn form-draft-banner__btn--discard" (click)="discard.emit()" type="button">
|
|
42
|
-
✕ {{ discardText }}
|
|
43
|
-
</button>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
`, isInline: true, styles: [":host{display:block;width:100%}.form-draft-banner{display:flex;align-items:center;gap:10px;padding:10px 14px;margin-bottom:12px;border-radius:8px;background:linear-gradient(135deg,#eef6ff 0%,#f0f4ff 100%);border:1px solid #c5ddf8;box-shadow:0 2px 8px #128ad614;animation:slideDown .25s cubic-bezier(.4,0,.2,1)}@keyframes slideDown{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.form-draft-banner__icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:6px;background:linear-gradient(135deg,#128ad6,#22b9ff);color:#fff;flex-shrink:0}.form-draft-banner__content{flex:1;min-width:0}.form-draft-banner__text{font-size:13px;font-weight:600;color:#1a3a5c}.form-draft-banner__time{font-weight:400;color:#6b8aaa;font-size:12px}.form-draft-banner__actions{flex-shrink:0}.form-draft-banner__btn{border:none;padding:5px 12px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s ease}.form-draft-banner__btn--discard{background:transparent;color:#8899a6;border:1px solid #d0dce6}.form-draft-banner__btn--discard:hover{background:#fef2f2;color:#e62e43;border-color:#e62e43}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
47
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftBannerComponent, decorators: [{
|
|
48
|
-
type: Component,
|
|
49
|
-
args: [{ selector: 'ngx-form-draft-banner', template: `
|
|
50
|
-
<div class="form-draft-banner" *ngIf="visible">
|
|
51
|
-
<div class="form-draft-banner__icon">
|
|
52
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
53
|
-
<path d="M12 8V12L14.5 14.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
54
|
-
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
|
|
55
|
-
</svg>
|
|
56
|
-
</div>
|
|
57
|
-
<div class="form-draft-banner__content">
|
|
58
|
-
<span class="form-draft-banner__text">
|
|
59
|
-
<span *ngIf="isRestored">{{ restoredText }}</span>
|
|
60
|
-
<span *ngIf="!isRestored">{{ savedText }}</span>
|
|
61
|
-
<span class="form-draft-banner__time" *ngIf="timeLabel && isRestored">
|
|
62
|
-
· {{ savedLabel }} {{ timeLabel }}
|
|
63
|
-
</span>
|
|
64
|
-
</span>
|
|
65
|
-
</div>
|
|
66
|
-
<div class="form-draft-banner__actions">
|
|
67
|
-
<button class="form-draft-banner__btn form-draft-banner__btn--discard" (click)="discard.emit()" type="button">
|
|
68
|
-
✕ {{ discardText }}
|
|
69
|
-
</button>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:100%}.form-draft-banner{display:flex;align-items:center;gap:10px;padding:10px 14px;margin-bottom:12px;border-radius:8px;background:linear-gradient(135deg,#eef6ff 0%,#f0f4ff 100%);border:1px solid #c5ddf8;box-shadow:0 2px 8px #128ad614;animation:slideDown .25s cubic-bezier(.4,0,.2,1)}@keyframes slideDown{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.form-draft-banner__icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:6px;background:linear-gradient(135deg,#128ad6,#22b9ff);color:#fff;flex-shrink:0}.form-draft-banner__content{flex:1;min-width:0}.form-draft-banner__text{font-size:13px;font-weight:600;color:#1a3a5c}.form-draft-banner__time{font-weight:400;color:#6b8aaa;font-size:12px}.form-draft-banner__actions{flex-shrink:0}.form-draft-banner__btn{border:none;padding:5px 12px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s ease}.form-draft-banner__btn--discard{background:transparent;color:#8899a6;border:1px solid #d0dce6}.form-draft-banner__btn--discard:hover{background:#fef2f2;color:#e62e43;border-color:#e62e43}\n"] }]
|
|
73
|
-
}], propDecorators: { visible: [{
|
|
74
|
-
type: Input
|
|
75
|
-
}], timeLabel: [{
|
|
76
|
-
type: Input
|
|
77
|
-
}], isRestored: [{
|
|
78
|
-
type: Input
|
|
79
|
-
}], restoredText: [{
|
|
80
|
-
type: Input
|
|
81
|
-
}], savedText: [{
|
|
82
|
-
type: Input
|
|
83
|
-
}], savedLabel: [{
|
|
84
|
-
type: Input
|
|
85
|
-
}], discardText: [{
|
|
86
|
-
type: Input
|
|
87
|
-
}], discard: [{
|
|
88
|
-
type: Output
|
|
10
|
+
class FormDraftBannerComponent {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.visible = false;
|
|
13
|
+
this.timeLabel = '';
|
|
14
|
+
this.isRestored = false;
|
|
15
|
+
this.restoredText = 'Draft restored';
|
|
16
|
+
this.savedText = 'Draft saved';
|
|
17
|
+
this.savedLabel = 'saved';
|
|
18
|
+
this.discardText = 'Discard';
|
|
19
|
+
this.discard = new EventEmitter();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
FormDraftBannerComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftBannerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
23
|
+
FormDraftBannerComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: FormDraftBannerComponent, selector: "ngx-form-draft-banner", inputs: { visible: "visible", timeLabel: "timeLabel", isRestored: "isRestored", restoredText: "restoredText", savedText: "savedText", savedLabel: "savedLabel", discardText: "discardText" }, outputs: { discard: "discard" }, ngImport: i0, template: `
|
|
24
|
+
<div class="form-draft-banner" *ngIf="visible">
|
|
25
|
+
<div class="form-draft-banner__icon">
|
|
26
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
27
|
+
<path d="M12 8V12L14.5 14.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
28
|
+
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
|
|
29
|
+
</svg>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="form-draft-banner__content">
|
|
32
|
+
<span class="form-draft-banner__text">
|
|
33
|
+
<span *ngIf="isRestored">{{ restoredText }}</span>
|
|
34
|
+
<span *ngIf="!isRestored">{{ savedText }}</span>
|
|
35
|
+
<span class="form-draft-banner__time" *ngIf="timeLabel && isRestored">
|
|
36
|
+
· {{ savedLabel }} {{ timeLabel }}
|
|
37
|
+
</span>
|
|
38
|
+
</span>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="form-draft-banner__actions">
|
|
41
|
+
<button class="form-draft-banner__btn form-draft-banner__btn--discard" (click)="discard.emit()" type="button">
|
|
42
|
+
✕ {{ discardText }}
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
`, isInline: true, styles: [":host{display:block;width:100%}.form-draft-banner{display:flex;align-items:center;gap:10px;padding:10px 14px;margin-bottom:12px;border-radius:8px;background:linear-gradient(135deg,#eef6ff 0%,#f0f4ff 100%);border:1px solid #c5ddf8;box-shadow:0 2px 8px #128ad614;animation:slideDown .25s cubic-bezier(.4,0,.2,1)}@keyframes slideDown{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.form-draft-banner__icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:6px;background:linear-gradient(135deg,#128ad6,#22b9ff);color:#fff;flex-shrink:0}.form-draft-banner__content{flex:1;min-width:0}.form-draft-banner__text{font-size:13px;font-weight:600;color:#1a3a5c}.form-draft-banner__time{font-weight:400;color:#6b8aaa;font-size:12px}.form-draft-banner__actions{flex-shrink:0}.form-draft-banner__btn{border:none;padding:5px 12px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s ease}.form-draft-banner__btn--discard{background:transparent;color:#8899a6;border:1px solid #d0dce6}.form-draft-banner__btn--discard:hover{background:#fef2f2;color:#e62e43;border-color:#e62e43}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
47
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftBannerComponent, decorators: [{
|
|
48
|
+
type: Component,
|
|
49
|
+
args: [{ selector: 'ngx-form-draft-banner', template: `
|
|
50
|
+
<div class="form-draft-banner" *ngIf="visible">
|
|
51
|
+
<div class="form-draft-banner__icon">
|
|
52
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
53
|
+
<path d="M12 8V12L14.5 14.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
54
|
+
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
|
|
55
|
+
</svg>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="form-draft-banner__content">
|
|
58
|
+
<span class="form-draft-banner__text">
|
|
59
|
+
<span *ngIf="isRestored">{{ restoredText }}</span>
|
|
60
|
+
<span *ngIf="!isRestored">{{ savedText }}</span>
|
|
61
|
+
<span class="form-draft-banner__time" *ngIf="timeLabel && isRestored">
|
|
62
|
+
· {{ savedLabel }} {{ timeLabel }}
|
|
63
|
+
</span>
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="form-draft-banner__actions">
|
|
67
|
+
<button class="form-draft-banner__btn form-draft-banner__btn--discard" (click)="discard.emit()" type="button">
|
|
68
|
+
✕ {{ discardText }}
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:100%}.form-draft-banner{display:flex;align-items:center;gap:10px;padding:10px 14px;margin-bottom:12px;border-radius:8px;background:linear-gradient(135deg,#eef6ff 0%,#f0f4ff 100%);border:1px solid #c5ddf8;box-shadow:0 2px 8px #128ad614;animation:slideDown .25s cubic-bezier(.4,0,.2,1)}@keyframes slideDown{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.form-draft-banner__icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:6px;background:linear-gradient(135deg,#128ad6,#22b9ff);color:#fff;flex-shrink:0}.form-draft-banner__content{flex:1;min-width:0}.form-draft-banner__text{font-size:13px;font-weight:600;color:#1a3a5c}.form-draft-banner__time{font-weight:400;color:#6b8aaa;font-size:12px}.form-draft-banner__actions{flex-shrink:0}.form-draft-banner__btn{border:none;padding:5px 12px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s ease}.form-draft-banner__btn--discard{background:transparent;color:#8899a6;border:1px solid #d0dce6}.form-draft-banner__btn--discard:hover{background:#fef2f2;color:#e62e43;border-color:#e62e43}\n"] }]
|
|
73
|
+
}], propDecorators: { visible: [{
|
|
74
|
+
type: Input
|
|
75
|
+
}], timeLabel: [{
|
|
76
|
+
type: Input
|
|
77
|
+
}], isRestored: [{
|
|
78
|
+
type: Input
|
|
79
|
+
}], restoredText: [{
|
|
80
|
+
type: Input
|
|
81
|
+
}], savedText: [{
|
|
82
|
+
type: Input
|
|
83
|
+
}], savedLabel: [{
|
|
84
|
+
type: Input
|
|
85
|
+
}], discardText: [{
|
|
86
|
+
type: Input
|
|
87
|
+
}], discard: [{
|
|
88
|
+
type: Output
|
|
89
89
|
}] } });
|
|
90
90
|
|
|
91
|
-
class FormDraftService {
|
|
92
|
-
constructor() {
|
|
93
|
-
this.STORAGE_PREFIX = 'form_draft_';
|
|
94
|
-
this.MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
95
|
-
/** Registry of formId -> reset callback (used by directive to reset form + banner) */
|
|
96
|
-
this.resetRegistry = new Map();
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Register a callback to reset the form and draft UI for the given formId.
|
|
100
|
-
* Called by the directive; apps should use clearAndReset(formId) instead.
|
|
101
|
-
*/
|
|
102
|
-
registerReset(formId, resetFn) {
|
|
103
|
-
this.resetRegistry.set(formId, resetFn);
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Unregister the reset callback for the given formId.
|
|
107
|
-
* Called by the directive on destroy.
|
|
108
|
-
*/
|
|
109
|
-
unregisterReset(formId) {
|
|
110
|
-
this.resetRegistry.delete(formId);
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Clear the draft from storage and, if a directive is registered for this formId,
|
|
114
|
-
* reset the form and dismiss the draft banner in one call.
|
|
115
|
-
* Use this on form submit to clear local storage and reset the form in one line.
|
|
116
|
-
*/
|
|
117
|
-
clearAndReset(formId) {
|
|
118
|
-
var _a;
|
|
119
|
-
this.clear(formId);
|
|
120
|
-
(_a = this.resetRegistry.get(formId)) === null || _a === void 0 ? void 0 : _a();
|
|
121
|
-
}
|
|
122
|
-
buildKey(formId) {
|
|
123
|
-
return `${this.STORAGE_PREFIX}${formId}`;
|
|
124
|
-
}
|
|
125
|
-
save(formId, values) {
|
|
126
|
-
try {
|
|
127
|
-
const draft = { values, savedAt: Date.now(), formId };
|
|
128
|
-
localStorage.setItem(this.buildKey(formId), JSON.stringify(draft));
|
|
129
|
-
}
|
|
130
|
-
catch (e) {
|
|
131
|
-
console.warn('[FormDraft] Could not save draft:', e);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
load(formId) {
|
|
135
|
-
try {
|
|
136
|
-
const raw = localStorage.getItem(this.buildKey(formId));
|
|
137
|
-
if (!raw)
|
|
138
|
-
return null;
|
|
139
|
-
const draft = JSON.parse(raw);
|
|
140
|
-
if (Date.now() - draft.savedAt > this.MAX_AGE_MS) {
|
|
141
|
-
this.clear(formId);
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
return draft;
|
|
145
|
-
}
|
|
146
|
-
catch (e) {
|
|
147
|
-
console.warn('[FormDraft] Could not load draft:', e);
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
clear(formId) {
|
|
152
|
-
try {
|
|
153
|
-
localStorage.removeItem(this.buildKey(formId));
|
|
154
|
-
}
|
|
155
|
-
catch (e) {
|
|
156
|
-
console.warn('[FormDraft] Could not clear draft:', e);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
formatTimestamp(timestamp) {
|
|
160
|
-
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
161
|
-
if (seconds < 60)
|
|
162
|
-
return 'just now';
|
|
163
|
-
const minutes = Math.floor(seconds / 60);
|
|
164
|
-
if (minutes < 60)
|
|
165
|
-
return `${minutes}m ago`;
|
|
166
|
-
const hours = Math.floor(minutes / 60);
|
|
167
|
-
if (hours < 24)
|
|
168
|
-
return `${hours}h ago`;
|
|
169
|
-
const days = Math.floor(hours / 24);
|
|
170
|
-
return `${days}d ago`;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
FormDraftService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
174
|
-
FormDraftService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, providedIn: 'root' });
|
|
175
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, decorators: [{
|
|
176
|
-
type: Injectable,
|
|
177
|
-
args: [{
|
|
178
|
-
providedIn: 'root'
|
|
179
|
-
}]
|
|
91
|
+
class FormDraftService {
|
|
92
|
+
constructor() {
|
|
93
|
+
this.STORAGE_PREFIX = 'form_draft_';
|
|
94
|
+
this.MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
95
|
+
/** Registry of formId -> reset callback (used by directive to reset form + banner) */
|
|
96
|
+
this.resetRegistry = new Map();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Register a callback to reset the form and draft UI for the given formId.
|
|
100
|
+
* Called by the directive; apps should use clearAndReset(formId) instead.
|
|
101
|
+
*/
|
|
102
|
+
registerReset(formId, resetFn) {
|
|
103
|
+
this.resetRegistry.set(formId, resetFn);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Unregister the reset callback for the given formId.
|
|
107
|
+
* Called by the directive on destroy.
|
|
108
|
+
*/
|
|
109
|
+
unregisterReset(formId) {
|
|
110
|
+
this.resetRegistry.delete(formId);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Clear the draft from storage and, if a directive is registered for this formId,
|
|
114
|
+
* reset the form and dismiss the draft banner in one call.
|
|
115
|
+
* Use this on form submit to clear local storage and reset the form in one line.
|
|
116
|
+
*/
|
|
117
|
+
clearAndReset(formId) {
|
|
118
|
+
var _a;
|
|
119
|
+
this.clear(formId);
|
|
120
|
+
(_a = this.resetRegistry.get(formId)) === null || _a === void 0 ? void 0 : _a();
|
|
121
|
+
}
|
|
122
|
+
buildKey(formId) {
|
|
123
|
+
return `${this.STORAGE_PREFIX}${formId}`;
|
|
124
|
+
}
|
|
125
|
+
save(formId, values) {
|
|
126
|
+
try {
|
|
127
|
+
const draft = { values, savedAt: Date.now(), formId };
|
|
128
|
+
localStorage.setItem(this.buildKey(formId), JSON.stringify(draft));
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
console.warn('[FormDraft] Could not save draft:', e);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
load(formId) {
|
|
135
|
+
try {
|
|
136
|
+
const raw = localStorage.getItem(this.buildKey(formId));
|
|
137
|
+
if (!raw)
|
|
138
|
+
return null;
|
|
139
|
+
const draft = JSON.parse(raw);
|
|
140
|
+
if (Date.now() - draft.savedAt > this.MAX_AGE_MS) {
|
|
141
|
+
this.clear(formId);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return draft;
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
console.warn('[FormDraft] Could not load draft:', e);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
clear(formId) {
|
|
152
|
+
try {
|
|
153
|
+
localStorage.removeItem(this.buildKey(formId));
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
console.warn('[FormDraft] Could not clear draft:', e);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
formatTimestamp(timestamp) {
|
|
160
|
+
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
161
|
+
if (seconds < 60)
|
|
162
|
+
return 'just now';
|
|
163
|
+
const minutes = Math.floor(seconds / 60);
|
|
164
|
+
if (minutes < 60)
|
|
165
|
+
return `${minutes}m ago`;
|
|
166
|
+
const hours = Math.floor(minutes / 60);
|
|
167
|
+
if (hours < 24)
|
|
168
|
+
return `${hours}h ago`;
|
|
169
|
+
const days = Math.floor(hours / 24);
|
|
170
|
+
return `${days}d ago`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
FormDraftService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
174
|
+
FormDraftService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, providedIn: 'root' });
|
|
175
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftService, decorators: [{
|
|
176
|
+
type: Injectable,
|
|
177
|
+
args: [{
|
|
178
|
+
providedIn: 'root'
|
|
179
|
+
}]
|
|
180
180
|
}] });
|
|
181
181
|
|
|
182
|
-
/**
|
|
183
|
-
* Auto-saves and restores form drafts
|
|
184
|
-
*
|
|
185
|
-
* @example
|
|
186
|
-
* <form [formGroup]="myForm" ngxFormDraft="myFormId">
|
|
187
|
-
*
|
|
188
|
-
* @example
|
|
189
|
-
* <form [formGroup]="myForm" [ngxFormDraft]="'edit_' + entityId" [draftExcludeFields]="['password']">
|
|
190
|
-
*/
|
|
191
|
-
class FormDraftDirective {
|
|
192
|
-
constructor(formGroupDir, ngForm, draftService, viewContainerRef, cdRef, elRef, renderer) {
|
|
193
|
-
this.formGroupDir = formGroupDir;
|
|
194
|
-
this.ngForm = ngForm;
|
|
195
|
-
this.draftService = draftService;
|
|
196
|
-
this.viewContainerRef = viewContainerRef;
|
|
197
|
-
this.cdRef = cdRef;
|
|
198
|
-
this.elRef = elRef;
|
|
199
|
-
this.renderer = renderer;
|
|
200
|
-
this.draftDebounce = 800;
|
|
201
|
-
this.draftExcludeFields = [];
|
|
202
|
-
this.draftShowOnChange = false;
|
|
203
|
-
this.draftRestoredText = 'Draft restored';
|
|
204
|
-
this.draftSavedText = 'Draft saved';
|
|
205
|
-
this.draftSavedLabel = 'saved';
|
|
206
|
-
this.draftDiscardText = 'Discard';
|
|
207
|
-
this.destroy$ = new Subject();
|
|
208
|
-
this.bannerRef = null;
|
|
209
|
-
this.formControl = null;
|
|
210
|
-
this.initialValues = {};
|
|
211
|
-
this.isRestoring = false;
|
|
212
|
-
}
|
|
213
|
-
ngOnInit() {
|
|
214
|
-
var _a, _b;
|
|
215
|
-
this.formControl = ((_a = this.formGroupDir) === null || _a === void 0 ? void 0 : _a.form) || ((_b = this.ngForm) === null || _b === void 0 ? void 0 : _b.form) || null;
|
|
216
|
-
if (!this.formControl || !this.formId)
|
|
217
|
-
return;
|
|
218
|
-
this.draftService.registerReset(this.formId, () => this.performResetAndDestroyBanner());
|
|
219
|
-
// For reactive forms, capture initial values and restore draft immediately
|
|
220
|
-
if (this.formGroupDir) {
|
|
221
|
-
this.initialValues = JSON.parse(JSON.stringify(this.formControl.value));
|
|
222
|
-
const draft = this.draftService.load(this.formId);
|
|
223
|
-
if (draft) {
|
|
224
|
-
this.restoreDraft(draft.values);
|
|
225
|
-
this.showBanner(draft.savedAt, true);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
let hasUserInteraction = false;
|
|
229
|
-
this.formControl.valueChanges
|
|
230
|
-
.pipe(filter(() => {
|
|
231
|
-
var _a;
|
|
232
|
-
if (this.isRestoring)
|
|
233
|
-
return false;
|
|
234
|
-
if (this.ngForm && !hasUserInteraction) {
|
|
235
|
-
const currentValues = ((_a = this.formControl) === null || _a === void 0 ? void 0 : _a.value) || {};
|
|
236
|
-
const isDifferent = JSON.stringify(currentValues) !== JSON.stringify(this.initialValues);
|
|
237
|
-
if (isDifferent) {
|
|
238
|
-
hasUserInteraction = true;
|
|
239
|
-
return true;
|
|
240
|
-
}
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
return true;
|
|
244
|
-
}), debounceTime(this.draftDebounce), takeUntil(this.destroy$))
|
|
245
|
-
.subscribe((values) => {
|
|
246
|
-
this.saveDraft(values);
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
ngAfterViewInit() {
|
|
250
|
-
if (this.ngForm && this.formControl) {
|
|
251
|
-
const draft = this.draftService.load(this.formId);
|
|
252
|
-
if (draft) {
|
|
253
|
-
this.isRestoring = true;
|
|
254
|
-
}
|
|
255
|
-
setTimeout(() => {
|
|
256
|
-
if (draft) {
|
|
257
|
-
this.restoreDraft(draft.values);
|
|
258
|
-
this.showBanner(draft.savedAt, true);
|
|
259
|
-
this.initialValues = {};
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
this.initialValues = JSON.parse(JSON.stringify(this.formControl.value));
|
|
263
|
-
}
|
|
264
|
-
}, 0);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
ngOnDestroy() {
|
|
268
|
-
this.draftService.unregisterReset(this.formId);
|
|
269
|
-
this.destroy$.next();
|
|
270
|
-
this.destroy$.complete();
|
|
271
|
-
this.destroyBanner();
|
|
272
|
-
}
|
|
273
|
-
saveDraft(values) {
|
|
274
|
-
const filtered = this.filterFields(values);
|
|
275
|
-
// Don't save if empty
|
|
276
|
-
if (this.isAllEmpty(filtered)) {
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
// Don't save if matches initial values (even if initial is empty)
|
|
280
|
-
if (this.matchesInitialValues(filtered)) {
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
this.draftService.save(this.formId, filtered);
|
|
284
|
-
if (this.draftShowOnChange && !this.bannerRef) {
|
|
285
|
-
this.showBanner(Date.now(), false);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
filterFields(values) {
|
|
289
|
-
if (!this.draftExcludeFields.length)
|
|
290
|
-
return values;
|
|
291
|
-
const result = Object.assign({}, values);
|
|
292
|
-
this.draftExcludeFields.forEach(field => delete result[field]);
|
|
293
|
-
return result;
|
|
294
|
-
}
|
|
295
|
-
isAllEmpty(values) {
|
|
296
|
-
return Object.values(values).every(v => v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0));
|
|
297
|
-
}
|
|
298
|
-
matchesInitialValues(values) {
|
|
299
|
-
return JSON.stringify(values) === JSON.stringify(this.initialValues);
|
|
300
|
-
}
|
|
301
|
-
restoreDraft(values) {
|
|
302
|
-
var _a, _b;
|
|
303
|
-
if (!this.formControl)
|
|
304
|
-
return;
|
|
305
|
-
this.isRestoring = true;
|
|
306
|
-
const form = ((_a = this.formGroupDir) === null || _a === void 0 ? void 0 : _a.form) || ((_b = this.ngForm) === null || _b === void 0 ? void 0 : _b.form);
|
|
307
|
-
if (form) {
|
|
308
|
-
this.prepareFormArrays(form, values);
|
|
309
|
-
form.patchValue(values);
|
|
310
|
-
}
|
|
311
|
-
setTimeout(() => this.isRestoring = false, 100);
|
|
312
|
-
}
|
|
313
|
-
prepareFormArrays(control, value) {
|
|
314
|
-
if (!control || value == null)
|
|
315
|
-
return;
|
|
316
|
-
if (control instanceof FormGroup && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
317
|
-
Object.keys(value).forEach(key => {
|
|
318
|
-
const childControl = control.get(key);
|
|
319
|
-
if (childControl)
|
|
320
|
-
this.prepareFormArrays(childControl, value[key]);
|
|
321
|
-
});
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
if (control instanceof FormArray && Array.isArray(value)) {
|
|
325
|
-
const formArray = control;
|
|
326
|
-
while (formArray.length < value.length) {
|
|
327
|
-
const template = formArray.at(0);
|
|
328
|
-
if (template instanceof FormGroup) {
|
|
329
|
-
const newGroup = new FormGroup({});
|
|
330
|
-
Object.keys(template.controls).forEach(ctrlName => {
|
|
331
|
-
const existing = template.get(ctrlName);
|
|
332
|
-
if (existing instanceof FormArray) {
|
|
333
|
-
newGroup.addControl(ctrlName, new FormArray([]));
|
|
334
|
-
}
|
|
335
|
-
else if (existing instanceof FormGroup) {
|
|
336
|
-
newGroup.addControl(ctrlName, new FormGroup({}));
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
newGroup.addControl(ctrlName, new FormControl(null));
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
formArray.push(newGroup);
|
|
343
|
-
}
|
|
344
|
-
else if (template) {
|
|
345
|
-
formArray.push(new FormControl(null));
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
const firstValue = value[0];
|
|
349
|
-
if (firstValue && typeof firstValue === 'object' && !Array.isArray(firstValue)) {
|
|
350
|
-
const group = new FormGroup({});
|
|
351
|
-
Object.keys(firstValue).forEach(key => group.addControl(key, new FormControl(null)));
|
|
352
|
-
formArray.push(group);
|
|
353
|
-
}
|
|
354
|
-
else {
|
|
355
|
-
formArray.push(new FormControl(null));
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
while (formArray.length > value.length) {
|
|
360
|
-
formArray.removeAt(formArray.length - 1);
|
|
361
|
-
}
|
|
362
|
-
value.forEach((childValue, index) => {
|
|
363
|
-
this.prepareFormArrays(formArray.at(index), childValue);
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
discardDraft() {
|
|
368
|
-
this.draftService.clear(this.formId);
|
|
369
|
-
this.performResetAndDestroyBanner();
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* Resets the form to initial values and destroys the draft banner.
|
|
373
|
-
* Used by the service when clearAndReset(formId) is called (e.g. on submit).
|
|
374
|
-
*/
|
|
375
|
-
performResetAndDestroyBanner() {
|
|
376
|
-
var _a, _b;
|
|
377
|
-
if (this.formControl) {
|
|
378
|
-
this.isRestoring = true;
|
|
379
|
-
const form = ((_a = this.formGroupDir) === null || _a === void 0 ? void 0 : _a.form) || ((_b = this.ngForm) === null || _b === void 0 ? void 0 : _b.form);
|
|
380
|
-
if (form) {
|
|
381
|
-
this.prepareFormArrays(form, this.initialValues);
|
|
382
|
-
form.reset(this.initialValues);
|
|
383
|
-
}
|
|
384
|
-
setTimeout(() => {
|
|
385
|
-
this.isRestoring = false;
|
|
386
|
-
}, this.draftDebounce + 200);
|
|
387
|
-
}
|
|
388
|
-
this.destroyBanner();
|
|
389
|
-
}
|
|
390
|
-
showBanner(savedAt, isRestored = false) {
|
|
391
|
-
this.bannerRef = this.viewContainerRef.createComponent(FormDraftBannerComponent);
|
|
392
|
-
this.bannerRef.setInput('visible', true);
|
|
393
|
-
this.bannerRef.setInput('isRestored', isRestored);
|
|
394
|
-
this.bannerRef.setInput('timeLabel', isRestored ? this.draftService.formatTimestamp(savedAt) : '');
|
|
395
|
-
this.bannerRef.setInput('restoredText', this.draftRestoredText);
|
|
396
|
-
this.bannerRef.setInput('savedText', this.draftSavedText);
|
|
397
|
-
this.bannerRef.setInput('savedLabel', this.draftSavedLabel);
|
|
398
|
-
this.bannerRef.setInput('discardText', this.draftDiscardText);
|
|
399
|
-
this.bannerRef.instance.discard.subscribe(() => this.discardDraft());
|
|
400
|
-
const bannerEl = this.bannerRef.location.nativeElement;
|
|
401
|
-
const formEl = this.elRef.nativeElement;
|
|
402
|
-
this.renderer.insertBefore(formEl, bannerEl, formEl.firstChild);
|
|
403
|
-
this.cdRef.detectChanges();
|
|
404
|
-
}
|
|
405
|
-
destroyBanner() {
|
|
406
|
-
if (this.bannerRef) {
|
|
407
|
-
this.bannerRef.destroy();
|
|
408
|
-
this.bannerRef = null;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
clearDraft() {
|
|
412
|
-
this.draftService.clear(this.formId);
|
|
413
|
-
this.destroyBanner();
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
FormDraftDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftDirective, deps: [{ token: i1$1.FormGroupDirective, optional: true }, { token: i1$1.NgForm, optional: true }, { token: FormDraftService }, { token: i0.ViewContainerRef }, { token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
|
|
417
|
-
FormDraftDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.3.0", type: FormDraftDirective, selector: "[ngxFormDraft]", inputs: { formId: ["ngxFormDraft", "formId"], draftDebounce: "draftDebounce", draftExcludeFields: "draftExcludeFields", draftShowOnChange: "draftShowOnChange", draftRestoredText: "draftRestoredText", draftSavedText: "draftSavedText", draftSavedLabel: "draftSavedLabel", draftDiscardText: "draftDiscardText" }, ngImport: i0 });
|
|
418
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftDirective, decorators: [{
|
|
419
|
-
type: Directive,
|
|
420
|
-
args: [{
|
|
421
|
-
selector: '[ngxFormDraft]',
|
|
422
|
-
}]
|
|
423
|
-
}], ctorParameters: function () {
|
|
424
|
-
return [{ type: i1$1.FormGroupDirective, decorators: [{
|
|
425
|
-
type: Optional
|
|
426
|
-
}] }, { type: i1$1.NgForm, decorators: [{
|
|
427
|
-
type: Optional
|
|
428
|
-
}] }, { type: FormDraftService }, { type: i0.ViewContainerRef }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i0.Renderer2 }];
|
|
429
|
-
}, propDecorators: { formId: [{
|
|
430
|
-
type: Input,
|
|
431
|
-
args: ['ngxFormDraft']
|
|
432
|
-
}], draftDebounce: [{
|
|
433
|
-
type: Input
|
|
434
|
-
}], draftExcludeFields: [{
|
|
435
|
-
type: Input
|
|
436
|
-
}], draftShowOnChange: [{
|
|
437
|
-
type: Input
|
|
438
|
-
}], draftRestoredText: [{
|
|
439
|
-
type: Input
|
|
440
|
-
}], draftSavedText: [{
|
|
441
|
-
type: Input
|
|
442
|
-
}], draftSavedLabel: [{
|
|
443
|
-
type: Input
|
|
444
|
-
}], draftDiscardText: [{
|
|
445
|
-
type: Input
|
|
182
|
+
/**
|
|
183
|
+
* Auto-saves and restores form drafts
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* <form [formGroup]="myForm" ngxFormDraft="myFormId">
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* <form [formGroup]="myForm" [ngxFormDraft]="'edit_' + entityId" [draftExcludeFields]="['password']">
|
|
190
|
+
*/
|
|
191
|
+
class FormDraftDirective {
|
|
192
|
+
constructor(formGroupDir, ngForm, draftService, viewContainerRef, cdRef, elRef, renderer) {
|
|
193
|
+
this.formGroupDir = formGroupDir;
|
|
194
|
+
this.ngForm = ngForm;
|
|
195
|
+
this.draftService = draftService;
|
|
196
|
+
this.viewContainerRef = viewContainerRef;
|
|
197
|
+
this.cdRef = cdRef;
|
|
198
|
+
this.elRef = elRef;
|
|
199
|
+
this.renderer = renderer;
|
|
200
|
+
this.draftDebounce = 800;
|
|
201
|
+
this.draftExcludeFields = [];
|
|
202
|
+
this.draftShowOnChange = false;
|
|
203
|
+
this.draftRestoredText = 'Draft restored';
|
|
204
|
+
this.draftSavedText = 'Draft saved';
|
|
205
|
+
this.draftSavedLabel = 'saved';
|
|
206
|
+
this.draftDiscardText = 'Discard';
|
|
207
|
+
this.destroy$ = new Subject();
|
|
208
|
+
this.bannerRef = null;
|
|
209
|
+
this.formControl = null;
|
|
210
|
+
this.initialValues = {};
|
|
211
|
+
this.isRestoring = false;
|
|
212
|
+
}
|
|
213
|
+
ngOnInit() {
|
|
214
|
+
var _a, _b;
|
|
215
|
+
this.formControl = ((_a = this.formGroupDir) === null || _a === void 0 ? void 0 : _a.form) || ((_b = this.ngForm) === null || _b === void 0 ? void 0 : _b.form) || null;
|
|
216
|
+
if (!this.formControl || !this.formId)
|
|
217
|
+
return;
|
|
218
|
+
this.draftService.registerReset(this.formId, () => this.performResetAndDestroyBanner());
|
|
219
|
+
// For reactive forms, capture initial values and restore draft immediately
|
|
220
|
+
if (this.formGroupDir) {
|
|
221
|
+
this.initialValues = JSON.parse(JSON.stringify(this.formControl.value));
|
|
222
|
+
const draft = this.draftService.load(this.formId);
|
|
223
|
+
if (draft) {
|
|
224
|
+
this.restoreDraft(draft.values);
|
|
225
|
+
this.showBanner(draft.savedAt, true);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
let hasUserInteraction = false;
|
|
229
|
+
this.formControl.valueChanges
|
|
230
|
+
.pipe(filter(() => {
|
|
231
|
+
var _a;
|
|
232
|
+
if (this.isRestoring)
|
|
233
|
+
return false;
|
|
234
|
+
if (this.ngForm && !hasUserInteraction) {
|
|
235
|
+
const currentValues = ((_a = this.formControl) === null || _a === void 0 ? void 0 : _a.value) || {};
|
|
236
|
+
const isDifferent = JSON.stringify(currentValues) !== JSON.stringify(this.initialValues);
|
|
237
|
+
if (isDifferent) {
|
|
238
|
+
hasUserInteraction = true;
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
}), debounceTime(this.draftDebounce), takeUntil(this.destroy$))
|
|
245
|
+
.subscribe((values) => {
|
|
246
|
+
this.saveDraft(values);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
ngAfterViewInit() {
|
|
250
|
+
if (this.ngForm && this.formControl) {
|
|
251
|
+
const draft = this.draftService.load(this.formId);
|
|
252
|
+
if (draft) {
|
|
253
|
+
this.isRestoring = true;
|
|
254
|
+
}
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
if (draft) {
|
|
257
|
+
this.restoreDraft(draft.values);
|
|
258
|
+
this.showBanner(draft.savedAt, true);
|
|
259
|
+
this.initialValues = {};
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
this.initialValues = JSON.parse(JSON.stringify(this.formControl.value));
|
|
263
|
+
}
|
|
264
|
+
}, 0);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
ngOnDestroy() {
|
|
268
|
+
this.draftService.unregisterReset(this.formId);
|
|
269
|
+
this.destroy$.next();
|
|
270
|
+
this.destroy$.complete();
|
|
271
|
+
this.destroyBanner();
|
|
272
|
+
}
|
|
273
|
+
saveDraft(values) {
|
|
274
|
+
const filtered = this.filterFields(values);
|
|
275
|
+
// Don't save if empty
|
|
276
|
+
if (this.isAllEmpty(filtered)) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
// Don't save if matches initial values (even if initial is empty)
|
|
280
|
+
if (this.matchesInitialValues(filtered)) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
this.draftService.save(this.formId, filtered);
|
|
284
|
+
if (this.draftShowOnChange && !this.bannerRef) {
|
|
285
|
+
this.showBanner(Date.now(), false);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
filterFields(values) {
|
|
289
|
+
if (!this.draftExcludeFields.length)
|
|
290
|
+
return values;
|
|
291
|
+
const result = Object.assign({}, values);
|
|
292
|
+
this.draftExcludeFields.forEach(field => delete result[field]);
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
isAllEmpty(values) {
|
|
296
|
+
return Object.values(values).every(v => v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0));
|
|
297
|
+
}
|
|
298
|
+
matchesInitialValues(values) {
|
|
299
|
+
return JSON.stringify(values) === JSON.stringify(this.initialValues);
|
|
300
|
+
}
|
|
301
|
+
restoreDraft(values) {
|
|
302
|
+
var _a, _b;
|
|
303
|
+
if (!this.formControl)
|
|
304
|
+
return;
|
|
305
|
+
this.isRestoring = true;
|
|
306
|
+
const form = ((_a = this.formGroupDir) === null || _a === void 0 ? void 0 : _a.form) || ((_b = this.ngForm) === null || _b === void 0 ? void 0 : _b.form);
|
|
307
|
+
if (form) {
|
|
308
|
+
this.prepareFormArrays(form, values);
|
|
309
|
+
form.patchValue(values);
|
|
310
|
+
}
|
|
311
|
+
setTimeout(() => this.isRestoring = false, 100);
|
|
312
|
+
}
|
|
313
|
+
prepareFormArrays(control, value) {
|
|
314
|
+
if (!control || value == null)
|
|
315
|
+
return;
|
|
316
|
+
if (control instanceof FormGroup && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
317
|
+
Object.keys(value).forEach(key => {
|
|
318
|
+
const childControl = control.get(key);
|
|
319
|
+
if (childControl)
|
|
320
|
+
this.prepareFormArrays(childControl, value[key]);
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (control instanceof FormArray && Array.isArray(value)) {
|
|
325
|
+
const formArray = control;
|
|
326
|
+
while (formArray.length < value.length) {
|
|
327
|
+
const template = formArray.at(0);
|
|
328
|
+
if (template instanceof FormGroup) {
|
|
329
|
+
const newGroup = new FormGroup({});
|
|
330
|
+
Object.keys(template.controls).forEach(ctrlName => {
|
|
331
|
+
const existing = template.get(ctrlName);
|
|
332
|
+
if (existing instanceof FormArray) {
|
|
333
|
+
newGroup.addControl(ctrlName, new FormArray([]));
|
|
334
|
+
}
|
|
335
|
+
else if (existing instanceof FormGroup) {
|
|
336
|
+
newGroup.addControl(ctrlName, new FormGroup({}));
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
newGroup.addControl(ctrlName, new FormControl(null));
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
formArray.push(newGroup);
|
|
343
|
+
}
|
|
344
|
+
else if (template) {
|
|
345
|
+
formArray.push(new FormControl(null));
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
const firstValue = value[0];
|
|
349
|
+
if (firstValue && typeof firstValue === 'object' && !Array.isArray(firstValue)) {
|
|
350
|
+
const group = new FormGroup({});
|
|
351
|
+
Object.keys(firstValue).forEach(key => group.addControl(key, new FormControl(null)));
|
|
352
|
+
formArray.push(group);
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
formArray.push(new FormControl(null));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
while (formArray.length > value.length) {
|
|
360
|
+
formArray.removeAt(formArray.length - 1);
|
|
361
|
+
}
|
|
362
|
+
value.forEach((childValue, index) => {
|
|
363
|
+
this.prepareFormArrays(formArray.at(index), childValue);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
discardDraft() {
|
|
368
|
+
this.draftService.clear(this.formId);
|
|
369
|
+
this.performResetAndDestroyBanner();
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Resets the form to initial values and destroys the draft banner.
|
|
373
|
+
* Used by the service when clearAndReset(formId) is called (e.g. on submit).
|
|
374
|
+
*/
|
|
375
|
+
performResetAndDestroyBanner() {
|
|
376
|
+
var _a, _b;
|
|
377
|
+
if (this.formControl) {
|
|
378
|
+
this.isRestoring = true;
|
|
379
|
+
const form = ((_a = this.formGroupDir) === null || _a === void 0 ? void 0 : _a.form) || ((_b = this.ngForm) === null || _b === void 0 ? void 0 : _b.form);
|
|
380
|
+
if (form) {
|
|
381
|
+
this.prepareFormArrays(form, this.initialValues);
|
|
382
|
+
form.reset(this.initialValues);
|
|
383
|
+
}
|
|
384
|
+
setTimeout(() => {
|
|
385
|
+
this.isRestoring = false;
|
|
386
|
+
}, this.draftDebounce + 200);
|
|
387
|
+
}
|
|
388
|
+
this.destroyBanner();
|
|
389
|
+
}
|
|
390
|
+
showBanner(savedAt, isRestored = false) {
|
|
391
|
+
this.bannerRef = this.viewContainerRef.createComponent(FormDraftBannerComponent);
|
|
392
|
+
this.bannerRef.setInput('visible', true);
|
|
393
|
+
this.bannerRef.setInput('isRestored', isRestored);
|
|
394
|
+
this.bannerRef.setInput('timeLabel', isRestored ? this.draftService.formatTimestamp(savedAt) : '');
|
|
395
|
+
this.bannerRef.setInput('restoredText', this.draftRestoredText);
|
|
396
|
+
this.bannerRef.setInput('savedText', this.draftSavedText);
|
|
397
|
+
this.bannerRef.setInput('savedLabel', this.draftSavedLabel);
|
|
398
|
+
this.bannerRef.setInput('discardText', this.draftDiscardText);
|
|
399
|
+
this.bannerRef.instance.discard.subscribe(() => this.discardDraft());
|
|
400
|
+
const bannerEl = this.bannerRef.location.nativeElement;
|
|
401
|
+
const formEl = this.elRef.nativeElement;
|
|
402
|
+
this.renderer.insertBefore(formEl, bannerEl, formEl.firstChild);
|
|
403
|
+
this.cdRef.detectChanges();
|
|
404
|
+
}
|
|
405
|
+
destroyBanner() {
|
|
406
|
+
if (this.bannerRef) {
|
|
407
|
+
this.bannerRef.destroy();
|
|
408
|
+
this.bannerRef = null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
clearDraft() {
|
|
412
|
+
this.draftService.clear(this.formId);
|
|
413
|
+
this.destroyBanner();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
FormDraftDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftDirective, deps: [{ token: i1$1.FormGroupDirective, optional: true }, { token: i1$1.NgForm, optional: true }, { token: FormDraftService }, { token: i0.ViewContainerRef }, { token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
|
|
417
|
+
FormDraftDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.3.0", type: FormDraftDirective, selector: "[ngxFormDraft]", inputs: { formId: ["ngxFormDraft", "formId"], draftDebounce: "draftDebounce", draftExcludeFields: "draftExcludeFields", draftShowOnChange: "draftShowOnChange", draftRestoredText: "draftRestoredText", draftSavedText: "draftSavedText", draftSavedLabel: "draftSavedLabel", draftDiscardText: "draftDiscardText" }, ngImport: i0 });
|
|
418
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: FormDraftDirective, decorators: [{
|
|
419
|
+
type: Directive,
|
|
420
|
+
args: [{
|
|
421
|
+
selector: '[ngxFormDraft]',
|
|
422
|
+
}]
|
|
423
|
+
}], ctorParameters: function () {
|
|
424
|
+
return [{ type: i1$1.FormGroupDirective, decorators: [{
|
|
425
|
+
type: Optional
|
|
426
|
+
}] }, { type: i1$1.NgForm, decorators: [{
|
|
427
|
+
type: Optional
|
|
428
|
+
}] }, { type: FormDraftService }, { type: i0.ViewContainerRef }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i0.Renderer2 }];
|
|
429
|
+
}, propDecorators: { formId: [{
|
|
430
|
+
type: Input,
|
|
431
|
+
args: ['ngxFormDraft']
|
|
432
|
+
}], draftDebounce: [{
|
|
433
|
+
type: Input
|
|
434
|
+
}], draftExcludeFields: [{
|
|
435
|
+
type: Input
|
|
436
|
+
}], draftShowOnChange: [{
|
|
437
|
+
type: Input
|
|
438
|
+
}], draftRestoredText: [{
|
|
439
|
+
type: Input
|
|
440
|
+
}], draftSavedText: [{
|
|
441
|
+
type: Input
|
|
442
|
+
}], draftSavedLabel: [{
|
|
443
|
+
type: Input
|
|
444
|
+
}], draftDiscardText: [{
|
|
445
|
+
type: Input
|
|
446
446
|
}] } });
|
|
447
447
|
|
|
448
|
-
class NgxFormDraftModule {
|
|
449
|
-
}
|
|
450
|
-
NgxFormDraftModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
451
|
-
NgxFormDraftModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, declarations: [FormDraftDirective, FormDraftBannerComponent], imports: [CommonModule], exports: [FormDraftDirective, FormDraftBannerComponent] });
|
|
452
|
-
NgxFormDraftModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, imports: [CommonModule] });
|
|
453
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, decorators: [{
|
|
454
|
-
type: NgModule,
|
|
455
|
-
args: [{
|
|
456
|
-
declarations: [FormDraftDirective, FormDraftBannerComponent],
|
|
457
|
-
imports: [CommonModule],
|
|
458
|
-
exports: [FormDraftDirective, FormDraftBannerComponent],
|
|
459
|
-
}]
|
|
448
|
+
class NgxFormDraftModule {
|
|
449
|
+
}
|
|
450
|
+
NgxFormDraftModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
451
|
+
NgxFormDraftModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, declarations: [FormDraftDirective, FormDraftBannerComponent], imports: [CommonModule], exports: [FormDraftDirective, FormDraftBannerComponent] });
|
|
452
|
+
NgxFormDraftModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, imports: [CommonModule] });
|
|
453
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: NgxFormDraftModule, decorators: [{
|
|
454
|
+
type: NgModule,
|
|
455
|
+
args: [{
|
|
456
|
+
declarations: [FormDraftDirective, FormDraftBannerComponent],
|
|
457
|
+
imports: [CommonModule],
|
|
458
|
+
exports: [FormDraftDirective, FormDraftBannerComponent],
|
|
459
|
+
}]
|
|
460
460
|
}] });
|
|
461
461
|
|
|
462
|
-
/**
|
|
463
|
-
* Generated bundle index. Do not edit.
|
|
462
|
+
/**
|
|
463
|
+
* Generated bundle index. Do not edit.
|
|
464
464
|
*/
|
|
465
465
|
|
|
466
466
|
export { FormDraftBannerComponent, FormDraftDirective, FormDraftService, NgxFormDraftModule };
|