@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,1639 @@
|
|
|
1
|
+
// src/experiences/survey.ts
|
|
2
|
+
// Unified Survey component implementation for collecting user feedback
|
|
3
|
+
// Supports both full-featured modal surveys and lightweight micro surveys
|
|
4
|
+
|
|
5
|
+
import { sanitizeHtml } from "../utils/sanitize";
|
|
6
|
+
import { register } from "./registry";
|
|
7
|
+
import { surveyCssText } from "../styles/survey.css";
|
|
8
|
+
import { http } from "../http";
|
|
9
|
+
import { resolveSelector } from "../utils/selectors";
|
|
10
|
+
import type { DapConfig } from "../config";
|
|
11
|
+
import type { CompletionTracker } from "./types";
|
|
12
|
+
|
|
13
|
+
// Inline modal CSS for surveys
|
|
14
|
+
const modalCssText = `
|
|
15
|
+
:root {
|
|
16
|
+
--dap-z: 2147483640;
|
|
17
|
+
--dap-overlay: rgba(15, 23, 42, 0.5);
|
|
18
|
+
--dap-modal-bg: #f8fafc;
|
|
19
|
+
--dap-modal-header-bg: #f1f5f9;
|
|
20
|
+
--dap-modal-border: #e2e8f0;
|
|
21
|
+
--dap-modal-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
22
|
+
--dap-text-primary: #1e293b;
|
|
23
|
+
--dap-text-secondary: #64748b;
|
|
24
|
+
--dap-text-muted: #94a3b8;
|
|
25
|
+
--dap-btn-primary: #3b82f6;
|
|
26
|
+
--dap-btn-primary-hover: #2563eb;
|
|
27
|
+
--dap-btn-secondary: #e2e8f0;
|
|
28
|
+
--dap-btn-secondary-hover: #cbd5e1;
|
|
29
|
+
--dap-radius: 12px;
|
|
30
|
+
--dap-spacing: 16px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.dap-modal-wrap {
|
|
34
|
+
position: fixed;
|
|
35
|
+
inset: 0;
|
|
36
|
+
background: var(--dap-overlay);
|
|
37
|
+
z-index: var(--dap-z);
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
padding: 24px;
|
|
42
|
+
backdrop-filter: blur(4px);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.dap-modal {
|
|
46
|
+
background: var(--dap-modal-bg);
|
|
47
|
+
border: 1px solid var(--dap-modal-border);
|
|
48
|
+
border-radius: var(--dap-radius);
|
|
49
|
+
box-shadow: var(--dap-modal-shadow);
|
|
50
|
+
width: 100%;
|
|
51
|
+
max-width: min(90vw, 500px);
|
|
52
|
+
max-height: min(90vh, 600px);
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-direction: column;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Content-adaptive size classes */
|
|
59
|
+
.dap-modal.dap-size-small {
|
|
60
|
+
max-width: min(90vw, 400px);
|
|
61
|
+
max-height: min(90vh, 500px);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.dap-modal.dap-size-medium {
|
|
65
|
+
max-width: min(90vw, 600px);
|
|
66
|
+
max-height: min(90vh, 650px);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.dap-modal.dap-size-large {
|
|
70
|
+
max-width: min(90vw, 900px);
|
|
71
|
+
max-height: min(90vh, 800px);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Scrollable class when content overflows */
|
|
75
|
+
.dap-modal.dap-scrollable .dap-modal-body {
|
|
76
|
+
overflow-y: auto;
|
|
77
|
+
overflow-x: auto;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Default: no scroll unless needed */
|
|
81
|
+
.dap-modal .dap-modal-body {
|
|
82
|
+
overflow: visible;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.dap-header-bar {
|
|
86
|
+
background: var(--dap-modal-header-bg);
|
|
87
|
+
border-bottom: 1px solid var(--dap-modal-border);
|
|
88
|
+
padding: var(--dap-spacing);
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
justify-content: space-between;
|
|
92
|
+
min-height: 60px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.dap-modal-header {
|
|
96
|
+
color: var(--dap-text-primary);
|
|
97
|
+
font-size: 18px;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
margin: 0;
|
|
100
|
+
flex: 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.dap-close {
|
|
104
|
+
background: transparent;
|
|
105
|
+
border: none;
|
|
106
|
+
color: var(--dap-text-secondary);
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
padding: 8px;
|
|
109
|
+
border-radius: 6px;
|
|
110
|
+
width: 32px;
|
|
111
|
+
height: 32px;
|
|
112
|
+
font-size: 18px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.dap-close:hover {
|
|
116
|
+
background: var(--dap-btn-secondary);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.dap-modal-body {
|
|
120
|
+
flex: 1;
|
|
121
|
+
overflow-y: auto;
|
|
122
|
+
padding: 32px;
|
|
123
|
+
background: var(--dap-modal-bg);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.dap-footer {
|
|
127
|
+
background: var(--dap-modal-bg);
|
|
128
|
+
border-top: 1px solid var(--dap-modal-border);
|
|
129
|
+
padding: var(--dap-spacing);
|
|
130
|
+
display: flex;
|
|
131
|
+
gap: 12px;
|
|
132
|
+
justify-content: flex-end;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.dap-cta {
|
|
136
|
+
background: var(--dap-btn-primary);
|
|
137
|
+
color: white;
|
|
138
|
+
border: 1px solid var(--dap-btn-primary);
|
|
139
|
+
cursor: pointer;
|
|
140
|
+
padding: 12px 24px;
|
|
141
|
+
border-radius: 8px;
|
|
142
|
+
font-size: 14px;
|
|
143
|
+
font-weight: 500;
|
|
144
|
+
transition: all 0.15s ease;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.dap-cta:hover {
|
|
148
|
+
background: var(--dap-btn-primary-hover);
|
|
149
|
+
transform: translateY(-1px);
|
|
150
|
+
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.25);
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
export type SurveyQuestion = {
|
|
155
|
+
questionId: string;
|
|
156
|
+
question: string;
|
|
157
|
+
type: SurveyQuestionType;
|
|
158
|
+
options?: string[];
|
|
159
|
+
scaleMin?: number;
|
|
160
|
+
scaleMax?: number;
|
|
161
|
+
labelMin?: string;
|
|
162
|
+
labelMax?: string;
|
|
163
|
+
criteria?: string[];
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export type SurveyQuestionType =
|
|
167
|
+
| "SingleChoice"
|
|
168
|
+
| "MultipleChoice"
|
|
169
|
+
| "Dropdown"
|
|
170
|
+
| "TextSingle"
|
|
171
|
+
| "TextMulti"
|
|
172
|
+
| "OpinionScale"
|
|
173
|
+
| "OpinionScaleChoice"
|
|
174
|
+
| "NpsScale"
|
|
175
|
+
| "NpsOptions"
|
|
176
|
+
| "StarRating"
|
|
177
|
+
| "StarChoice";
|
|
178
|
+
|
|
179
|
+
export type SurveyPayload = {
|
|
180
|
+
// Standard survey fields
|
|
181
|
+
header?: string;
|
|
182
|
+
body?: string;
|
|
183
|
+
questions?: SurveyQuestion[]; // Made optional to support micro surveys
|
|
184
|
+
theme?: Record<string, string>;
|
|
185
|
+
flowId?: string;
|
|
186
|
+
organizationId?: string;
|
|
187
|
+
siteId?: string;
|
|
188
|
+
stepId?: string;
|
|
189
|
+
|
|
190
|
+
// Micro survey fields (for lightweight single-question surveys)
|
|
191
|
+
question?: string; // Single question for micro surveys
|
|
192
|
+
options?: Array<{ label: string; value: string }>; // Simple options for micro surveys
|
|
193
|
+
placeholder?: string;
|
|
194
|
+
submitText?: string;
|
|
195
|
+
cancelText?: string;
|
|
196
|
+
type?: 'rating' | 'choice' | 'text' | 'modal'; // 'modal' = traditional full survey
|
|
197
|
+
rating?: {
|
|
198
|
+
min?: number;
|
|
199
|
+
max?: number;
|
|
200
|
+
labels?: { min?: string; max?: string };
|
|
201
|
+
};
|
|
202
|
+
targetSelector?: string; // For micro survey positioning
|
|
203
|
+
position?: 'top' | 'bottom' | 'right' | 'left' | 'center';
|
|
204
|
+
mode?: 'modal' | 'inline'; // Explicit mode selection
|
|
205
|
+
_completionTracker?: CompletionTracker;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
type SurveyFlow = {
|
|
209
|
+
id: string;
|
|
210
|
+
type: "survey";
|
|
211
|
+
payload: SurveyPayload;
|
|
212
|
+
config?: DapConfig;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export function registerSurvey() {
|
|
216
|
+
register("survey", renderSurvey);
|
|
217
|
+
// Also register microsurvey as an alias for backward compatibility
|
|
218
|
+
register("microsurvey", renderSurvey);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function renderSurvey(flow: SurveyFlow): Promise<void> {
|
|
222
|
+
const { payload } = flow;
|
|
223
|
+
|
|
224
|
+
console.debug('[DAP] renderSurvey called with payload:', {
|
|
225
|
+
hasHeader: !!payload.header,
|
|
226
|
+
hasBody: !!payload.body,
|
|
227
|
+
hasQuestion: !!payload.question,
|
|
228
|
+
questionsCount: payload.questions?.length || 0,
|
|
229
|
+
targetSelector: payload.targetSelector,
|
|
230
|
+
mode: payload.mode,
|
|
231
|
+
type: payload.type
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Determine survey mode
|
|
235
|
+
const surveyMode = determineSurveyMode(payload);
|
|
236
|
+
|
|
237
|
+
console.debug('[DAP] Survey mode determined:', surveyMode);
|
|
238
|
+
|
|
239
|
+
if (surveyMode === 'inline') {
|
|
240
|
+
return renderMicroSurvey(flow);
|
|
241
|
+
} else {
|
|
242
|
+
return renderModalSurvey(flow);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function determineSurveyMode(payload: SurveyPayload): 'modal' | 'inline' {
|
|
247
|
+
console.debug('[DAP] Survey mode detection:', {
|
|
248
|
+
mode: payload.mode,
|
|
249
|
+
question: payload.question,
|
|
250
|
+
questionsArray: payload.questions?.length || 0,
|
|
251
|
+
targetSelector: payload.targetSelector,
|
|
252
|
+
type: payload.type
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Explicit mode setting takes precedence
|
|
256
|
+
if (payload.mode) {
|
|
257
|
+
console.debug('[DAP] Using explicit mode:', payload.mode);
|
|
258
|
+
return payload.mode;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// If we have multiple questions, always use modal (even with targetSelector)
|
|
262
|
+
if (payload.questions && payload.questions.length > 1) {
|
|
263
|
+
console.debug('[DAP] Multiple questions - using modal mode');
|
|
264
|
+
return 'modal';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// If there's a single question field and no questions array, it's a micro survey
|
|
268
|
+
if (payload.question && !payload.questions?.length) {
|
|
269
|
+
console.debug('[DAP] Single question micro survey - using inline mode');
|
|
270
|
+
return 'inline';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// If there's a targetSelector, it's probably meant to be inline
|
|
274
|
+
if (payload.targetSelector) {
|
|
275
|
+
console.debug('[DAP] Has targetSelector - using inline mode');
|
|
276
|
+
return 'inline';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// If type is one of the micro survey types, it's inline
|
|
280
|
+
if (payload.type && ['rating', 'choice', 'text'].includes(payload.type)) {
|
|
281
|
+
console.debug('[DAP] Simple survey type - using inline mode');
|
|
282
|
+
return 'inline';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Default to modal for traditional surveys
|
|
286
|
+
console.debug('[DAP] Defaulting to modal mode');
|
|
287
|
+
return 'modal';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function renderModalSurvey(flow: SurveyFlow): Promise<void> {
|
|
291
|
+
const { payload } = flow;
|
|
292
|
+
|
|
293
|
+
// Validate that we have questions for modal surveys
|
|
294
|
+
if (!payload.questions || payload.questions.length === 0) {
|
|
295
|
+
console.error("[DAP] Modal survey requires questions array");
|
|
296
|
+
payload._completionTracker?.onComplete?.();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
console.debug('[DAP] === SURVEY DEBUG: Rendering modal survey ===');
|
|
301
|
+
console.debug('[DAP] Survey payload:', payload);
|
|
302
|
+
console.debug('[DAP] Has completion tracker:', !!payload._completionTracker);
|
|
303
|
+
console.debug('[DAP] Has onComplete callback:', !!payload._completionTracker?.onComplete);
|
|
304
|
+
|
|
305
|
+
// TEMPORARY: Auto-advance after 3 seconds for rule testing
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
console.debug('[DAP] === SURVEY AUTO-ADVANCE: Simulating completion ===');
|
|
308
|
+
alert('🚀 Survey Auto-Advance!\n\nSimulating survey completion to test rule steps...');
|
|
309
|
+
payload._completionTracker?.onComplete?.();
|
|
310
|
+
}, 3000);
|
|
311
|
+
|
|
312
|
+
const prevActive = document.activeElement as HTMLElement | null;
|
|
313
|
+
|
|
314
|
+
const shell = createShell(payload.theme);
|
|
315
|
+
|
|
316
|
+
const onKey = (e: KeyboardEvent) => {
|
|
317
|
+
if (e.key === "Escape") closeAll();
|
|
318
|
+
else if (e.key === "Tab") trapTab(e, shell.dlg);
|
|
319
|
+
};
|
|
320
|
+
document.addEventListener("keydown", onKey, true);
|
|
321
|
+
|
|
322
|
+
// header
|
|
323
|
+
shell.titleEl.textContent = payload.header ?? "Survey";
|
|
324
|
+
|
|
325
|
+
// body
|
|
326
|
+
shell.body.replaceChildren();
|
|
327
|
+
if (payload.body) {
|
|
328
|
+
const bodyText = document.createElement("div");
|
|
329
|
+
bodyText.className = "dap-survey-intro";
|
|
330
|
+
bodyText.innerHTML = sanitizeHtml(payload.body);
|
|
331
|
+
shell.body.appendChild(bodyText);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Survey form
|
|
335
|
+
const form = document.createElement("form");
|
|
336
|
+
form.className = "dap-survey-form";
|
|
337
|
+
form.addEventListener("submit", async (e) => {
|
|
338
|
+
e.preventDefault();
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
// Get all form elements and extract values manually
|
|
342
|
+
const responses: Array<{
|
|
343
|
+
question: string;
|
|
344
|
+
type: SurveyQuestionType;
|
|
345
|
+
answer: any;
|
|
346
|
+
}> = [];
|
|
347
|
+
|
|
348
|
+
// Process each question based on its type (already validated above)
|
|
349
|
+
for (const q of payload.questions!) {
|
|
350
|
+
const questionData = {
|
|
351
|
+
question: q.question,
|
|
352
|
+
type: q.type,
|
|
353
|
+
answer: null as any
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
switch (q.type) {
|
|
357
|
+
case "SingleChoice":
|
|
358
|
+
case "Dropdown": {
|
|
359
|
+
const radio = form.querySelector(`input[name="${q.questionId}"]:checked`) as HTMLInputElement;
|
|
360
|
+
const select = form.querySelector(`select[name="${q.questionId}"]`) as HTMLSelectElement;
|
|
361
|
+
questionData.answer = radio?.value || select?.value || null;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
case "MultipleChoice": {
|
|
366
|
+
const checkboxes = Array.from(form.querySelectorAll(`input[name="${q.questionId}[]"]:checked`)) as HTMLInputElement[];
|
|
367
|
+
questionData.answer = checkboxes.map(cb => cb.value);
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
case "TextSingle": {
|
|
372
|
+
const input = form.querySelector(`input[name="${q.questionId}"]`) as HTMLInputElement;
|
|
373
|
+
questionData.answer = input?.value || "";
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
case "TextMulti": {
|
|
378
|
+
const textarea = form.querySelector(`textarea[name="${q.questionId}"]`) as HTMLTextAreaElement;
|
|
379
|
+
questionData.answer = textarea?.value || "";
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
case "OpinionScale":
|
|
384
|
+
case "StarRating":
|
|
385
|
+
case "NpsScale": {
|
|
386
|
+
const radio = form.querySelector(`input[name="${q.questionId}"]:checked`) as HTMLInputElement;
|
|
387
|
+
questionData.answer = radio?.value ? parseInt(radio.value) : null;
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
case "OpinionScaleChoice": {
|
|
392
|
+
const items = q.criteria || [];
|
|
393
|
+
|
|
394
|
+
// If no criteria provided, skip this question
|
|
395
|
+
if (items.length === 0) break;
|
|
396
|
+
|
|
397
|
+
const ratings: Record<string, number> = {};
|
|
398
|
+
|
|
399
|
+
items.forEach((item, idx) => {
|
|
400
|
+
const radio = form.querySelector(`input[name="${q.questionId}_${idx}"]:checked`) as HTMLInputElement;
|
|
401
|
+
if (radio?.value) {
|
|
402
|
+
ratings[item] = parseInt(radio.value);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
questionData.answer = Object.keys(ratings).length > 0 ? ratings : null;
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
case "StarChoice": {
|
|
411
|
+
// For the updated StarChoice, get the single selected rating value
|
|
412
|
+
const radio = form.querySelector(`input[name="${q.questionId}"]:checked`) as HTMLInputElement;
|
|
413
|
+
|
|
414
|
+
// Define default options from Poor to Excellent for mapping
|
|
415
|
+
const defaultLabels = ["Poor", "Fair", "Good", "Very Good", "Excellent"];
|
|
416
|
+
const max = q.scaleMax || 5;
|
|
417
|
+
|
|
418
|
+
// Use options if provided, otherwise use default labels (limited to max)
|
|
419
|
+
const starLabels = q.options && q.options.length > 0
|
|
420
|
+
? q.options
|
|
421
|
+
: defaultLabels.slice(0, max);
|
|
422
|
+
|
|
423
|
+
if (radio?.value) {
|
|
424
|
+
const ratingValue = parseInt(radio.value);
|
|
425
|
+
const labelIndex = Math.min(starLabels.length, ratingValue) - 1;
|
|
426
|
+
const label = starLabels[labelIndex];
|
|
427
|
+
|
|
428
|
+
questionData.answer = {
|
|
429
|
+
value: ratingValue,
|
|
430
|
+
label: label
|
|
431
|
+
};
|
|
432
|
+
} else {
|
|
433
|
+
questionData.answer = null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
case "NpsOptions": {
|
|
440
|
+
const category = form.querySelector(`input[name="${q.questionId}"]:checked`) as HTMLInputElement;
|
|
441
|
+
|
|
442
|
+
questionData.answer = category?.value || null;
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (questionData.answer !== null) {
|
|
448
|
+
responses.push(questionData);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Create the survey submission payload
|
|
453
|
+
const submissionData = {
|
|
454
|
+
stepId: payload.stepId,
|
|
455
|
+
sessionId: `user-session-${Date.now()}`,
|
|
456
|
+
submittedAt: new Date().toISOString(),
|
|
457
|
+
responses,
|
|
458
|
+
client: {
|
|
459
|
+
userId: "",
|
|
460
|
+
clientIP: "",
|
|
461
|
+
userAgent: navigator.userAgent,
|
|
462
|
+
locale: navigator.language
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
console.log("[DAP] Survey submission payload:", submissionData);
|
|
467
|
+
|
|
468
|
+
// Submit to API if configuration is available
|
|
469
|
+
if (flow.config && payload.flowId && payload.organizationId && payload.siteId) {
|
|
470
|
+
const url = flow.config?.apiurl + `/iap-experience/${payload.organizationId}/${payload.siteId}/survey-responses/${payload.flowId}`;
|
|
471
|
+
const hostBase = location.origin;
|
|
472
|
+
|
|
473
|
+
// Log the API request details
|
|
474
|
+
console.log("[DAP] Submitting survey to API:", url);
|
|
475
|
+
console.log("[DAP] Request will include X-Host-Url header:", hostBase);
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
await http(flow.config, url, {
|
|
479
|
+
method: "POST",
|
|
480
|
+
body: submissionData,
|
|
481
|
+
hostBase,
|
|
482
|
+
includeHostHeader: true
|
|
483
|
+
});
|
|
484
|
+
console.log("[DAP] Survey successfully submitted to API");
|
|
485
|
+
|
|
486
|
+
// Track the survey submission
|
|
487
|
+
try {
|
|
488
|
+
// Legacy: Survey submission tracking - now handled by step view system
|
|
489
|
+
console.debug('[DAP Survey] Survey submission - tracking handled by step view system');
|
|
490
|
+
} catch (trackingError) {
|
|
491
|
+
console.error("[DAP] Survey submission tracking error:", trackingError);
|
|
492
|
+
// Don't re-throw tracking errors - we still want the submission to be considered successful
|
|
493
|
+
}
|
|
494
|
+
} catch (error) {
|
|
495
|
+
console.error("[DAP] Survey submission API error:", error);
|
|
496
|
+
throw error; // Re-throw to be caught by the outer try/catch
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
console.warn("[DAP] Survey API submission skipped - missing configuration");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Close the survey
|
|
503
|
+
closeAll();
|
|
504
|
+
} catch (err) {
|
|
505
|
+
console.error("[DAP] Survey submission error:", err);
|
|
506
|
+
|
|
507
|
+
// Show error message
|
|
508
|
+
const errorMsg = document.createElement("div");
|
|
509
|
+
errorMsg.className = "dap-survey-error";
|
|
510
|
+
errorMsg.textContent = "An error occurred while submitting your responses. Please try again.";
|
|
511
|
+
form.prepend(errorMsg);
|
|
512
|
+
|
|
513
|
+
// Remove error message after 5 seconds
|
|
514
|
+
setTimeout(() => {
|
|
515
|
+
errorMsg.remove();
|
|
516
|
+
}, 5000);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Add questions (already validated above)
|
|
521
|
+
payload.questions!.forEach((q, index) => {
|
|
522
|
+
const questionEl = renderQuestion(q, index);
|
|
523
|
+
form.appendChild(questionEl);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
shell.body.appendChild(form);
|
|
527
|
+
|
|
528
|
+
// Apply adaptive modal sizing based on content
|
|
529
|
+
setTimeout(() => {
|
|
530
|
+
adjustSurveyModalSize(shell.dlg, shell.body);
|
|
531
|
+
}, 0);
|
|
532
|
+
|
|
533
|
+
// footer buttons
|
|
534
|
+
shell.prevBtn.textContent = "Cancel";
|
|
535
|
+
shell.nextBtn.textContent = "Submit";
|
|
536
|
+
shell.prevBtn.style.display = "inline-block";
|
|
537
|
+
|
|
538
|
+
const closeAll = () => {
|
|
539
|
+
document.removeEventListener("keydown", onKey, true);
|
|
540
|
+
shell.wrap.remove();
|
|
541
|
+
if (prevActive?.focus) prevActive.focus();
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
shell.wrap.addEventListener("click", (e) => { if (e.target === shell.wrap) closeAll(); });
|
|
545
|
+
shell.closeBtn.addEventListener("click", closeAll);
|
|
546
|
+
shell.prevBtn.addEventListener("click", closeAll);
|
|
547
|
+
shell.nextBtn.addEventListener("click", () => {
|
|
548
|
+
form.requestSubmit();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
setTimeout(() => shell.dlg.focus(), 0);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/* ===================== Micro Survey Implementation ===================== */
|
|
555
|
+
|
|
556
|
+
interface MicroSurveyState {
|
|
557
|
+
id: string;
|
|
558
|
+
element: HTMLElement;
|
|
559
|
+
targetElement?: HTMLElement;
|
|
560
|
+
cleanup: (() => void)[];
|
|
561
|
+
isActive: boolean;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const activeMicroSurveys = new Map<string, MicroSurveyState>();
|
|
565
|
+
|
|
566
|
+
async function renderMicroSurvey(flow: SurveyFlow): Promise<void> {
|
|
567
|
+
const { payload, id } = flow;
|
|
568
|
+
|
|
569
|
+
console.debug("[DAP] MicroSurvey initialized", { id, payload });
|
|
570
|
+
|
|
571
|
+
if (!payload.question) {
|
|
572
|
+
console.error("[DAP] MicroSurvey missing required question");
|
|
573
|
+
payload._completionTracker?.onComplete?.();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Clean up any existing micro survey with same ID
|
|
578
|
+
if (activeMicroSurveys.has(id)) {
|
|
579
|
+
cleanupMicroSurvey(id);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Find target element if selector provided
|
|
583
|
+
let targetElement: HTMLElement | undefined;
|
|
584
|
+
if (payload.targetSelector) {
|
|
585
|
+
const element = resolveSelector(payload.targetSelector);
|
|
586
|
+
if (element instanceof HTMLElement) {
|
|
587
|
+
targetElement = element;
|
|
588
|
+
} else {
|
|
589
|
+
console.warn(`[DAP] MicroSurvey: Target element not found for selector: ${payload.targetSelector}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Create micro survey element
|
|
594
|
+
const microSurveyElement = createMicroSurveyElement(payload, id, flow);
|
|
595
|
+
|
|
596
|
+
const microSurveyState: MicroSurveyState = {
|
|
597
|
+
id,
|
|
598
|
+
element: microSurveyElement,
|
|
599
|
+
targetElement,
|
|
600
|
+
cleanup: [],
|
|
601
|
+
isActive: false
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
activeMicroSurveys.set(id, microSurveyState);
|
|
605
|
+
|
|
606
|
+
// Show micro survey
|
|
607
|
+
showMicroSurvey(microSurveyState, payload);
|
|
608
|
+
|
|
609
|
+
console.debug("[DAP] MicroSurvey setup complete", { id });
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function createMicroSurveyElement(payload: SurveyPayload, id: string, flow: SurveyFlow): HTMLElement {
|
|
613
|
+
const microSurvey = document.createElement('div');
|
|
614
|
+
microSurvey.className = 'dap-microsurvey';
|
|
615
|
+
microSurvey.id = `dap-microsurvey-${id}`;
|
|
616
|
+
microSurvey.setAttribute('role', 'dialog');
|
|
617
|
+
microSurvey.setAttribute('aria-label', 'Quick Survey');
|
|
618
|
+
|
|
619
|
+
// Base styling
|
|
620
|
+
Object.assign(microSurvey.style, {
|
|
621
|
+
position: 'fixed',
|
|
622
|
+
zIndex: '10000',
|
|
623
|
+
backgroundColor: '#ffffff',
|
|
624
|
+
border: '1px solid #e2e8f0',
|
|
625
|
+
borderRadius: '12px',
|
|
626
|
+
boxShadow: '0 10px 25px rgba(0, 0, 0, 0.1), 0 4px 12px rgba(0, 0, 0, 0.05)',
|
|
627
|
+
padding: '20px',
|
|
628
|
+
maxWidth: '320px',
|
|
629
|
+
minWidth: '280px',
|
|
630
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
631
|
+
fontSize: '14px',
|
|
632
|
+
lineHeight: '1.5',
|
|
633
|
+
color: '#1e293b',
|
|
634
|
+
opacity: '0',
|
|
635
|
+
transform: 'scale(0.95) translateY(10px)',
|
|
636
|
+
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
637
|
+
backdropFilter: 'blur(8px)'
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// Add question
|
|
641
|
+
const questionEl = document.createElement('div');
|
|
642
|
+
questionEl.style.cssText = `
|
|
643
|
+
font-weight: 600;
|
|
644
|
+
margin-bottom: 16px;
|
|
645
|
+
color: #1e293b;
|
|
646
|
+
line-height: 1.4;
|
|
647
|
+
`;
|
|
648
|
+
questionEl.innerHTML = sanitizeHtml(payload.question || '');
|
|
649
|
+
microSurvey.appendChild(questionEl);
|
|
650
|
+
|
|
651
|
+
// Add content based on type
|
|
652
|
+
const contentEl = document.createElement('div');
|
|
653
|
+
contentEl.style.marginBottom = '16px';
|
|
654
|
+
|
|
655
|
+
const surveyType = payload.type || 'choice';
|
|
656
|
+
|
|
657
|
+
if (surveyType === 'rating') {
|
|
658
|
+
createRatingContent(contentEl, payload, id);
|
|
659
|
+
} else if (surveyType === 'choice') {
|
|
660
|
+
createChoiceContent(contentEl, payload, id);
|
|
661
|
+
} else if (surveyType === 'text') {
|
|
662
|
+
createTextContent(contentEl, payload, id);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
microSurvey.appendChild(contentEl);
|
|
666
|
+
|
|
667
|
+
// Add buttons
|
|
668
|
+
const buttonsEl = document.createElement('div');
|
|
669
|
+
buttonsEl.style.cssText = `
|
|
670
|
+
display: flex;
|
|
671
|
+
gap: 12px;
|
|
672
|
+
justify-content: flex-end;
|
|
673
|
+
`;
|
|
674
|
+
|
|
675
|
+
// Cancel button
|
|
676
|
+
const cancelBtn = document.createElement('button');
|
|
677
|
+
cancelBtn.textContent = payload.cancelText || 'Cancel';
|
|
678
|
+
cancelBtn.style.cssText = `
|
|
679
|
+
padding: 8px 16px;
|
|
680
|
+
border: 1px solid #d1d5db;
|
|
681
|
+
border-radius: 6px;
|
|
682
|
+
background: #ffffff;
|
|
683
|
+
color: #374151;
|
|
684
|
+
cursor: pointer;
|
|
685
|
+
font-size: 14px;
|
|
686
|
+
`;
|
|
687
|
+
cancelBtn.addEventListener('click', () => {
|
|
688
|
+
cleanupMicroSurvey(id);
|
|
689
|
+
payload._completionTracker?.onComplete?.();
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// Submit button
|
|
693
|
+
const submitBtn = document.createElement('button');
|
|
694
|
+
submitBtn.textContent = payload.submitText || 'Submit';
|
|
695
|
+
submitBtn.style.cssText = `
|
|
696
|
+
padding: 8px 16px;
|
|
697
|
+
border: none;
|
|
698
|
+
border-radius: 6px;
|
|
699
|
+
background: #3b82f6;
|
|
700
|
+
color: #ffffff;
|
|
701
|
+
cursor: pointer;
|
|
702
|
+
font-size: 14px;
|
|
703
|
+
font-weight: 500;
|
|
704
|
+
`;
|
|
705
|
+
|
|
706
|
+
submitBtn.addEventListener('click', async () => {
|
|
707
|
+
const formData = extractMicroSurveyData(microSurvey, payload);
|
|
708
|
+
if (formData) {
|
|
709
|
+
try {
|
|
710
|
+
await submitMicroSurveyData(formData, payload, flow);
|
|
711
|
+
cleanupMicroSurvey(id);
|
|
712
|
+
payload._completionTracker?.onComplete?.();
|
|
713
|
+
} catch (error) {
|
|
714
|
+
console.error('[DAP] Micro survey submission failed:', error);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
buttonsEl.appendChild(cancelBtn);
|
|
720
|
+
buttonsEl.appendChild(submitBtn);
|
|
721
|
+
microSurvey.appendChild(buttonsEl);
|
|
722
|
+
|
|
723
|
+
return microSurvey;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function showMicroSurvey(state: MicroSurveyState, payload: SurveyPayload): void {
|
|
727
|
+
document.body.appendChild(state.element);
|
|
728
|
+
|
|
729
|
+
// Position micro survey
|
|
730
|
+
positionMicroSurvey(state.element, state.targetElement, payload.position || 'center');
|
|
731
|
+
|
|
732
|
+
// Animate in
|
|
733
|
+
requestAnimationFrame(() => {
|
|
734
|
+
state.element.style.opacity = '1';
|
|
735
|
+
state.element.style.transform = 'scale(1) translateY(0)';
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
state.isActive = true;
|
|
739
|
+
|
|
740
|
+
// Set up cleanup handlers
|
|
741
|
+
const cleanup = () => cleanupMicroSurvey(state.id);
|
|
742
|
+
state.cleanup.push(cleanup);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function positionMicroSurvey(element: HTMLElement, targetElement?: HTMLElement, position: string = 'center'): void {
|
|
746
|
+
if (targetElement) {
|
|
747
|
+
// Position relative to target element
|
|
748
|
+
const targetRect = targetElement.getBoundingClientRect();
|
|
749
|
+
const elementRect = element.getBoundingClientRect();
|
|
750
|
+
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
751
|
+
|
|
752
|
+
switch (position) {
|
|
753
|
+
case 'top':
|
|
754
|
+
element.style.left = `${targetRect.left + (targetRect.width - elementRect.width) / 2}px`;
|
|
755
|
+
element.style.top = `${targetRect.top - elementRect.height - 10}px`;
|
|
756
|
+
break;
|
|
757
|
+
case 'bottom':
|
|
758
|
+
element.style.left = `${targetRect.left + (targetRect.width - elementRect.width) / 2}px`;
|
|
759
|
+
element.style.top = `${targetRect.bottom + 10}px`;
|
|
760
|
+
break;
|
|
761
|
+
case 'left':
|
|
762
|
+
element.style.left = `${targetRect.left - elementRect.width - 10}px`;
|
|
763
|
+
element.style.top = `${targetRect.top + (targetRect.height - elementRect.height) / 2}px`;
|
|
764
|
+
break;
|
|
765
|
+
case 'right':
|
|
766
|
+
element.style.left = `${targetRect.right + 10}px`;
|
|
767
|
+
element.style.top = `${targetRect.top + (targetRect.height - elementRect.height) / 2}px`;
|
|
768
|
+
break;
|
|
769
|
+
default: // center
|
|
770
|
+
element.style.left = `${(viewport.width - elementRect.width) / 2}px`;
|
|
771
|
+
element.style.top = `${(viewport.height - elementRect.height) / 2}px`;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Ensure element stays within viewport bounds
|
|
775
|
+
const rect = element.getBoundingClientRect();
|
|
776
|
+
if (rect.right > viewport.width) {
|
|
777
|
+
element.style.left = `${viewport.width - elementRect.width - 10}px`;
|
|
778
|
+
}
|
|
779
|
+
if (rect.bottom > viewport.height) {
|
|
780
|
+
element.style.top = `${viewport.height - elementRect.height - 10}px`;
|
|
781
|
+
}
|
|
782
|
+
if (rect.left < 0) {
|
|
783
|
+
element.style.left = '10px';
|
|
784
|
+
}
|
|
785
|
+
if (rect.top < 0) {
|
|
786
|
+
element.style.top = '10px';
|
|
787
|
+
}
|
|
788
|
+
} else {
|
|
789
|
+
// Center on screen
|
|
790
|
+
element.style.left = '50%';
|
|
791
|
+
element.style.top = '50%';
|
|
792
|
+
element.style.transform = 'translate(-50%, -50%) scale(0.95)';
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function createRatingContent(container: HTMLElement, payload: SurveyPayload, id: string): void {
|
|
797
|
+
const min = payload.rating?.min || 1;
|
|
798
|
+
const max = payload.rating?.max || 5;
|
|
799
|
+
|
|
800
|
+
const ratingContainer = document.createElement('div');
|
|
801
|
+
ratingContainer.style.cssText = `
|
|
802
|
+
display: flex;
|
|
803
|
+
gap: 8px;
|
|
804
|
+
align-items: center;
|
|
805
|
+
justify-content: center;
|
|
806
|
+
`;
|
|
807
|
+
|
|
808
|
+
for (let i = min; i <= max; i++) {
|
|
809
|
+
const star = document.createElement('button');
|
|
810
|
+
star.type = 'button';
|
|
811
|
+
star.innerHTML = '★';
|
|
812
|
+
star.dataset.value = i.toString();
|
|
813
|
+
star.style.cssText = `
|
|
814
|
+
background: none;
|
|
815
|
+
border: none;
|
|
816
|
+
font-size: 24px;
|
|
817
|
+
color: #d1d5db;
|
|
818
|
+
cursor: pointer;
|
|
819
|
+
transition: color 0.2s;
|
|
820
|
+
`;
|
|
821
|
+
|
|
822
|
+
star.addEventListener('click', () => {
|
|
823
|
+
// Update visual state
|
|
824
|
+
ratingContainer.querySelectorAll('button').forEach((btn, idx) => {
|
|
825
|
+
btn.style.color = idx < i ? '#fbbf24' : '#d1d5db';
|
|
826
|
+
});
|
|
827
|
+
// Store value
|
|
828
|
+
ratingContainer.dataset.value = i.toString();
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
ratingContainer.appendChild(star);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
container.appendChild(ratingContainer);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function createChoiceContent(container: HTMLElement, payload: SurveyPayload, id: string): void {
|
|
838
|
+
if (!payload.options?.length) return;
|
|
839
|
+
|
|
840
|
+
const choiceContainer = document.createElement('div');
|
|
841
|
+
choiceContainer.style.cssText = `
|
|
842
|
+
display: flex;
|
|
843
|
+
flex-direction: column;
|
|
844
|
+
gap: 8px;
|
|
845
|
+
`;
|
|
846
|
+
|
|
847
|
+
payload.options.forEach((option, index) => {
|
|
848
|
+
const optionEl = document.createElement('button');
|
|
849
|
+
optionEl.type = 'button';
|
|
850
|
+
optionEl.textContent = option.label;
|
|
851
|
+
optionEl.dataset.value = option.value;
|
|
852
|
+
optionEl.style.cssText = `
|
|
853
|
+
padding: 12px 16px;
|
|
854
|
+
border: 1px solid #d1d5db;
|
|
855
|
+
border-radius: 6px;
|
|
856
|
+
background: #ffffff;
|
|
857
|
+
color: #374151;
|
|
858
|
+
cursor: pointer;
|
|
859
|
+
text-align: left;
|
|
860
|
+
transition: all 0.2s;
|
|
861
|
+
`;
|
|
862
|
+
|
|
863
|
+
optionEl.addEventListener('click', () => {
|
|
864
|
+
// Clear previous selection
|
|
865
|
+
choiceContainer.querySelectorAll('button').forEach(btn => {
|
|
866
|
+
btn.style.background = '#ffffff';
|
|
867
|
+
btn.style.borderColor = '#d1d5db';
|
|
868
|
+
});
|
|
869
|
+
// Select this option
|
|
870
|
+
optionEl.style.background = '#eff6ff';
|
|
871
|
+
optionEl.style.borderColor = '#3b82f6';
|
|
872
|
+
choiceContainer.dataset.value = option.value;
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
choiceContainer.appendChild(optionEl);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
container.appendChild(choiceContainer);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function createTextContent(container: HTMLElement, payload: SurveyPayload, id: string): void {
|
|
882
|
+
const textarea = document.createElement('textarea');
|
|
883
|
+
textarea.placeholder = payload.placeholder || 'Your feedback...';
|
|
884
|
+
textarea.style.cssText = `
|
|
885
|
+
width: 100%;
|
|
886
|
+
min-height: 80px;
|
|
887
|
+
padding: 12px;
|
|
888
|
+
border: 1px solid #d1d5db;
|
|
889
|
+
border-radius: 6px;
|
|
890
|
+
font-family: inherit;
|
|
891
|
+
font-size: 14px;
|
|
892
|
+
resize: vertical;
|
|
893
|
+
`;
|
|
894
|
+
|
|
895
|
+
container.appendChild(textarea);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function extractMicroSurveyData(element: HTMLElement, payload: SurveyPayload): any {
|
|
899
|
+
const surveyType = payload.type || 'choice';
|
|
900
|
+
|
|
901
|
+
switch (surveyType) {
|
|
902
|
+
case 'rating': {
|
|
903
|
+
const ratingContainer = element.querySelector('[data-value]') as HTMLElement;
|
|
904
|
+
return ratingContainer?.dataset.value ? parseInt(ratingContainer.dataset.value) : null;
|
|
905
|
+
}
|
|
906
|
+
case 'choice': {
|
|
907
|
+
const choiceContainer = element.querySelector('[data-value]') as HTMLElement;
|
|
908
|
+
return choiceContainer?.dataset.value || null;
|
|
909
|
+
}
|
|
910
|
+
case 'text': {
|
|
911
|
+
const textarea = element.querySelector('textarea') as HTMLTextAreaElement;
|
|
912
|
+
return textarea?.value || '';
|
|
913
|
+
}
|
|
914
|
+
default:
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
async function submitMicroSurveyData(data: any, payload: SurveyPayload, flow: SurveyFlow): Promise<void> {
|
|
920
|
+
const submissionData = {
|
|
921
|
+
stepId: payload.stepId,
|
|
922
|
+
sessionId: `user-session-${Date.now()}`,
|
|
923
|
+
submittedAt: new Date().toISOString(),
|
|
924
|
+
response: data,
|
|
925
|
+
question: payload.question,
|
|
926
|
+
type: payload.type,
|
|
927
|
+
client: {
|
|
928
|
+
userId: "",
|
|
929
|
+
clientIP: "",
|
|
930
|
+
userAgent: navigator.userAgent,
|
|
931
|
+
locale: navigator.language
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
console.log("[DAP] MicroSurvey submission payload:", submissionData);
|
|
936
|
+
|
|
937
|
+
// Submit to API if configuration is available
|
|
938
|
+
if (flow.config && payload.flowId && payload.organizationId && payload.siteId) {
|
|
939
|
+
const url = flow.config?.apiurl + `/iap-experience/${payload.organizationId}/${payload.siteId}/survey-responses/${payload.flowId}`;
|
|
940
|
+
const hostBase = location.origin;
|
|
941
|
+
|
|
942
|
+
console.log("[DAP] Submitting micro survey to API:", url);
|
|
943
|
+
|
|
944
|
+
await http(flow.config, url, {
|
|
945
|
+
method: "POST",
|
|
946
|
+
body: submissionData,
|
|
947
|
+
hostBase,
|
|
948
|
+
includeHostHeader: true
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
console.log("[DAP] MicroSurvey successfully submitted to API");
|
|
952
|
+
} else {
|
|
953
|
+
console.warn("[DAP] MicroSurvey API submission skipped - missing configuration");
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function cleanupMicroSurvey(id: string): void {
|
|
958
|
+
const state = activeMicroSurveys.get(id);
|
|
959
|
+
if (!state) return;
|
|
960
|
+
|
|
961
|
+
// Run cleanup functions
|
|
962
|
+
state.cleanup.forEach(fn => {
|
|
963
|
+
try {
|
|
964
|
+
fn();
|
|
965
|
+
} catch (error) {
|
|
966
|
+
console.error('[DAP] Cleanup error:', error);
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
// Remove element
|
|
971
|
+
if (state.element.parentElement) {
|
|
972
|
+
state.element.style.opacity = '0';
|
|
973
|
+
state.element.style.transform = 'scale(0.95) translateY(10px)';
|
|
974
|
+
setTimeout(() => {
|
|
975
|
+
if (state.element.parentElement) {
|
|
976
|
+
state.element.parentElement.removeChild(state.element);
|
|
977
|
+
}
|
|
978
|
+
}, 300);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Remove from active surveys
|
|
982
|
+
activeMicroSurveys.delete(id);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/* ===================== End Micro Survey Implementation ===================== */
|
|
986
|
+
|
|
987
|
+
function renderQuestion(question: SurveyQuestion, index: number): HTMLElement {
|
|
988
|
+
const wrapper = document.createElement("div");
|
|
989
|
+
wrapper.className = "dap-survey-question";
|
|
990
|
+
wrapper.dataset.type = question.type;
|
|
991
|
+
|
|
992
|
+
// Add full-width class to complex question types that need more space
|
|
993
|
+
if (["TextMulti", "NpsScale", "NpsOptions", "OpinionScaleChoice", "StarChoice"].includes(question.type)) {
|
|
994
|
+
wrapper.classList.add("dap-full-width");
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const label = document.createElement("label");
|
|
998
|
+
label.className = "dap-question-label";
|
|
999
|
+
label.textContent = `${index + 1}. ${question.question}`;
|
|
1000
|
+
wrapper.appendChild(label);
|
|
1001
|
+
|
|
1002
|
+
const inputContainer = document.createElement("div");
|
|
1003
|
+
inputContainer.className = "dap-question-input";
|
|
1004
|
+
|
|
1005
|
+
switch (question.type) {
|
|
1006
|
+
case "SingleChoice":
|
|
1007
|
+
renderSingleChoice(inputContainer, question);
|
|
1008
|
+
break;
|
|
1009
|
+
case "MultipleChoice":
|
|
1010
|
+
renderMultipleChoice(inputContainer, question);
|
|
1011
|
+
break;
|
|
1012
|
+
case "Dropdown":
|
|
1013
|
+
renderDropdown(inputContainer, question);
|
|
1014
|
+
break;
|
|
1015
|
+
case "TextSingle":
|
|
1016
|
+
renderTextSingle(inputContainer, question);
|
|
1017
|
+
break;
|
|
1018
|
+
case "TextMulti":
|
|
1019
|
+
renderTextMulti(inputContainer, question);
|
|
1020
|
+
break;
|
|
1021
|
+
case "OpinionScale":
|
|
1022
|
+
renderOpinionScale(inputContainer, question);
|
|
1023
|
+
break;
|
|
1024
|
+
case "OpinionScaleChoice":
|
|
1025
|
+
renderOpinionScaleChoice(inputContainer, question);
|
|
1026
|
+
break;
|
|
1027
|
+
case "NpsScale":
|
|
1028
|
+
renderNpsScale(inputContainer, question);
|
|
1029
|
+
break;
|
|
1030
|
+
case "NpsOptions":
|
|
1031
|
+
renderNpsOptions(inputContainer, question);
|
|
1032
|
+
break;
|
|
1033
|
+
case "StarRating":
|
|
1034
|
+
renderStarRating(inputContainer, question);
|
|
1035
|
+
break;
|
|
1036
|
+
case "StarChoice":
|
|
1037
|
+
renderStarChoice(inputContainer, question);
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
wrapper.appendChild(inputContainer);
|
|
1042
|
+
return wrapper;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function renderSingleChoice(container: HTMLElement, question: SurveyQuestion) {
|
|
1046
|
+
if (!question.options?.length) return;
|
|
1047
|
+
|
|
1048
|
+
question.options.forEach((option, i) => {
|
|
1049
|
+
const wrapper = document.createElement("div");
|
|
1050
|
+
wrapper.className = "dap-radio-wrapper";
|
|
1051
|
+
|
|
1052
|
+
const input = document.createElement("input");
|
|
1053
|
+
input.type = "radio";
|
|
1054
|
+
input.name = question.questionId;
|
|
1055
|
+
input.id = `${question.questionId}_${i}`;
|
|
1056
|
+
input.value = option;
|
|
1057
|
+
|
|
1058
|
+
const label = document.createElement("label");
|
|
1059
|
+
label.htmlFor = input.id;
|
|
1060
|
+
label.textContent = option;
|
|
1061
|
+
|
|
1062
|
+
wrapper.appendChild(input);
|
|
1063
|
+
wrapper.appendChild(label);
|
|
1064
|
+
container.appendChild(wrapper);
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function renderMultipleChoice(container: HTMLElement, question: SurveyQuestion) {
|
|
1069
|
+
if (!question.options?.length) return;
|
|
1070
|
+
|
|
1071
|
+
question.options.forEach((option, i) => {
|
|
1072
|
+
const wrapper = document.createElement("div");
|
|
1073
|
+
wrapper.className = "dap-checkbox-wrapper";
|
|
1074
|
+
|
|
1075
|
+
const input = document.createElement("input");
|
|
1076
|
+
input.type = "checkbox";
|
|
1077
|
+
input.name = `${question.questionId}[]`;
|
|
1078
|
+
input.id = `${question.questionId}_${i}`;
|
|
1079
|
+
input.value = option;
|
|
1080
|
+
|
|
1081
|
+
const label = document.createElement("label");
|
|
1082
|
+
label.htmlFor = input.id;
|
|
1083
|
+
label.textContent = option;
|
|
1084
|
+
|
|
1085
|
+
wrapper.appendChild(input);
|
|
1086
|
+
wrapper.appendChild(label);
|
|
1087
|
+
container.appendChild(wrapper);
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function renderDropdown(container: HTMLElement, question: SurveyQuestion) {
|
|
1092
|
+
if (!question.options?.length) return;
|
|
1093
|
+
|
|
1094
|
+
const select = document.createElement("select");
|
|
1095
|
+
select.name = question.questionId;
|
|
1096
|
+
|
|
1097
|
+
const defaultOption = document.createElement("option");
|
|
1098
|
+
defaultOption.value = "";
|
|
1099
|
+
defaultOption.textContent = "-- Select an option --";
|
|
1100
|
+
defaultOption.selected = true;
|
|
1101
|
+
defaultOption.disabled = true;
|
|
1102
|
+
select.appendChild(defaultOption);
|
|
1103
|
+
|
|
1104
|
+
question.options.forEach((option, i) => {
|
|
1105
|
+
const optionEl = document.createElement("option");
|
|
1106
|
+
optionEl.value = option;
|
|
1107
|
+
optionEl.textContent = option;
|
|
1108
|
+
select.appendChild(optionEl);
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
container.appendChild(select);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function renderTextSingle(container: HTMLElement, question: SurveyQuestion) {
|
|
1115
|
+
const input = document.createElement("input");
|
|
1116
|
+
input.type = "text";
|
|
1117
|
+
input.name = question.questionId;
|
|
1118
|
+
input.placeholder = "Your answer...";
|
|
1119
|
+
container.appendChild(input);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function renderTextMulti(container: HTMLElement, question: SurveyQuestion) {
|
|
1123
|
+
const textarea = document.createElement("textarea");
|
|
1124
|
+
textarea.name = question.questionId;
|
|
1125
|
+
textarea.placeholder = "Your answer...";
|
|
1126
|
+
textarea.rows = 4;
|
|
1127
|
+
container.appendChild(textarea);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function renderOpinionScale(container: HTMLElement, question: SurveyQuestion) {
|
|
1131
|
+
const min = question.scaleMin || 1;
|
|
1132
|
+
const max = question.scaleMax || 5;
|
|
1133
|
+
|
|
1134
|
+
const scaleContainer = document.createElement("div");
|
|
1135
|
+
scaleContainer.className = "dap-scale-container";
|
|
1136
|
+
|
|
1137
|
+
if (question.labelMin) {
|
|
1138
|
+
const minLabel = document.createElement("div");
|
|
1139
|
+
minLabel.className = "dap-scale-label";
|
|
1140
|
+
minLabel.textContent = question.labelMin;
|
|
1141
|
+
scaleContainer.appendChild(minLabel);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const scaleOptions = document.createElement("div");
|
|
1145
|
+
scaleOptions.className = "dap-scale-options";
|
|
1146
|
+
|
|
1147
|
+
for (let i = min; i <= max; i++) {
|
|
1148
|
+
const option = document.createElement("div");
|
|
1149
|
+
option.className = "dap-scale-option";
|
|
1150
|
+
|
|
1151
|
+
const input = document.createElement("input");
|
|
1152
|
+
input.type = "radio";
|
|
1153
|
+
input.name = question.questionId;
|
|
1154
|
+
input.id = `${question.questionId}_${i}`;
|
|
1155
|
+
input.value = i.toString();
|
|
1156
|
+
|
|
1157
|
+
const label = document.createElement("label");
|
|
1158
|
+
label.htmlFor = input.id;
|
|
1159
|
+
label.textContent = i.toString();
|
|
1160
|
+
|
|
1161
|
+
option.appendChild(input);
|
|
1162
|
+
option.appendChild(label);
|
|
1163
|
+
scaleOptions.appendChild(option);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
scaleContainer.appendChild(scaleOptions);
|
|
1167
|
+
|
|
1168
|
+
if (question.labelMax) {
|
|
1169
|
+
const maxLabel = document.createElement("div");
|
|
1170
|
+
maxLabel.className = "dap-scale-label";
|
|
1171
|
+
maxLabel.textContent = question.labelMax;
|
|
1172
|
+
scaleContainer.appendChild(maxLabel);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
container.appendChild(scaleContainer);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function renderOpinionScaleChoice(container: HTMLElement, question: SurveyQuestion) {
|
|
1179
|
+
const min = question.scaleMin || 1;
|
|
1180
|
+
const max = question.scaleMax || 5;
|
|
1181
|
+
const scaleSize = max - min + 1;
|
|
1182
|
+
|
|
1183
|
+
// Define emoji faces based on a 5-point scale
|
|
1184
|
+
const faces = ["😣", "😕", "😐", "🙂", "😄"];
|
|
1185
|
+
|
|
1186
|
+
// Container for face options
|
|
1187
|
+
const scaleContainer = document.createElement("div");
|
|
1188
|
+
scaleContainer.className = "dap-scale-faces";
|
|
1189
|
+
|
|
1190
|
+
// Create options for each point on the scale
|
|
1191
|
+
for (let i = min; i <= max; i++) {
|
|
1192
|
+
const faceIndex = Math.min(scaleSize - 1, Math.floor((i - min) / (max - min) * (faces.length - 1)));
|
|
1193
|
+
|
|
1194
|
+
const option = document.createElement("div");
|
|
1195
|
+
option.className = "dap-face-option";
|
|
1196
|
+
|
|
1197
|
+
const input = document.createElement("input");
|
|
1198
|
+
input.type = "radio";
|
|
1199
|
+
input.className = "dap-face-radio";
|
|
1200
|
+
input.name = question.questionId;
|
|
1201
|
+
input.id = `${question.questionId}_${i}`;
|
|
1202
|
+
input.value = i.toString();
|
|
1203
|
+
|
|
1204
|
+
const label = document.createElement("label");
|
|
1205
|
+
label.className = "dap-face-label";
|
|
1206
|
+
label.htmlFor = `${question.questionId}_${i}`;
|
|
1207
|
+
label.textContent = faces[faceIndex];
|
|
1208
|
+
label.title = `Rating: ${i}`;
|
|
1209
|
+
|
|
1210
|
+
option.appendChild(input);
|
|
1211
|
+
option.appendChild(label);
|
|
1212
|
+
scaleContainer.appendChild(option);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
container.appendChild(scaleContainer);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function renderNpsScale(container: HTMLElement, question: SurveyQuestion) {
|
|
1219
|
+
const min = question.scaleMin || 0;
|
|
1220
|
+
const max = question.scaleMax || 10;
|
|
1221
|
+
|
|
1222
|
+
const npsContainer = document.createElement("div");
|
|
1223
|
+
npsContainer.className = "dap-nps-container";
|
|
1224
|
+
|
|
1225
|
+
const scaleOptions = document.createElement("div");
|
|
1226
|
+
scaleOptions.className = "dap-nps-scale";
|
|
1227
|
+
|
|
1228
|
+
for (let i = min; i <= max; i++) {
|
|
1229
|
+
const option = document.createElement("div");
|
|
1230
|
+
option.className = "dap-nps-option";
|
|
1231
|
+
|
|
1232
|
+
const input = document.createElement("input");
|
|
1233
|
+
input.type = "radio";
|
|
1234
|
+
input.name = question.questionId;
|
|
1235
|
+
input.id = `${question.questionId}_${i}`;
|
|
1236
|
+
input.value = i.toString();
|
|
1237
|
+
|
|
1238
|
+
const label = document.createElement("label");
|
|
1239
|
+
label.htmlFor = input.id;
|
|
1240
|
+
label.textContent = i.toString();
|
|
1241
|
+
|
|
1242
|
+
option.appendChild(input);
|
|
1243
|
+
option.appendChild(label);
|
|
1244
|
+
scaleOptions.appendChild(option);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
npsContainer.appendChild(scaleOptions);
|
|
1248
|
+
|
|
1249
|
+
// Labels under the scale
|
|
1250
|
+
const labelContainer = document.createElement("div");
|
|
1251
|
+
labelContainer.className = "dap-nps-labels";
|
|
1252
|
+
|
|
1253
|
+
if (question.labelMin) {
|
|
1254
|
+
const minLabel = document.createElement("div");
|
|
1255
|
+
minLabel.className = "dap-nps-label-min";
|
|
1256
|
+
minLabel.textContent = question.labelMin;
|
|
1257
|
+
labelContainer.appendChild(minLabel);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (question.labelMax) {
|
|
1261
|
+
const maxLabel = document.createElement("div");
|
|
1262
|
+
maxLabel.className = "dap-nps-label-max";
|
|
1263
|
+
maxLabel.textContent = question.labelMax;
|
|
1264
|
+
labelContainer.appendChild(maxLabel);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
npsContainer.appendChild(labelContainer);
|
|
1268
|
+
container.appendChild(npsContainer);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function renderNpsOptions(container: HTMLElement, question: SurveyQuestion) {
|
|
1272
|
+
// Create predefined NPS categories
|
|
1273
|
+
const npsCategories = [
|
|
1274
|
+
{ key: "not_likely", label: "Not Likely (0-2)" },
|
|
1275
|
+
{ key: "somewhat_likely", label: "Somewhat Likely (3-8)" },
|
|
1276
|
+
{ key: "very_likely", label: "Very Likely (9-10)" }
|
|
1277
|
+
];
|
|
1278
|
+
|
|
1279
|
+
const optionsContainer = document.createElement("div");
|
|
1280
|
+
optionsContainer.className = "dap-nps-options";
|
|
1281
|
+
|
|
1282
|
+
npsCategories.forEach((category) => {
|
|
1283
|
+
const wrapper = document.createElement("div");
|
|
1284
|
+
wrapper.className = "dap-nps-category";
|
|
1285
|
+
|
|
1286
|
+
const input = document.createElement("input");
|
|
1287
|
+
input.type = "radio";
|
|
1288
|
+
input.name = question.questionId;
|
|
1289
|
+
input.id = `${question.questionId}_${category.key}`;
|
|
1290
|
+
input.value = category.key;
|
|
1291
|
+
|
|
1292
|
+
const label = document.createElement("label");
|
|
1293
|
+
label.htmlFor = input.id;
|
|
1294
|
+
label.textContent = category.label;
|
|
1295
|
+
|
|
1296
|
+
wrapper.appendChild(input);
|
|
1297
|
+
wrapper.appendChild(label);
|
|
1298
|
+
optionsContainer.appendChild(wrapper);
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
container.appendChild(optionsContainer);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function renderStarRating(container: HTMLElement, question: SurveyQuestion) {
|
|
1305
|
+
const max = question.scaleMax || 5;
|
|
1306
|
+
|
|
1307
|
+
// Define star labels for tooltips based on options if provided
|
|
1308
|
+
// Otherwise use default labels
|
|
1309
|
+
const defaultStarLabels = {
|
|
1310
|
+
5: "Excellent",
|
|
1311
|
+
4: "Very Good",
|
|
1312
|
+
3: "Good",
|
|
1313
|
+
2: "Fair",
|
|
1314
|
+
1: "Poor"
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
// Create wrapper for stars and clear button
|
|
1318
|
+
const ratingWrapper = document.createElement("div");
|
|
1319
|
+
ratingWrapper.className = "dap-rating-wrapper";
|
|
1320
|
+
|
|
1321
|
+
// Create radio button-based star rating
|
|
1322
|
+
const starContainer = document.createElement("div");
|
|
1323
|
+
starContainer.className = "dap-star-rating";
|
|
1324
|
+
|
|
1325
|
+
// Add a hidden input to track if any star is selected
|
|
1326
|
+
const hiddenStatusInput = document.createElement("input");
|
|
1327
|
+
hiddenStatusInput.type = "hidden";
|
|
1328
|
+
hiddenStatusInput.className = "dap-star-status";
|
|
1329
|
+
hiddenStatusInput.value = "0";
|
|
1330
|
+
starContainer.appendChild(hiddenStatusInput);
|
|
1331
|
+
|
|
1332
|
+
// Create stars
|
|
1333
|
+
for (let i = 1; i <= max; i++) {
|
|
1334
|
+
// Create input radio button (hidden visually)
|
|
1335
|
+
const input = document.createElement("input");
|
|
1336
|
+
input.type = "radio";
|
|
1337
|
+
input.name = question.questionId;
|
|
1338
|
+
input.id = `${question.questionId}_${i}`;
|
|
1339
|
+
// Calculate the actual rating value (from 1 to max) since we're using RTL
|
|
1340
|
+
const actualRating = max - i + 1;
|
|
1341
|
+
input.value = actualRating.toString();
|
|
1342
|
+
input.className = "dap-star-input";
|
|
1343
|
+
|
|
1344
|
+
// Update status when a star is selected
|
|
1345
|
+
input.addEventListener("change", () => {
|
|
1346
|
+
if (input.checked) {
|
|
1347
|
+
hiddenStatusInput.value = "1";
|
|
1348
|
+
clearButton.style.display = "inline-flex";
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
// Create star label (visible element)
|
|
1353
|
+
const label = document.createElement("label");
|
|
1354
|
+
label.htmlFor = input.id;
|
|
1355
|
+
label.className = "dap-star-label";
|
|
1356
|
+
|
|
1357
|
+
// Get the appropriate label for this star
|
|
1358
|
+
const starLabel = question.options && question.options.length === max
|
|
1359
|
+
? question.options[actualRating - 1]
|
|
1360
|
+
: defaultStarLabels[actualRating as keyof typeof defaultStarLabels];
|
|
1361
|
+
|
|
1362
|
+
label.setAttribute("aria-label", `${actualRating} star${actualRating > 1 ? 's' : ''}`);
|
|
1363
|
+
label.setAttribute("title", `${actualRating} star${actualRating > 1 ? 's' : ''}: ${starLabel}`);
|
|
1364
|
+
|
|
1365
|
+
// Add both to container
|
|
1366
|
+
starContainer.appendChild(input);
|
|
1367
|
+
starContainer.appendChild(label);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Create clear button
|
|
1371
|
+
const clearButton = document.createElement("button");
|
|
1372
|
+
clearButton.type = "button";
|
|
1373
|
+
clearButton.className = "dap-clear-rating";
|
|
1374
|
+
clearButton.textContent = "Clear";
|
|
1375
|
+
clearButton.title = "Clear rating";
|
|
1376
|
+
clearButton.style.display = "none"; // Hidden by default
|
|
1377
|
+
|
|
1378
|
+
// Clear selection when clicked
|
|
1379
|
+
clearButton.addEventListener("click", () => {
|
|
1380
|
+
// Uncheck all inputs
|
|
1381
|
+
starContainer.querySelectorAll("input[type='radio']").forEach((input) => {
|
|
1382
|
+
(input as HTMLInputElement).checked = false;
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
// Update status
|
|
1386
|
+
hiddenStatusInput.value = "0";
|
|
1387
|
+
|
|
1388
|
+
// Hide clear button
|
|
1389
|
+
clearButton.style.display = "none";
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
ratingWrapper.appendChild(starContainer);
|
|
1393
|
+
ratingWrapper.appendChild(clearButton);
|
|
1394
|
+
container.appendChild(ratingWrapper);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function renderStarChoice(container: HTMLElement, question: SurveyQuestion) {
|
|
1398
|
+
// Set sensible defaults according to requirements
|
|
1399
|
+
const min = question.scaleMin || 1;
|
|
1400
|
+
const max = question.scaleMax || 5;
|
|
1401
|
+
|
|
1402
|
+
// Default star labels if options are not provided
|
|
1403
|
+
const defaultLabels = ["Poor", "Fair", "Good", "Very Good", "Excellent"];
|
|
1404
|
+
|
|
1405
|
+
// Use options if provided, otherwise use default labels (limited to max)
|
|
1406
|
+
const starLabels = question.options && question.options.length > 0
|
|
1407
|
+
? question.options
|
|
1408
|
+
: defaultLabels.slice(0, max);
|
|
1409
|
+
|
|
1410
|
+
// Create main container
|
|
1411
|
+
const choiceContainer = document.createElement("div");
|
|
1412
|
+
choiceContainer.className = "dap-star-choice-container";
|
|
1413
|
+
choiceContainer.setAttribute("role", "radiogroup");
|
|
1414
|
+
choiceContainer.setAttribute("aria-labelledby", `${question.questionId}-heading`);
|
|
1415
|
+
|
|
1416
|
+
// Create options list
|
|
1417
|
+
const optionsList = document.createElement("div");
|
|
1418
|
+
optionsList.className = "dap-star-choice-options";
|
|
1419
|
+
|
|
1420
|
+
// Create each star choice option
|
|
1421
|
+
for (let i = 1; i <= max; i++) {
|
|
1422
|
+
// Create option container
|
|
1423
|
+
const optionItem = document.createElement("div");
|
|
1424
|
+
optionItem.className = "dap-star-choice-option";
|
|
1425
|
+
|
|
1426
|
+
// Create radio input
|
|
1427
|
+
const input = document.createElement("input");
|
|
1428
|
+
input.type = "radio";
|
|
1429
|
+
input.className = "dap-star-choice-input";
|
|
1430
|
+
input.name = question.questionId;
|
|
1431
|
+
input.id = `${question.questionId}_${i}`;
|
|
1432
|
+
input.value = i.toString();
|
|
1433
|
+
|
|
1434
|
+
// Create label that contains stars and text
|
|
1435
|
+
const label = document.createElement("label");
|
|
1436
|
+
label.className = "dap-star-choice-label";
|
|
1437
|
+
label.htmlFor = input.id;
|
|
1438
|
+
|
|
1439
|
+
// Create stars display
|
|
1440
|
+
const starsDisplay = document.createElement("div");
|
|
1441
|
+
starsDisplay.className = "dap-star-choice-stars";
|
|
1442
|
+
|
|
1443
|
+
// Add filled and empty stars based on the rating
|
|
1444
|
+
for (let j = 1; j <= max; j++) {
|
|
1445
|
+
const starSpan = document.createElement("span");
|
|
1446
|
+
starSpan.className = j <= i
|
|
1447
|
+
? "dap-star-choice-star filled"
|
|
1448
|
+
: "dap-star-choice-star";
|
|
1449
|
+
starSpan.innerHTML = "★"; // Unicode star character
|
|
1450
|
+
starsDisplay.appendChild(starSpan);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// Create text label
|
|
1454
|
+
const textLabel = document.createElement("span");
|
|
1455
|
+
textLabel.className = "dap-star-choice-text";
|
|
1456
|
+
textLabel.textContent = starLabels[i-1];
|
|
1457
|
+
|
|
1458
|
+
// Add stars and text to the label
|
|
1459
|
+
label.appendChild(starsDisplay);
|
|
1460
|
+
label.appendChild(textLabel);
|
|
1461
|
+
|
|
1462
|
+
// Add input and label to the option container
|
|
1463
|
+
optionItem.appendChild(input);
|
|
1464
|
+
optionItem.appendChild(label);
|
|
1465
|
+
|
|
1466
|
+
// Add option to the list
|
|
1467
|
+
optionsList.appendChild(optionItem);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// Add options list to the main container
|
|
1471
|
+
choiceContainer.appendChild(optionsList);
|
|
1472
|
+
|
|
1473
|
+
// Add the container to the parent element
|
|
1474
|
+
container.appendChild(choiceContainer);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/* ===================== Survey Adaptive Sizing ===================== */
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* Adjusts survey modal size based on content to minimize scrollbars
|
|
1481
|
+
* @param modal The modal element
|
|
1482
|
+
* @param body The modal body element
|
|
1483
|
+
*/
|
|
1484
|
+
function adjustSurveyModalSize(modal: HTMLElement, body: HTMLElement): void {
|
|
1485
|
+
console.debug('[DAP] Adjusting survey modal size based on content');
|
|
1486
|
+
|
|
1487
|
+
try {
|
|
1488
|
+
// Get content dimensions
|
|
1489
|
+
const bodyRect = body.getBoundingClientRect();
|
|
1490
|
+
const modalRect = modal.getBoundingClientRect();
|
|
1491
|
+
|
|
1492
|
+
console.debug('[DAP] Content dimensions:', {
|
|
1493
|
+
bodyWidth: bodyRect.width,
|
|
1494
|
+
bodyHeight: bodyRect.height,
|
|
1495
|
+
modalWidth: modalRect.width,
|
|
1496
|
+
modalHeight: modalRect.height
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
// Remove existing size classes
|
|
1500
|
+
modal.classList.remove('dap-size-small', 'dap-size-medium', 'dap-size-large', 'dap-scrollable');
|
|
1501
|
+
|
|
1502
|
+
// Determine optimal size based on content width
|
|
1503
|
+
let sizeClass = 'dap-size-medium'; // default
|
|
1504
|
+
|
|
1505
|
+
if (bodyRect.width <= 480) {
|
|
1506
|
+
sizeClass = 'dap-size-small';
|
|
1507
|
+
} else if (bodyRect.width <= 700) {
|
|
1508
|
+
sizeClass = 'dap-size-medium';
|
|
1509
|
+
} else if (bodyRect.width <= 1000) {
|
|
1510
|
+
sizeClass = 'dap-size-large';
|
|
1511
|
+
} else {
|
|
1512
|
+
sizeClass = 'dap-size-large'; // max size for very wide content
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
modal.classList.add(sizeClass);
|
|
1516
|
+
|
|
1517
|
+
// Check if content needs scrolling after applying size class
|
|
1518
|
+
requestAnimationFrame(() => {
|
|
1519
|
+
const updatedBodyRect = body.getBoundingClientRect();
|
|
1520
|
+
const updatedModalRect = modal.getBoundingClientRect();
|
|
1521
|
+
|
|
1522
|
+
// Check for overflow
|
|
1523
|
+
const needsHorizontalScroll = body.scrollWidth > updatedBodyRect.width;
|
|
1524
|
+
const needsVerticalScroll = body.scrollHeight > updatedBodyRect.height;
|
|
1525
|
+
|
|
1526
|
+
// Check for viewport overflow
|
|
1527
|
+
const viewportWidth = window.innerWidth;
|
|
1528
|
+
const viewportHeight = window.innerHeight;
|
|
1529
|
+
const wouldOverflowViewport = updatedModalRect.width > viewportWidth * 0.9 ||
|
|
1530
|
+
updatedModalRect.height > viewportHeight * 0.9;
|
|
1531
|
+
|
|
1532
|
+
if (needsHorizontalScroll || needsVerticalScroll || wouldOverflowViewport) {
|
|
1533
|
+
modal.classList.add('dap-scrollable');
|
|
1534
|
+
console.debug('[DAP] Added scrollable class due to overflow:', {
|
|
1535
|
+
needsHorizontalScroll,
|
|
1536
|
+
needsVerticalScroll,
|
|
1537
|
+
wouldOverflowViewport
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
console.debug('[DAP] Final survey modal size class:', sizeClass, {
|
|
1542
|
+
hasScrollable: modal.classList.contains('dap-scrollable'),
|
|
1543
|
+
finalWidth: updatedModalRect.width,
|
|
1544
|
+
finalHeight: updatedModalRect.height
|
|
1545
|
+
});
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
console.warn('[DAP] Error adjusting survey modal size:', error);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/* ===================== Modal Shell (reused from modal.ts) ===================== */
|
|
1554
|
+
|
|
1555
|
+
function createShell(theme?: Record<string, string>) {
|
|
1556
|
+
const root = ensureRoot();
|
|
1557
|
+
|
|
1558
|
+
const wrap = document.createElement("div");
|
|
1559
|
+
wrap.className = "dap-modal-wrap";
|
|
1560
|
+
wrap.setAttribute("role", "dialog");
|
|
1561
|
+
wrap.setAttribute("aria-modal", "true");
|
|
1562
|
+
wrap.style.pointerEvents = "auto";
|
|
1563
|
+
wrap.style.zIndex = "2147483647";
|
|
1564
|
+
|
|
1565
|
+
const dlg = document.createElement("div");
|
|
1566
|
+
dlg.className = "dap-modal dap-survey-modal";
|
|
1567
|
+
dlg.tabIndex = -1;
|
|
1568
|
+
if (theme) for (const [k, v] of Object.entries(theme)) (dlg.style as any).setProperty(k, v as string);
|
|
1569
|
+
|
|
1570
|
+
const headerBar = document.createElement("div");
|
|
1571
|
+
headerBar.className = "dap-header-bar";
|
|
1572
|
+
|
|
1573
|
+
const titleEl = document.createElement("div");
|
|
1574
|
+
titleEl.className = "dap-modal-header";
|
|
1575
|
+
|
|
1576
|
+
const closeBtn = document.createElement("button");
|
|
1577
|
+
closeBtn.className = "dap-close";
|
|
1578
|
+
closeBtn.setAttribute("aria-label", "Close");
|
|
1579
|
+
closeBtn.innerHTML = "×";
|
|
1580
|
+
|
|
1581
|
+
headerBar.appendChild(titleEl); headerBar.appendChild(closeBtn);
|
|
1582
|
+
|
|
1583
|
+
const body = document.createElement("div"); body.className = "dap-modal-body dap-survey-body";
|
|
1584
|
+
const footer = document.createElement("div"); footer.className = "dap-footer dap-nav";
|
|
1585
|
+
const prevBtn = document.createElement("button"); prevBtn.className = "dap-secondary"; prevBtn.type = "button"; prevBtn.textContent = "Cancel";
|
|
1586
|
+
const nextBtn = document.createElement("button"); nextBtn.className = "dap-cta"; nextBtn.type = "button"; nextBtn.textContent = "Submit";
|
|
1587
|
+
footer.appendChild(prevBtn); footer.appendChild(nextBtn);
|
|
1588
|
+
|
|
1589
|
+
dlg.appendChild(headerBar); dlg.appendChild(body); dlg.appendChild(footer);
|
|
1590
|
+
|
|
1591
|
+
(root as unknown as Node).appendChild(wrap);
|
|
1592
|
+
wrap.appendChild(dlg);
|
|
1593
|
+
|
|
1594
|
+
return { wrap, dlg, headerBar, titleEl, body, footer, prevBtn, nextBtn, closeBtn };
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
function ensureRoot(): ShadowRoot {
|
|
1598
|
+
let host = document.querySelector("dap-root") as HTMLElement | null;
|
|
1599
|
+
if (!host) {
|
|
1600
|
+
host = document.createElement("dap-root");
|
|
1601
|
+
host.style.position = "fixed";
|
|
1602
|
+
host.style.zIndex = "2147483647"; // Maximum z-index
|
|
1603
|
+
host.style.inset = "0";
|
|
1604
|
+
host.style.pointerEvents = "none";
|
|
1605
|
+
host.style.width = "100vw";
|
|
1606
|
+
host.style.height = "100vh";
|
|
1607
|
+
host.style.display = "flex";
|
|
1608
|
+
host.style.alignItems = "center";
|
|
1609
|
+
host.style.justifyContent = "center";
|
|
1610
|
+
document.documentElement.appendChild(host);
|
|
1611
|
+
}
|
|
1612
|
+
const shadow = host.shadowRoot ?? host.attachShadow({ mode: "open" });
|
|
1613
|
+
|
|
1614
|
+
if (!shadow.getElementById("dap-modal-style")) {
|
|
1615
|
+
const style = document.createElement("style");
|
|
1616
|
+
style.id = "dap-modal-style";
|
|
1617
|
+
style.textContent = modalCssText;
|
|
1618
|
+
shadow.appendChild(style);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
if (!shadow.getElementById("dap-survey-style")) {
|
|
1622
|
+
const style = document.createElement("style");
|
|
1623
|
+
style.id = "dap-survey-style";
|
|
1624
|
+
style.textContent = surveyCssText;
|
|
1625
|
+
shadow.appendChild(style);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
return shadow;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/* ===================== Utilities ===================== */
|
|
1632
|
+
|
|
1633
|
+
function trapTab(e: KeyboardEvent, root: HTMLElement) {
|
|
1634
|
+
const focusables = Array.from(root.querySelectorAll<HTMLElement>('a,button,input,textarea,select,details,[tabindex]:not([tabindex="-1"])')).filter(el => !el.hasAttribute("disabled"));
|
|
1635
|
+
if (!focusables.length) return;
|
|
1636
|
+
const first = focusables[0], last = focusables[focusables.length - 1];
|
|
1637
|
+
if (e.shiftKey && document.activeElement === first) { last.focus(); e.preventDefault(); }
|
|
1638
|
+
else if (!e.shiftKey && document.activeElement === last) { first.focus(); e.preventDefault(); }
|
|
1639
|
+
}
|