@cognior/iap-sdk 0.1.0
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.
Potentially problematic release.
This version of @cognior/iap-sdk might be problematic. Click here for more details.
- package/.github/copilot-instructions.md +95 -0
- package/README.md +79 -0
- package/TRACKING.md +105 -0
- package/USER_CONTEXT_README.md +284 -0
- package/package.json +154 -0
- package/src/config.ts +25 -0
- package/src/core/flowEngine.ts +1833 -0
- package/src/core/triggerManager.ts +1011 -0
- package/src/experiences/banner.ts +366 -0
- package/src/experiences/beacon.ts +668 -0
- package/src/experiences/hotspotTour.ts +654 -0
- package/src/experiences/hotspots.ts +566 -0
- package/src/experiences/modal.ts +1337 -0
- package/src/experiences/modalSequence.ts +1247 -0
- package/src/experiences/popover.ts +652 -0
- package/src/experiences/registry.ts +21 -0
- package/src/experiences/survey.ts +1639 -0
- package/src/experiences/taskList.ts +625 -0
- package/src/experiences/tooltip.ts +740 -0
- package/src/experiences/types.ts +395 -0
- package/src/experiences/walkthrough.ts +670 -0
- package/src/flow-sequence.ts +177 -0
- package/src/flows.ts +512 -0
- package/src/http.ts +61 -0
- package/src/index.ts +355 -0
- package/src/services/flowManager.ts +905 -0
- package/src/services/flowNormalizer.ts +74 -0
- package/src/services/locationContextService.ts +189 -0
- package/src/services/pageContextService.ts +221 -0
- package/src/services/userContextService.ts +286 -0
- package/src/state/appState.ts +0 -0
- package/src/state/hooks.ts +0 -0
- package/src/state/index.ts +0 -0
- package/src/state/migration.ts +0 -0
- package/src/state/store.ts +0 -0
- package/src/styles/banner.css.ts +0 -0
- package/src/styles/hotspot.css.ts +0 -0
- package/src/styles/hotspotTour.css.ts +0 -0
- package/src/styles/modal.css.ts +564 -0
- package/src/styles/survey.css.ts +1013 -0
- package/src/styles/taskList.css.ts +0 -0
- package/src/styles/tooltip.css.ts +149 -0
- package/src/styles/walkthrough.css.ts +0 -0
- package/src/tourUtils.ts +0 -0
- package/src/tracking.ts +223 -0
- package/src/utils/debounce.ts +66 -0
- package/src/utils/eventSequenceValidator.ts +124 -0
- package/src/utils/flowTrackingSystem.ts +524 -0
- package/src/utils/idGenerator.ts +155 -0
- package/src/utils/immediateValidationPrevention.ts +184 -0
- package/src/utils/normalize.ts +50 -0
- package/src/utils/privacyManager.ts +166 -0
- package/src/utils/ruleEvaluator.ts +199 -0
- package/src/utils/sanitize.ts +79 -0
- package/src/utils/selectors.ts +107 -0
- package/src/utils/stepExecutor.ts +345 -0
- package/src/utils/triggerNormalizer.ts +149 -0
- package/src/utils/validationInterceptor.ts +650 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +13 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Interceptor
|
|
3
|
+
* Disables browser-native validation tooltips and replaces them with DAP tooltips
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class ValidationInterceptor {
|
|
7
|
+
private static instance: ValidationInterceptor | null = null;
|
|
8
|
+
private observerRef: MutationObserver | null = null;
|
|
9
|
+
private isInitialized: boolean = false;
|
|
10
|
+
|
|
11
|
+
static getInstance(): ValidationInterceptor {
|
|
12
|
+
if (!ValidationInterceptor.instance) {
|
|
13
|
+
ValidationInterceptor.instance = new ValidationInterceptor();
|
|
14
|
+
}
|
|
15
|
+
return ValidationInterceptor.instance;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Inject CSS styles for validation tooltips and error states
|
|
20
|
+
*/
|
|
21
|
+
private injectValidationStyles(): void {
|
|
22
|
+
const existingStyles = document.querySelector('#dap-validation-styles');
|
|
23
|
+
if (existingStyles) return;
|
|
24
|
+
|
|
25
|
+
const style = document.createElement('style');
|
|
26
|
+
style.id = 'dap-validation-styles';
|
|
27
|
+
style.textContent = `
|
|
28
|
+
/* Validation error state for inputs */
|
|
29
|
+
.dap-validation-error {
|
|
30
|
+
border-color: #ef4444 !important;
|
|
31
|
+
box-shadow: 0 0 0 1px #ef4444 !important;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Fallback validation tooltip */
|
|
35
|
+
.dap-validation-tooltip-fallback {
|
|
36
|
+
position: absolute !important;
|
|
37
|
+
background: #ef4444 !important;
|
|
38
|
+
color: white !important;
|
|
39
|
+
padding: 8px 12px !important;
|
|
40
|
+
border-radius: 4px !important;
|
|
41
|
+
font-size: 14px !important;
|
|
42
|
+
font-family: system-ui, -apple-system, sans-serif !important;
|
|
43
|
+
z-index: 10000 !important;
|
|
44
|
+
max-width: 200px !important;
|
|
45
|
+
word-wrap: break-word !important;
|
|
46
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2) !important;
|
|
47
|
+
pointer-events: none !important;
|
|
48
|
+
transform: translateY(-2px) !important;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.dap-validation-tooltip-fallback::before {
|
|
52
|
+
content: '';
|
|
53
|
+
position: absolute;
|
|
54
|
+
top: 100%;
|
|
55
|
+
left: 12px;
|
|
56
|
+
width: 0;
|
|
57
|
+
height: 0;
|
|
58
|
+
border-left: 6px solid transparent;
|
|
59
|
+
border-right: 6px solid transparent;
|
|
60
|
+
border-top: 6px solid #ef4444;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Hide native validation bubbles */
|
|
64
|
+
input:invalid {
|
|
65
|
+
box-shadow: none !important;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Ensure tooltips appear above everything */
|
|
69
|
+
.dap-tip-layer {
|
|
70
|
+
z-index: 10001 !important;
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
document.head.appendChild(style);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Initialize validation interception for the entire page
|
|
79
|
+
*/
|
|
80
|
+
public initialize(): void {
|
|
81
|
+
if (this.isInitialized) {
|
|
82
|
+
console.debug("[DAP] Validation interceptor already initialized");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.debug("[DAP] Initializing validation interceptor for DAP tooltips");
|
|
87
|
+
|
|
88
|
+
// Add validation tooltip styles immediately
|
|
89
|
+
this.injectValidationStyles();
|
|
90
|
+
|
|
91
|
+
// Focus on DAP tooltip replacement rather than prevention (handled by immediate prevention)
|
|
92
|
+
this.setupDAPTooltipTriggers();
|
|
93
|
+
|
|
94
|
+
this.isInitialized = true;
|
|
95
|
+
console.debug("[DAP] Validation interceptor initialized successfully");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Set up DAP tooltip triggers for validation errors
|
|
100
|
+
*/
|
|
101
|
+
private setupDAPTooltipTriggers(): void {
|
|
102
|
+
// Listen for validation events to trigger DAP tooltips
|
|
103
|
+
document.addEventListener('submit', async (event) => {
|
|
104
|
+
const form = event.target as HTMLFormElement;
|
|
105
|
+
if (form.tagName === 'FORM') {
|
|
106
|
+
// Check for validation manually and show DAP tooltips
|
|
107
|
+
const firstInvalidInput = this.validateForm(form);
|
|
108
|
+
if (firstInvalidInput) {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
firstInvalidInput.focus();
|
|
111
|
+
await this.triggerDAPValidation(firstInvalidInput);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}, { passive: false });
|
|
115
|
+
|
|
116
|
+
// Also listen for input blur events for real-time validation
|
|
117
|
+
document.addEventListener('blur', async (event) => {
|
|
118
|
+
const input = event.target as HTMLInputElement;
|
|
119
|
+
if (input && (input.tagName === 'INPUT' || input.tagName === 'TEXTAREA') && input.hasAttribute('required')) {
|
|
120
|
+
if (!this.isInputValid(input)) {
|
|
121
|
+
await this.triggerDAPValidation(input);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}, { capture: true });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Disable native validation on all forms in the document
|
|
129
|
+
*/
|
|
130
|
+
private disableNativeValidationOnForms(): void {
|
|
131
|
+
const forms = document.querySelectorAll('form');
|
|
132
|
+
forms.forEach(form => {
|
|
133
|
+
this.disableFormValidation(form);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (forms.length > 0) {
|
|
137
|
+
console.debug("[DAP] Browser validation suppressed on", forms.length, "forms");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Also run after DOM is fully loaded in case forms are added dynamically
|
|
141
|
+
if (document.readyState === 'loading') {
|
|
142
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
const newForms = document.querySelectorAll('form:not([novalidate])');
|
|
145
|
+
newForms.forEach(form => {
|
|
146
|
+
this.disableFormValidation(form as HTMLFormElement);
|
|
147
|
+
});
|
|
148
|
+
if (newForms.length > 0) {
|
|
149
|
+
console.debug("[DAP] Additional forms processed after DOM load:", newForms.length);
|
|
150
|
+
}
|
|
151
|
+
}, 100);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Disable native validation on a specific form
|
|
158
|
+
*/
|
|
159
|
+
private disableFormValidation(form: HTMLFormElement): void {
|
|
160
|
+
// Add novalidate attribute to prevent browser validation UI
|
|
161
|
+
form.setAttribute('novalidate', '');
|
|
162
|
+
|
|
163
|
+
// Also set the noValidate property for extra security
|
|
164
|
+
form.noValidate = true;
|
|
165
|
+
|
|
166
|
+
// Add event listeners to handle validation manually
|
|
167
|
+
form.addEventListener('submit', this.handleFormSubmit.bind(this), { capture: true, passive: false });
|
|
168
|
+
|
|
169
|
+
// Add listeners to form inputs for real-time validation
|
|
170
|
+
const inputs = form.querySelectorAll('input, select, textarea');
|
|
171
|
+
inputs.forEach(input => {
|
|
172
|
+
this.setupInputValidation(input as HTMLElement);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
console.debug("[DAP] Form validation disabled for:", form);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Set up validation listeners on individual inputs
|
|
180
|
+
*/
|
|
181
|
+
private setupInputValidation(input: HTMLElement): void {
|
|
182
|
+
// Prevent browser validation tooltips aggressively
|
|
183
|
+
input.addEventListener('invalid', this.preventBrowserTooltip.bind(this), { capture: true, passive: false });
|
|
184
|
+
|
|
185
|
+
// Clear any existing validation state
|
|
186
|
+
if ('setCustomValidity' in input) {
|
|
187
|
+
(input as HTMLInputElement).setCustomValidity('');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Add validation on blur/change for better UX
|
|
191
|
+
input.addEventListener('blur', this.validateInput.bind(this));
|
|
192
|
+
input.addEventListener('input', this.clearValidationErrors.bind(this));
|
|
193
|
+
|
|
194
|
+
// Also prevent validation on focus
|
|
195
|
+
input.addEventListener('focus', (event) => {
|
|
196
|
+
const inputElement = event.target as HTMLInputElement;
|
|
197
|
+
if ('setCustomValidity' in inputElement) {
|
|
198
|
+
inputElement.setCustomValidity('');
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
console.debug("[DAP] Input validation setup for:", input);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Prevent browser validation tooltips from appearing
|
|
207
|
+
*/
|
|
208
|
+
private preventBrowserTooltip(event: Event): void {
|
|
209
|
+
console.debug("[DAP] Browser validation suppressed for element:", event.target);
|
|
210
|
+
event.preventDefault();
|
|
211
|
+
event.stopPropagation();
|
|
212
|
+
event.stopImmediatePropagation();
|
|
213
|
+
|
|
214
|
+
// Clear any validation state on the element
|
|
215
|
+
const target = event.target as HTMLInputElement;
|
|
216
|
+
if (target && 'setCustomValidity' in target) {
|
|
217
|
+
target.setCustomValidity('');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Trigger DAP validation instead
|
|
221
|
+
this.triggerDAPValidation(event.target as HTMLElement);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Handle form submission with custom validation
|
|
226
|
+
*/
|
|
227
|
+
private handleFormSubmit(event: Event): void {
|
|
228
|
+
const form = event.target as HTMLFormElement;
|
|
229
|
+
const firstInvalidInput = this.validateForm(form);
|
|
230
|
+
|
|
231
|
+
if (firstInvalidInput) {
|
|
232
|
+
event.preventDefault();
|
|
233
|
+
event.stopPropagation();
|
|
234
|
+
|
|
235
|
+
// Focus the first invalid input and show DAP tooltip
|
|
236
|
+
firstInvalidInput.focus();
|
|
237
|
+
this.triggerDAPValidation(firstInvalidInput);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Validate a form and return first invalid input
|
|
243
|
+
*/
|
|
244
|
+
private validateForm(form: HTMLFormElement): HTMLElement | null {
|
|
245
|
+
const inputs = form.querySelectorAll('input[required], select[required], textarea[required]');
|
|
246
|
+
|
|
247
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
248
|
+
const element = inputs[i] as HTMLInputElement;
|
|
249
|
+
if (!this.isInputValid(element)) {
|
|
250
|
+
return element;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Validate individual input
|
|
259
|
+
*/
|
|
260
|
+
private validateInput(event: Event): void {
|
|
261
|
+
const input = event.target as HTMLInputElement;
|
|
262
|
+
|
|
263
|
+
if (!this.isInputValid(input)) {
|
|
264
|
+
this.triggerDAPValidation(input);
|
|
265
|
+
} else {
|
|
266
|
+
this.clearValidationErrors(event);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Check if an input is valid
|
|
272
|
+
*/
|
|
273
|
+
private isInputValid(input: HTMLInputElement): boolean {
|
|
274
|
+
// Check required fields
|
|
275
|
+
if (input.hasAttribute('required') && !input.value.trim()) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check email format
|
|
280
|
+
if (input.type === 'email' && input.value) {
|
|
281
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
282
|
+
return emailRegex.test(input.value);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Check URL format
|
|
286
|
+
if (input.type === 'url' && input.value) {
|
|
287
|
+
try {
|
|
288
|
+
new URL(input.value);
|
|
289
|
+
return true;
|
|
290
|
+
} catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Add more validation rules as needed
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Trigger DAP validation tooltip
|
|
301
|
+
*/
|
|
302
|
+
private async triggerDAPValidation(input: HTMLElement): Promise<void> {
|
|
303
|
+
// Prevent duplicate tooltips
|
|
304
|
+
this.clearAllTooltips();
|
|
305
|
+
|
|
306
|
+
console.debug("[DAP] DAP validation tooltip triggered for:", input);
|
|
307
|
+
|
|
308
|
+
const validationMessage = this.getValidationMessage(input);
|
|
309
|
+
|
|
310
|
+
// Create a validation tooltip experience
|
|
311
|
+
const validationExperience = {
|
|
312
|
+
elementSelector: input.id ? `#${input.id}` : this.generateSelector(input),
|
|
313
|
+
elementTrigger: 'validation_error',
|
|
314
|
+
elementLocation: window.location.pathname,
|
|
315
|
+
content: {
|
|
316
|
+
text: validationMessage,
|
|
317
|
+
placement: 'top'
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// Add visual error state to input
|
|
322
|
+
input.classList.add('dap-validation-error');
|
|
323
|
+
|
|
324
|
+
// Trigger DAP tooltip programmatically
|
|
325
|
+
await this.showDAPTooltip(input, validationExperience);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get appropriate validation message for an input
|
|
330
|
+
*/
|
|
331
|
+
private getValidationMessage(input: HTMLElement): string {
|
|
332
|
+
const inputElement = input as HTMLInputElement;
|
|
333
|
+
const fieldName = this.getFieldName(input);
|
|
334
|
+
|
|
335
|
+
if (inputElement.hasAttribute('required') && !inputElement.value.trim()) {
|
|
336
|
+
return `Please enter ${fieldName}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (inputElement.type === 'email' && inputElement.value) {
|
|
340
|
+
return 'Please enter a valid email address';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (inputElement.type === 'url' && inputElement.value) {
|
|
344
|
+
return 'Please enter a valid URL';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return `Please check the ${fieldName} field`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Get user-friendly field name
|
|
352
|
+
*/
|
|
353
|
+
private getFieldName(input: HTMLElement): string {
|
|
354
|
+
// Try label text
|
|
355
|
+
const label = document.querySelector(`label[for="${input.id}"]`);
|
|
356
|
+
if (label) {
|
|
357
|
+
return label.textContent?.trim().replace(':', '') || 'field';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Try placeholder
|
|
361
|
+
const placeholder = (input as HTMLInputElement).placeholder;
|
|
362
|
+
if (placeholder) {
|
|
363
|
+
return placeholder;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Try name attribute
|
|
367
|
+
const name = (input as HTMLInputElement).name;
|
|
368
|
+
if (name) {
|
|
369
|
+
return name.replace(/[_-]/g, ' ').toLowerCase();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return 'field';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Generate CSS selector for element
|
|
377
|
+
*/
|
|
378
|
+
private generateSelector(element: HTMLElement): string {
|
|
379
|
+
if (element.id) {
|
|
380
|
+
return `#${element.id}`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (element.className) {
|
|
384
|
+
return `.${element.className.split(' ')[0]}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return element.tagName.toLowerCase();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Show DAP tooltip for validation
|
|
392
|
+
*/
|
|
393
|
+
private async showDAPTooltip(element: HTMLElement, experience: any): Promise<void> {
|
|
394
|
+
try {
|
|
395
|
+
// Import tooltip renderer
|
|
396
|
+
const { renderDirectTooltip } = await import('../experiences/tooltip');
|
|
397
|
+
|
|
398
|
+
const tooltipPayload = {
|
|
399
|
+
targetSelector: experience.elementSelector,
|
|
400
|
+
text: experience.content.text,
|
|
401
|
+
placement: experience.content.placement || 'top',
|
|
402
|
+
trigger: 'click' as const // Use click trigger for validation tooltips
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
console.debug('[DAP] Showing DAP validation tooltip:', tooltipPayload);
|
|
406
|
+
await renderDirectTooltip(tooltipPayload);
|
|
407
|
+
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error('[DAP] Failed to load tooltip renderer:', error);
|
|
410
|
+
console.debug('[DAP] Using enhanced fallback tooltip');
|
|
411
|
+
// Enhanced fallback with DAP-like styling
|
|
412
|
+
this.showDAPStyledFallbackTooltip(element, experience.content.text);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Enhanced fallback tooltip with DAP styling
|
|
418
|
+
*/
|
|
419
|
+
private showDAPStyledFallbackTooltip(element: HTMLElement, message: string): void {
|
|
420
|
+
// Remove any existing tooltips
|
|
421
|
+
this.clearAllTooltips();
|
|
422
|
+
|
|
423
|
+
// Create tooltip wrapper similar to DAP tooltip structure
|
|
424
|
+
const tooltipWrapper = document.createElement('div');
|
|
425
|
+
tooltipWrapper.className = 'dap-validation-tooltip-wrap';
|
|
426
|
+
tooltipWrapper.style.cssText = `
|
|
427
|
+
position: absolute;
|
|
428
|
+
z-index: 10001;
|
|
429
|
+
pointer-events: none;
|
|
430
|
+
`;
|
|
431
|
+
|
|
432
|
+
// Create tooltip bubble with DAP styling
|
|
433
|
+
const tooltip = document.createElement('div');
|
|
434
|
+
tooltip.className = 'dap-validation-tooltip-bubble';
|
|
435
|
+
tooltip.style.cssText = `
|
|
436
|
+
background: #2563eb;
|
|
437
|
+
color: white;
|
|
438
|
+
padding: 12px 16px;
|
|
439
|
+
border-radius: 8px;
|
|
440
|
+
font-size: 14px;
|
|
441
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
442
|
+
max-width: 280px;
|
|
443
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
444
|
+
position: relative;
|
|
445
|
+
word-wrap: break-word;
|
|
446
|
+
line-height: 1.4;
|
|
447
|
+
`;
|
|
448
|
+
tooltip.textContent = message;
|
|
449
|
+
|
|
450
|
+
// Create arrow pointing to the element
|
|
451
|
+
const arrow = document.createElement('div');
|
|
452
|
+
arrow.className = 'dap-validation-tooltip-arrow';
|
|
453
|
+
arrow.style.cssText = `
|
|
454
|
+
position: absolute;
|
|
455
|
+
top: 100%;
|
|
456
|
+
left: 20px;
|
|
457
|
+
width: 0;
|
|
458
|
+
height: 0;
|
|
459
|
+
border-left: 8px solid transparent;
|
|
460
|
+
border-right: 8px solid transparent;
|
|
461
|
+
border-top: 8px solid #2563eb;
|
|
462
|
+
`;
|
|
463
|
+
|
|
464
|
+
tooltip.appendChild(arrow);
|
|
465
|
+
tooltipWrapper.appendChild(tooltip);
|
|
466
|
+
|
|
467
|
+
// Position relative to element
|
|
468
|
+
const rect = element.getBoundingClientRect();
|
|
469
|
+
const tooltipTop = Math.max(10, rect.top - 50 + window.scrollY);
|
|
470
|
+
const tooltipLeft = Math.max(10, Math.min(rect.left + window.scrollX, window.innerWidth - 300));
|
|
471
|
+
|
|
472
|
+
tooltipWrapper.style.top = `${tooltipTop}px`;
|
|
473
|
+
tooltipWrapper.style.left = `${tooltipLeft}px`;
|
|
474
|
+
|
|
475
|
+
console.debug('[DAP] DAP-styled fallback tooltip positioned at:', tooltipTop, tooltipLeft);
|
|
476
|
+
|
|
477
|
+
document.body.appendChild(tooltipWrapper);
|
|
478
|
+
|
|
479
|
+
// Auto-remove after 4 seconds
|
|
480
|
+
setTimeout(() => {
|
|
481
|
+
if (tooltipWrapper.parentNode) {
|
|
482
|
+
tooltipWrapper.parentNode.removeChild(tooltipWrapper);
|
|
483
|
+
}
|
|
484
|
+
}, 4000);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Clear all validation tooltips (both DAP and fallback)
|
|
489
|
+
*/
|
|
490
|
+
private clearAllTooltips(): void {
|
|
491
|
+
// Clear DAP tooltips
|
|
492
|
+
const dapTooltips = document.querySelectorAll('.dap-tip-layer, .dap-tooltip-wrap');
|
|
493
|
+
dapTooltips.forEach(tooltip => tooltip.remove());
|
|
494
|
+
|
|
495
|
+
// Clear fallback tooltips
|
|
496
|
+
const fallbackTooltips = document.querySelectorAll('.dap-validation-tooltip-fallback, .dap-validation-tooltip-wrap');
|
|
497
|
+
fallbackTooltips.forEach(tooltip => tooltip.remove());
|
|
498
|
+
|
|
499
|
+
console.debug('[DAP] All validation tooltips cleared');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Fallback tooltip if DAP tooltip fails (DEPRECATED - use DAP styled version)
|
|
504
|
+
*/
|
|
505
|
+
private showFallbackTooltip(element: HTMLElement, message: string): void {
|
|
506
|
+
// Remove any existing fallback tooltips
|
|
507
|
+
this.clearFallbackTooltips();
|
|
508
|
+
|
|
509
|
+
const tooltip = document.createElement('div');
|
|
510
|
+
tooltip.className = 'dap-validation-tooltip-fallback';
|
|
511
|
+
tooltip.textContent = message;
|
|
512
|
+
tooltip.style.cssText = `
|
|
513
|
+
position: absolute;
|
|
514
|
+
background: #ef4444;
|
|
515
|
+
color: white;
|
|
516
|
+
padding: 8px 12px;
|
|
517
|
+
border-radius: 4px;
|
|
518
|
+
font-size: 14px;
|
|
519
|
+
z-index: 10000;
|
|
520
|
+
max-width: 200px;
|
|
521
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
522
|
+
pointer-events: none;
|
|
523
|
+
`;
|
|
524
|
+
|
|
525
|
+
// Position relative to element with better bounds checking
|
|
526
|
+
const rect = element.getBoundingClientRect();
|
|
527
|
+
const tooltipTop = Math.max(10, rect.top - 35 + window.scrollY); // Ensure minimum 10px from top
|
|
528
|
+
const tooltipLeft = Math.max(10, Math.min(rect.left + window.scrollX, window.innerWidth - 220)); // Stay in viewport
|
|
529
|
+
|
|
530
|
+
tooltip.style.top = `${tooltipTop}px`;
|
|
531
|
+
tooltip.style.left = `${tooltipLeft}px`;
|
|
532
|
+
|
|
533
|
+
console.debug('[DAP] Fallback tooltip positioned at:', tooltipTop, tooltipLeft);
|
|
534
|
+
|
|
535
|
+
document.body.appendChild(tooltip);
|
|
536
|
+
|
|
537
|
+
// Auto-remove after 3 seconds (shorter duration)
|
|
538
|
+
setTimeout(() => {
|
|
539
|
+
if (tooltip.parentNode) {
|
|
540
|
+
tooltip.parentNode.removeChild(tooltip);
|
|
541
|
+
}
|
|
542
|
+
}, 3000);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Clear validation errors when user starts typing
|
|
547
|
+
*/
|
|
548
|
+
private clearValidationErrors(event: Event): void {
|
|
549
|
+
const input = event.target as HTMLElement;
|
|
550
|
+
|
|
551
|
+
// Remove custom validation classes
|
|
552
|
+
input.classList.remove('dap-validation-error');
|
|
553
|
+
|
|
554
|
+
// Clear all validation tooltips
|
|
555
|
+
this.clearAllTooltips();
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Clear fallback tooltips
|
|
560
|
+
*/
|
|
561
|
+
private clearFallbackTooltips(): void {
|
|
562
|
+
const tooltips = document.querySelectorAll('.dap-validation-tooltip-fallback');
|
|
563
|
+
tooltips.forEach(tooltip => tooltip.remove());
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Dismiss DAP tooltips
|
|
568
|
+
*/
|
|
569
|
+
private dismissDAPTooltips(): void {
|
|
570
|
+
console.debug("[DAP] DAP tooltip dismissed");
|
|
571
|
+
|
|
572
|
+
// Try to dismiss any active DAP tooltips
|
|
573
|
+
const activeTooltips = document.querySelectorAll('.dap-tooltip-wrap');
|
|
574
|
+
activeTooltips.forEach(tooltip => {
|
|
575
|
+
const closeButton = tooltip.querySelector('.dap-tooltip-close') as HTMLElement;
|
|
576
|
+
if (closeButton) {
|
|
577
|
+
closeButton.click();
|
|
578
|
+
} else {
|
|
579
|
+
// Force remove if no close button
|
|
580
|
+
tooltip.remove();
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Set up observer for dynamically added forms
|
|
587
|
+
*/
|
|
588
|
+
private setupFormObserver(): void {
|
|
589
|
+
this.observerRef = new MutationObserver((mutations) => {
|
|
590
|
+
mutations.forEach((mutation) => {
|
|
591
|
+
mutation.addedNodes.forEach((node) => {
|
|
592
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
593
|
+
const element = node as HTMLElement;
|
|
594
|
+
|
|
595
|
+
// Check if the added node is a form
|
|
596
|
+
if (element.tagName === 'FORM') {
|
|
597
|
+
this.disableFormValidation(element as HTMLFormElement);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Check for forms within the added node
|
|
601
|
+
const forms = element.querySelectorAll?.('form');
|
|
602
|
+
forms?.forEach(form => {
|
|
603
|
+
this.disableFormValidation(form);
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
this.observerRef.observe(document.body, {
|
|
611
|
+
childList: true,
|
|
612
|
+
subtree: true
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Set up additional validation listeners for enhanced coverage
|
|
618
|
+
*/
|
|
619
|
+
private setupValidationListeners(): void {
|
|
620
|
+
// Additional validation prevention as backup
|
|
621
|
+
window.addEventListener('invalid', this.preventBrowserTooltip.bind(this), { capture: true, passive: false });
|
|
622
|
+
|
|
623
|
+
// Prevent any validation tooltips on focus/blur
|
|
624
|
+
document.addEventListener('focusin', (event) => {
|
|
625
|
+
const input = event.target as HTMLInputElement;
|
|
626
|
+
if (input.tagName === 'INPUT' || input.tagName === 'TEXTAREA' || input.tagName === 'SELECT') {
|
|
627
|
+
// Remove any browser validation state
|
|
628
|
+
input.setCustomValidity('');
|
|
629
|
+
}
|
|
630
|
+
}, { capture: true });
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Cleanup and destroy the interceptor
|
|
635
|
+
*/
|
|
636
|
+
public destroy(): void {
|
|
637
|
+
if (this.observerRef) {
|
|
638
|
+
this.observerRef.disconnect();
|
|
639
|
+
this.observerRef = null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Remove event listeners
|
|
643
|
+
document.removeEventListener('invalid', this.preventBrowserTooltip.bind(this), true);
|
|
644
|
+
|
|
645
|
+
// Clear any active tooltips
|
|
646
|
+
this.clearFallbackTooltips();
|
|
647
|
+
|
|
648
|
+
ValidationInterceptor.instance = null;
|
|
649
|
+
}
|
|
650
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"lib": ["ES2020", "DOM"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"resolveJsonModule": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*"]
|
|
13
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ["src/index.ts"],
|
|
5
|
+
clean: true,
|
|
6
|
+
format: ["esm", "iife"], // ESM + browser global
|
|
7
|
+
globalName: "DAP", // window.DAP for IIFE
|
|
8
|
+
sourcemap: true,
|
|
9
|
+
minify: true,
|
|
10
|
+
outDir: "public/dist", // serve directly in /public
|
|
11
|
+
target: "es2020",
|
|
12
|
+
dts: { entry: "src/index.ts" } // types for consumers of ESM entry
|
|
13
|
+
});
|