@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,1337 @@
|
|
|
1
|
+
// src/experiences/modal.ts
|
|
2
|
+
// Simple modal experience renderer
|
|
3
|
+
|
|
4
|
+
import { sanitizeHtml } from "../utils/sanitize";
|
|
5
|
+
import { register } from "./registry";
|
|
6
|
+
import { modalCssText } from "../styles/modal.css";
|
|
7
|
+
import type { ModalPayload, ModalContent, ModalSize } from "./types";
|
|
8
|
+
|
|
9
|
+
type ModalFlow = { id: string; type: "modal"; payload: ModalPayload };
|
|
10
|
+
|
|
11
|
+
export function registerModal() {
|
|
12
|
+
register("modal", renderModal);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function renderModal(flow: ModalFlow): Promise<void> {
|
|
16
|
+
const { payload, id } = flow;
|
|
17
|
+
|
|
18
|
+
console.debug("[DAP] renderModal called with payload:", payload);
|
|
19
|
+
console.debug("[DAP] Modal flow ID:", id);
|
|
20
|
+
|
|
21
|
+
// Extract completion tracker
|
|
22
|
+
const completionTracker = payload._completionTracker;
|
|
23
|
+
|
|
24
|
+
// Ensure CSS is injected
|
|
25
|
+
ensureStyles();
|
|
26
|
+
|
|
27
|
+
// Create modal elements
|
|
28
|
+
const { overlay, modal, header, body, footer } = createModalElements(payload);
|
|
29
|
+
|
|
30
|
+
// Add to DOM
|
|
31
|
+
document.documentElement.appendChild(overlay);
|
|
32
|
+
|
|
33
|
+
// Focus management
|
|
34
|
+
const prevActive = document.activeElement as HTMLElement | null;
|
|
35
|
+
modal.setAttribute("role", "dialog");
|
|
36
|
+
modal.setAttribute("aria-modal", "true");
|
|
37
|
+
modal.setAttribute("aria-labelledby", "modal-title");
|
|
38
|
+
|
|
39
|
+
// Enhanced accessibility setup
|
|
40
|
+
setupModalAccessibility(modal, overlay);
|
|
41
|
+
|
|
42
|
+
// Event handlers
|
|
43
|
+
function closeModal() {
|
|
44
|
+
// Cleanup accessibility handlers
|
|
45
|
+
if ((modal as any)._accessibilityCleanup) {
|
|
46
|
+
(modal as any)._accessibilityCleanup();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
overlay.style.animation = "modalFadeOut 0.2s ease-in";
|
|
50
|
+
modal.style.animation = "modalSlideOut 0.2s ease-in";
|
|
51
|
+
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
overlay.remove();
|
|
54
|
+
prevActive?.focus();
|
|
55
|
+
|
|
56
|
+
// Signal completion
|
|
57
|
+
if (completionTracker?.onComplete) {
|
|
58
|
+
console.debug(`[DAP] Completing modal flow: ${id}`);
|
|
59
|
+
completionTracker.onComplete();
|
|
60
|
+
}
|
|
61
|
+
}, 200);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Close on overlay click
|
|
65
|
+
overlay.addEventListener("click", (e) => {
|
|
66
|
+
if (e.target === overlay) {
|
|
67
|
+
closeModal();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Close button
|
|
72
|
+
const closeBtn = modal.querySelector(".dap-modal-close") as HTMLButtonElement;
|
|
73
|
+
if (closeBtn) {
|
|
74
|
+
closeBtn.addEventListener("click", closeModal);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Escape key
|
|
78
|
+
function handleKeyboard(e: KeyboardEvent) {
|
|
79
|
+
if (e.key === "Escape") {
|
|
80
|
+
closeModal();
|
|
81
|
+
document.removeEventListener("keydown", handleKeyboard);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
document.addEventListener("keydown", handleKeyboard);
|
|
85
|
+
|
|
86
|
+
// Add exit animations
|
|
87
|
+
if (!document.getElementById("dap-modal-exit-styles")) {
|
|
88
|
+
const style = document.createElement("style");
|
|
89
|
+
style.id = "dap-modal-exit-styles";
|
|
90
|
+
style.textContent = `
|
|
91
|
+
@keyframes modalFadeOut {
|
|
92
|
+
from { opacity: 1; backdrop-filter: blur(4px); }
|
|
93
|
+
to { opacity: 0; backdrop-filter: blur(0px); }
|
|
94
|
+
}
|
|
95
|
+
@keyframes modalSlideOut {
|
|
96
|
+
from { opacity: 1; transform: scale(1) translateY(0); }
|
|
97
|
+
to { opacity: 0; transform: scale(0.95) translateY(-10px); }
|
|
98
|
+
}
|
|
99
|
+
`;
|
|
100
|
+
document.head.appendChild(style);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Enterprise dragging functionality
|
|
104
|
+
setupModalDragging(modal, header, overlay);
|
|
105
|
+
|
|
106
|
+
// Media lifecycle management
|
|
107
|
+
setupMediaHandling(modal, overlay);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function setupModalDragging(modal: HTMLElement, header: HTMLElement, overlay: HTMLElement): void {
|
|
111
|
+
let isDragging = false;
|
|
112
|
+
let dragStartX = 0;
|
|
113
|
+
let dragStartY = 0;
|
|
114
|
+
let modalStartX = 0;
|
|
115
|
+
let modalStartY = 0;
|
|
116
|
+
|
|
117
|
+
// Make header the drag handle
|
|
118
|
+
header.style.cursor = "move";
|
|
119
|
+
|
|
120
|
+
const startDrag = (e: MouseEvent) => {
|
|
121
|
+
isDragging = true;
|
|
122
|
+
dragStartX = e.clientX;
|
|
123
|
+
dragStartY = e.clientY;
|
|
124
|
+
|
|
125
|
+
const rect = modal.getBoundingClientRect();
|
|
126
|
+
modalStartX = rect.left;
|
|
127
|
+
modalStartY = rect.top;
|
|
128
|
+
|
|
129
|
+
header.classList.add("dragging");
|
|
130
|
+
overlay.classList.add("dragging");
|
|
131
|
+
|
|
132
|
+
document.addEventListener("mousemove", drag);
|
|
133
|
+
document.addEventListener("mouseup", endDrag);
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const drag = (e: MouseEvent) => {
|
|
138
|
+
if (!isDragging) return;
|
|
139
|
+
|
|
140
|
+
const deltaX = e.clientX - dragStartX;
|
|
141
|
+
const deltaY = e.clientY - dragStartY;
|
|
142
|
+
|
|
143
|
+
let newX = modalStartX + deltaX;
|
|
144
|
+
let newY = modalStartY + deltaY;
|
|
145
|
+
|
|
146
|
+
// Constrain to viewport with 10px margin
|
|
147
|
+
const modalRect = modal.getBoundingClientRect();
|
|
148
|
+
const viewport = {
|
|
149
|
+
width: window.innerWidth,
|
|
150
|
+
height: window.innerHeight
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
newX = Math.max(10, Math.min(newX, viewport.width - modalRect.width - 10));
|
|
154
|
+
newY = Math.max(10, Math.min(newY, viewport.height - modalRect.height - 10));
|
|
155
|
+
|
|
156
|
+
// Position modal
|
|
157
|
+
modal.style.position = "fixed";
|
|
158
|
+
modal.style.left = `${newX}px`;
|
|
159
|
+
modal.style.top = `${newY}px`;
|
|
160
|
+
modal.style.transform = "none";
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const endDrag = () => {
|
|
164
|
+
isDragging = false;
|
|
165
|
+
header.classList.remove("dragging");
|
|
166
|
+
overlay.classList.remove("dragging");
|
|
167
|
+
|
|
168
|
+
document.removeEventListener("mousemove", drag);
|
|
169
|
+
document.removeEventListener("mouseup", endDrag);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
header.addEventListener("mousedown", startDrag);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function setupModalAccessibility(modal: HTMLElement, overlay: HTMLElement): void {
|
|
176
|
+
// Enhanced focus management - Focus trap
|
|
177
|
+
const focusableElements = modal.querySelectorAll(
|
|
178
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
179
|
+
) as NodeListOf<HTMLElement>;
|
|
180
|
+
|
|
181
|
+
const firstFocusable = focusableElements[0];
|
|
182
|
+
const lastFocusable = focusableElements[focusableElements.length - 1];
|
|
183
|
+
|
|
184
|
+
// Initial focus to first focusable element
|
|
185
|
+
if (firstFocusable) {
|
|
186
|
+
firstFocusable.focus();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Focus trap on Tab/Shift+Tab
|
|
190
|
+
const handleTabKey = (e: KeyboardEvent) => {
|
|
191
|
+
if (e.key === "Tab") {
|
|
192
|
+
if (e.shiftKey) {
|
|
193
|
+
// Shift + Tab - focus previous
|
|
194
|
+
if (document.activeElement === firstFocusable) {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
lastFocusable?.focus();
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
// Tab - focus next
|
|
200
|
+
if (document.activeElement === lastFocusable) {
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
firstFocusable?.focus();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
modal.addEventListener("keydown", handleTabKey);
|
|
209
|
+
|
|
210
|
+
// Store cleanup function
|
|
211
|
+
(modal as any)._accessibilityCleanup = () => {
|
|
212
|
+
modal.removeEventListener("keydown", handleTabKey);
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function setupMediaHandling(modal: HTMLElement, overlay: HTMLElement): void {
|
|
217
|
+
const videos = modal.querySelectorAll("video") as NodeListOf<HTMLVideoElement>;
|
|
218
|
+
const pausedVideos: HTMLVideoElement[] = [];
|
|
219
|
+
|
|
220
|
+
// Store original overlay.remove to intercept it
|
|
221
|
+
const originalRemove = overlay.remove.bind(overlay);
|
|
222
|
+
|
|
223
|
+
overlay.remove = () => {
|
|
224
|
+
// Pause all videos and track which ones were playing
|
|
225
|
+
videos.forEach((video) => {
|
|
226
|
+
if (!video.paused) {
|
|
227
|
+
video.pause();
|
|
228
|
+
pausedVideos.push(video);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Call original remove
|
|
233
|
+
originalRemove();
|
|
234
|
+
|
|
235
|
+
// Resume videos after a short delay (if they're still in DOM)
|
|
236
|
+
setTimeout(() => {
|
|
237
|
+
pausedVideos.forEach((video) => {
|
|
238
|
+
if (document.contains(video)) {
|
|
239
|
+
video.play().catch(() => {
|
|
240
|
+
// Ignore autoplay policy errors
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}, 100);
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function createModalElements(payload: ModalPayload) {
|
|
249
|
+
console.log("[DAP] Creating modal elements with payload:", payload);
|
|
250
|
+
const overlay = document.createElement("div");
|
|
251
|
+
overlay.className = "dap-modal-overlay";
|
|
252
|
+
|
|
253
|
+
const modal = document.createElement("div");
|
|
254
|
+
modal.className = "dap-modal";
|
|
255
|
+
|
|
256
|
+
// Apply size class if specified
|
|
257
|
+
if (payload.size) {
|
|
258
|
+
modal.classList.add(`dap-modal-${payload.size}`);
|
|
259
|
+
} else {
|
|
260
|
+
modal.classList.add("dap-modal-medium"); // Default to medium size
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Header - always include header for consistent layout
|
|
264
|
+
const header = document.createElement("div");
|
|
265
|
+
header.className = "dap-modal-header";
|
|
266
|
+
|
|
267
|
+
// Title (if provided)
|
|
268
|
+
if (payload.title) {
|
|
269
|
+
const title = document.createElement("h2");
|
|
270
|
+
title.className = "dap-modal-title";
|
|
271
|
+
title.id = "modal-title";
|
|
272
|
+
title.textContent = payload.title;
|
|
273
|
+
header.appendChild(title);
|
|
274
|
+
} else {
|
|
275
|
+
// Add empty space to maintain header layout
|
|
276
|
+
const title = document.createElement("h2");
|
|
277
|
+
title.className = "dap-modal-title";
|
|
278
|
+
title.id = "modal-title";
|
|
279
|
+
title.style.visibility = "hidden";
|
|
280
|
+
title.innerHTML = " ";
|
|
281
|
+
header.appendChild(title);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Close button - always present
|
|
285
|
+
const closeBtn = document.createElement("button");
|
|
286
|
+
closeBtn.className = "dap-modal-close";
|
|
287
|
+
closeBtn.setAttribute("aria-label", "Close modal");
|
|
288
|
+
closeBtn.innerHTML = "×";
|
|
289
|
+
header.appendChild(closeBtn);
|
|
290
|
+
|
|
291
|
+
// Body
|
|
292
|
+
const body = document.createElement("div");
|
|
293
|
+
body.className = "dap-modal-body";
|
|
294
|
+
|
|
295
|
+
console.debug("[DAP] Processing modal body:", payload.body);
|
|
296
|
+
console.debug("[DAP] Body type:", typeof payload.body);
|
|
297
|
+
console.debug("[DAP] Is body array:", Array.isArray(payload.body));
|
|
298
|
+
|
|
299
|
+
if (payload.body && Array.isArray(payload.body)) {
|
|
300
|
+
payload.body.forEach((content, index) => {
|
|
301
|
+
console.debug(`[DAP] Processing body content ${index}:`, content);
|
|
302
|
+
const contentEl = renderModalContent(content);
|
|
303
|
+
if (contentEl) body.appendChild(contentEl);
|
|
304
|
+
});
|
|
305
|
+
} else if (payload.body) {
|
|
306
|
+
console.warn("[DAP] Body is not an array:", payload.body);
|
|
307
|
+
// Handle non-array body content
|
|
308
|
+
const textEl = document.createElement("div");
|
|
309
|
+
textEl.className = "dap-content-text";
|
|
310
|
+
textEl.textContent = String(payload.body);
|
|
311
|
+
body.appendChild(textEl);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Footer - always include for consistent layout
|
|
315
|
+
const footer = document.createElement("div");
|
|
316
|
+
footer.className = "dap-modal-footer";
|
|
317
|
+
|
|
318
|
+
if (payload.footerText) {
|
|
319
|
+
const footerText = document.createElement("p");
|
|
320
|
+
footerText.className = "dap-footer-text";
|
|
321
|
+
footerText.innerHTML = sanitizeHtml(payload.footerText);
|
|
322
|
+
footer.appendChild(footerText);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Assemble modal - always include header and footer
|
|
326
|
+
modal.appendChild(header);
|
|
327
|
+
modal.appendChild(body);
|
|
328
|
+
modal.appendChild(footer);
|
|
329
|
+
overlay.appendChild(modal);
|
|
330
|
+
|
|
331
|
+
return { overlay, modal, header, body, footer };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function renderModalContent(content: ModalContent): HTMLElement | null {
|
|
335
|
+
console.log("[DAP] Rendering modal content with kind:", content.kind);
|
|
336
|
+
switch (content.kind) {
|
|
337
|
+
case "text":
|
|
338
|
+
const textEl = document.createElement("div");
|
|
339
|
+
textEl.className = "dap-content-text";
|
|
340
|
+
textEl.innerHTML = sanitizeHtml(content.html);
|
|
341
|
+
return textEl;
|
|
342
|
+
|
|
343
|
+
case "link":
|
|
344
|
+
const linkEl = document.createElement("a");
|
|
345
|
+
linkEl.className = "dap-content-link";
|
|
346
|
+
linkEl.href = content.href;
|
|
347
|
+
linkEl.textContent = content.label || content.href;
|
|
348
|
+
linkEl.target = "_blank";
|
|
349
|
+
linkEl.rel = "noopener noreferrer";
|
|
350
|
+
return linkEl;
|
|
351
|
+
|
|
352
|
+
case "image":
|
|
353
|
+
const imgEl = document.createElement("img");
|
|
354
|
+
imgEl.className = "dap-content-image";
|
|
355
|
+
imgEl.src = content.url;
|
|
356
|
+
imgEl.alt = content.alt || "";
|
|
357
|
+
return imgEl;
|
|
358
|
+
|
|
359
|
+
case "video":
|
|
360
|
+
if (content.sources && content.sources.length > 0) {
|
|
361
|
+
const videoEl = document.createElement("video");
|
|
362
|
+
videoEl.className = "dap-content-video";
|
|
363
|
+
videoEl.controls = true;
|
|
364
|
+
|
|
365
|
+
content.sources.forEach(source => {
|
|
366
|
+
const sourceEl = document.createElement("source");
|
|
367
|
+
sourceEl.src = source.src;
|
|
368
|
+
if (source.type) sourceEl.type = source.type;
|
|
369
|
+
videoEl.appendChild(sourceEl);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
return videoEl;
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
|
|
376
|
+
case "youtube":
|
|
377
|
+
const iframeEl = document.createElement("iframe");
|
|
378
|
+
iframeEl.className = "dap-content-youtube";
|
|
379
|
+
iframeEl.src = content.href;
|
|
380
|
+
iframeEl.setAttribute("frameborder", "0");
|
|
381
|
+
iframeEl.setAttribute("allowfullscreen", "true");
|
|
382
|
+
return iframeEl;
|
|
383
|
+
|
|
384
|
+
case "kb":
|
|
385
|
+
// Knowledge base rendering with in-modal viewing
|
|
386
|
+
console.debug("[DAP] Rendering KB content:", content);
|
|
387
|
+
return renderKnowledgeBase(content);
|
|
388
|
+
|
|
389
|
+
case "kb-item-viewer":
|
|
390
|
+
// Knowledge base item viewer
|
|
391
|
+
console.debug("[DAP] Rendering KB item viewer:", content);
|
|
392
|
+
return renderKBItemViewer(content);
|
|
393
|
+
|
|
394
|
+
case "article":
|
|
395
|
+
// Article content rendering with inline viewing
|
|
396
|
+
console.debug("[DAP] Rendering Article content:", content);
|
|
397
|
+
console.debug("[DAP] Detected MIME type:", content.mime);
|
|
398
|
+
return createArticleViewer(content);
|
|
399
|
+
|
|
400
|
+
default:
|
|
401
|
+
console.warn("[DAP] Unknown content kind:", (content as any)?.kind);
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function renderKnowledgeBase(content: any): HTMLElement {
|
|
407
|
+
const kbEl = document.createElement("div");
|
|
408
|
+
kbEl.className = "dap-content-kb";
|
|
409
|
+
|
|
410
|
+
// Initialize KB state when rendering list for the first time
|
|
411
|
+
if (!kbState || kbState.view === "item") {
|
|
412
|
+
console.debug("[DAP] Initializing KB state with items:", content.items);
|
|
413
|
+
kbState = {
|
|
414
|
+
view: "list",
|
|
415
|
+
items: content.items || [],
|
|
416
|
+
selectedItem: null,
|
|
417
|
+
title: content.title || "Knowledge Base",
|
|
418
|
+
modalBodyRef: null
|
|
419
|
+
};
|
|
420
|
+
console.debug("[DAP] KB state initialized, items count:", kbState.items.length);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (content.title) {
|
|
424
|
+
const title = document.createElement("h3");
|
|
425
|
+
title.textContent = content.title;
|
|
426
|
+
kbEl.appendChild(title);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (content.items && Array.isArray(content.items)) {
|
|
430
|
+
console.debug("[DAP] Processing KB items:", content.items);
|
|
431
|
+
|
|
432
|
+
content.items.forEach((item: any, index: number) => {
|
|
433
|
+
console.debug(`[DAP] Processing KB item ${index}:`, item);
|
|
434
|
+
|
|
435
|
+
const itemEl = document.createElement("div");
|
|
436
|
+
itemEl.className = "dap-kb-item";
|
|
437
|
+
|
|
438
|
+
// Handle KBItem structure from flows.ts
|
|
439
|
+
let itemUrl = "";
|
|
440
|
+
let itemTitle = "";
|
|
441
|
+
let itemDescription = "";
|
|
442
|
+
let itemType = "";
|
|
443
|
+
|
|
444
|
+
if (typeof item === "string") {
|
|
445
|
+
// If item is just a string, treat it as URL
|
|
446
|
+
itemUrl = item;
|
|
447
|
+
itemTitle = item;
|
|
448
|
+
itemType = "link";
|
|
449
|
+
} else if (item && typeof item === "object") {
|
|
450
|
+
// Handle KBItem structure with proper property access
|
|
451
|
+
const kbItem = item as any;
|
|
452
|
+
itemUrl = kbItem.url || "";
|
|
453
|
+
itemTitle = kbItem.title || "";
|
|
454
|
+
itemDescription = kbItem.description || "";
|
|
455
|
+
itemType = kbItem.itemType || detectContentType(itemUrl, kbItem.fileName);
|
|
456
|
+
|
|
457
|
+
console.debug(`[DAP] Extracted: url=${itemUrl}, title=${itemTitle}, description=${itemDescription}, type=${itemType}`);
|
|
458
|
+
} else {
|
|
459
|
+
console.warn("[DAP] Invalid KB item structure:", item);
|
|
460
|
+
return; // Skip this item
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!itemUrl || !itemTitle) {
|
|
464
|
+
console.warn("[DAP] KB item missing required fields (url or title), skipping:", item);
|
|
465
|
+
return; // Skip items without required fields
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Create clickable item (NO external navigation)
|
|
469
|
+
const button = document.createElement("button");
|
|
470
|
+
button.className = "dap-kb-item-button dap-modal-button primary";
|
|
471
|
+
button.textContent = itemTitle;
|
|
472
|
+
button.title = itemDescription || itemTitle;
|
|
473
|
+
|
|
474
|
+
// Add content type icon
|
|
475
|
+
const icon = document.createElement("span");
|
|
476
|
+
icon.className = `dap-kb-icon dap-kb-icon-${itemType}`;
|
|
477
|
+
button.insertBefore(icon, button.firstChild);
|
|
478
|
+
|
|
479
|
+
// In-modal viewing click handler
|
|
480
|
+
button.addEventListener("click", (e) => {
|
|
481
|
+
e.preventDefault();
|
|
482
|
+
e.stopPropagation();
|
|
483
|
+
openKBItemInModal(item, content.title || "Knowledge Base");
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
itemEl.appendChild(button);
|
|
487
|
+
|
|
488
|
+
// Add description if available
|
|
489
|
+
if (itemDescription) {
|
|
490
|
+
const desc = document.createElement("p");
|
|
491
|
+
desc.className = "dap-kb-description";
|
|
492
|
+
desc.textContent = itemDescription;
|
|
493
|
+
itemEl.appendChild(desc);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
kbEl.appendChild(itemEl);
|
|
497
|
+
});
|
|
498
|
+
} else {
|
|
499
|
+
console.warn("[DAP] KB content has no items or items is not an array:", content);
|
|
500
|
+
|
|
501
|
+
// Add fallback message
|
|
502
|
+
const noItemsMsg = document.createElement("p");
|
|
503
|
+
noItemsMsg.className = "dap-kb-no-items";
|
|
504
|
+
noItemsMsg.textContent = "No knowledge base items available.";
|
|
505
|
+
kbEl.appendChild(noItemsMsg);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return kbEl;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function renderKBItemViewer(content: any): HTMLElement {
|
|
512
|
+
const viewerEl = document.createElement("div");
|
|
513
|
+
viewerEl.className = "dap-kb-item-viewer";
|
|
514
|
+
|
|
515
|
+
// Back navigation header
|
|
516
|
+
const headerEl = document.createElement("div");
|
|
517
|
+
headerEl.className = "dap-kb-viewer-header";
|
|
518
|
+
|
|
519
|
+
const backBtn = document.createElement("button");
|
|
520
|
+
backBtn.className = "dap-kb-back-button dap-modal-button outline";
|
|
521
|
+
backBtn.innerHTML = "← Back to " + (content.kbTitle || "Knowledge Base");
|
|
522
|
+
backBtn.addEventListener("click", () => {
|
|
523
|
+
goBackToKBList();
|
|
524
|
+
});
|
|
525
|
+
headerEl.appendChild(backBtn);
|
|
526
|
+
|
|
527
|
+
// File type badge
|
|
528
|
+
const itemType = content.item.itemType || detectContentType(content.item.url, content.item.fileName);
|
|
529
|
+
const badge = createFileTypeBadge(itemType, content.item.fileName);
|
|
530
|
+
headerEl.appendChild(badge);
|
|
531
|
+
|
|
532
|
+
// Item title
|
|
533
|
+
const title = document.createElement("h3");
|
|
534
|
+
title.className = "dap-kb-item-title";
|
|
535
|
+
title.textContent = content.item.title || "Content";
|
|
536
|
+
headerEl.appendChild(title);
|
|
537
|
+
|
|
538
|
+
// File metadata
|
|
539
|
+
if (content.item.fileName) {
|
|
540
|
+
const fileInfo = document.createElement("p");
|
|
541
|
+
fileInfo.className = "dap-file-metadata";
|
|
542
|
+
fileInfo.innerHTML = `<strong>File:</strong> ${content.item.fileName}`;
|
|
543
|
+
headerEl.appendChild(fileInfo);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
viewerEl.appendChild(headerEl);
|
|
547
|
+
|
|
548
|
+
// Content viewer
|
|
549
|
+
const contentEl = renderKBItemContent(content.item);
|
|
550
|
+
if (contentEl) {
|
|
551
|
+
viewerEl.appendChild(contentEl);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return viewerEl;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function renderKBItemContent(item: any): HTMLElement | null {
|
|
558
|
+
const itemType = item.itemType || detectContentType(item.url, item.fileName);
|
|
559
|
+
console.debug("[DAP] Rendering KB item content, type:", itemType, "url:", item.url);
|
|
560
|
+
|
|
561
|
+
switch (itemType) {
|
|
562
|
+
case "video":
|
|
563
|
+
return createVideoViewer(item.url);
|
|
564
|
+
|
|
565
|
+
case "image":
|
|
566
|
+
return createImageViewer(item.url, item.title);
|
|
567
|
+
|
|
568
|
+
case "pdf":
|
|
569
|
+
return createPDFViewer(item.url, item.fileName);
|
|
570
|
+
|
|
571
|
+
case "article":
|
|
572
|
+
return createArticleViewer(item);
|
|
573
|
+
|
|
574
|
+
case "doc":
|
|
575
|
+
case "docx":
|
|
576
|
+
return createDocumentViewer(item.url, item.fileName, itemType);
|
|
577
|
+
|
|
578
|
+
case "youtube":
|
|
579
|
+
return createYouTubeViewer(item.url);
|
|
580
|
+
|
|
581
|
+
case "link":
|
|
582
|
+
default:
|
|
583
|
+
return createLinkViewer(item.url, item.title, item.description);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function createVideoViewer(url: string): HTMLElement {
|
|
588
|
+
const videoEl = document.createElement("video");
|
|
589
|
+
videoEl.className = "dap-kb-video";
|
|
590
|
+
videoEl.controls = true;
|
|
591
|
+
videoEl.preload = "metadata";
|
|
592
|
+
videoEl.style.width = "100%";
|
|
593
|
+
videoEl.style.maxHeight = "400px";
|
|
594
|
+
|
|
595
|
+
const source = document.createElement("source");
|
|
596
|
+
source.src = url;
|
|
597
|
+
videoEl.appendChild(source);
|
|
598
|
+
|
|
599
|
+
return videoEl;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function createImageViewer(url: string, alt?: string): HTMLElement {
|
|
603
|
+
const imgEl = document.createElement("img");
|
|
604
|
+
imgEl.className = "dap-kb-image";
|
|
605
|
+
imgEl.src = url;
|
|
606
|
+
imgEl.alt = alt || "";
|
|
607
|
+
imgEl.style.width = "100%";
|
|
608
|
+
imgEl.style.maxHeight = "500px";
|
|
609
|
+
imgEl.style.objectFit = "contain";
|
|
610
|
+
|
|
611
|
+
return imgEl;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function createPDFViewer(url: string, fileName?: string): HTMLElement {
|
|
615
|
+
const container = document.createElement("div");
|
|
616
|
+
container.className = "dap-kb-pdf-container";
|
|
617
|
+
|
|
618
|
+
// Try embedded PDF viewer first
|
|
619
|
+
const iframe = document.createElement("iframe");
|
|
620
|
+
iframe.className = "dap-kb-pdf-iframe";
|
|
621
|
+
iframe.src = url;
|
|
622
|
+
iframe.style.width = "100%";
|
|
623
|
+
iframe.style.height = "500px";
|
|
624
|
+
iframe.style.border = "1px solid #ddd";
|
|
625
|
+
|
|
626
|
+
// Fallback controls
|
|
627
|
+
const fallback = document.createElement("div");
|
|
628
|
+
fallback.className = "dap-kb-pdf-fallback";
|
|
629
|
+
fallback.innerHTML = `
|
|
630
|
+
<p>PDF preview not available in this browser.</p>
|
|
631
|
+
<button class="dap-kb-download-btn dap-modal-button secondary" onclick="window.open('${url}', '_blank')">
|
|
632
|
+
Open PDF in New Tab
|
|
633
|
+
</button>
|
|
634
|
+
`;
|
|
635
|
+
|
|
636
|
+
// Error handling
|
|
637
|
+
iframe.addEventListener("error", () => {
|
|
638
|
+
iframe.style.display = "none";
|
|
639
|
+
fallback.style.display = "block";
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
container.appendChild(iframe);
|
|
643
|
+
container.appendChild(fallback);
|
|
644
|
+
|
|
645
|
+
return container;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Centralized Article Viewer Resolver - determines appropriate viewer based on MIME type and file extension
|
|
649
|
+
function resolveArticleViewer(articleContent: any): { viewer: string; mimeType: string | null } {
|
|
650
|
+
console.debug("[DAP] Resolving Article viewer for:", articleContent);
|
|
651
|
+
|
|
652
|
+
const url = articleContent.url || articleContent.presignedUrl || "";
|
|
653
|
+
const mimeType = articleContent.mime || articleContent.mimeType || null;
|
|
654
|
+
const fileName = articleContent.fileName || "";
|
|
655
|
+
|
|
656
|
+
console.debug("[DAP] Detected MIME type:", mimeType || "none");
|
|
657
|
+
console.debug("[DAP] File URL:", url);
|
|
658
|
+
console.debug("[DAP] File name:", fileName);
|
|
659
|
+
|
|
660
|
+
// Check MIME type first (preferred)
|
|
661
|
+
if (mimeType) {
|
|
662
|
+
if (mimeType === "application/pdf") {
|
|
663
|
+
console.debug("[DAP] Selected viewer: pdf");
|
|
664
|
+
return { viewer: "pdf", mimeType };
|
|
665
|
+
}
|
|
666
|
+
if (mimeType.includes("word") || mimeType.includes("msword") ||
|
|
667
|
+
mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document") {
|
|
668
|
+
console.debug("[DAP] Selected viewer: document");
|
|
669
|
+
return { viewer: "document", mimeType };
|
|
670
|
+
}
|
|
671
|
+
if (mimeType.includes("presentation") || mimeType.includes("powerpoint") ||
|
|
672
|
+
mimeType === "application/vnd.openxmlformats-officedocument.presentationml.presentation") {
|
|
673
|
+
console.debug("[DAP] Selected viewer: presentation");
|
|
674
|
+
return { viewer: "presentation", mimeType };
|
|
675
|
+
}
|
|
676
|
+
if (mimeType === "text/html" || mimeType.includes("text/")) {
|
|
677
|
+
console.debug("[DAP] Selected viewer: web");
|
|
678
|
+
return { viewer: "web", mimeType };
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Fallback to file extension detection
|
|
683
|
+
const urlLower = url.toLowerCase();
|
|
684
|
+
const fileNameLower = fileName.toLowerCase();
|
|
685
|
+
|
|
686
|
+
if (urlLower.includes(".pdf") || fileNameLower.endsWith(".pdf")) {
|
|
687
|
+
console.debug("[DAP] Selected viewer: pdf (by extension)");
|
|
688
|
+
return { viewer: "pdf", mimeType: "application/pdf" };
|
|
689
|
+
}
|
|
690
|
+
if (urlLower.match(/\.(doc|docx)/) || fileNameLower.match(/\.(doc|docx)$/)) {
|
|
691
|
+
console.debug("[DAP] Selected viewer: document (by extension)");
|
|
692
|
+
return { viewer: "document", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" };
|
|
693
|
+
}
|
|
694
|
+
if (urlLower.match(/\.(ppt|pptx)/) || fileNameLower.match(/\.(ppt|pptx)$/)) {
|
|
695
|
+
console.debug("[DAP] Selected viewer: presentation (by extension)");
|
|
696
|
+
return { viewer: "presentation", mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation" };
|
|
697
|
+
}
|
|
698
|
+
if (urlLower.match(/\.(html?|htm)/) || fileNameLower.match(/\.(html?|htm)$/)) {
|
|
699
|
+
console.debug("[DAP] Selected viewer: web (by extension)");
|
|
700
|
+
return { viewer: "web", mimeType: "text/html" };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Check URL patterns for web content
|
|
704
|
+
if (url && (url.startsWith('http://') || url.startsWith('https://'))) {
|
|
705
|
+
// If it's a web URL and doesn't have a specific file extension, treat as web content
|
|
706
|
+
if (!urlLower.match(/\.(pdf|doc|docx|ppt|pptx|xls|xlsx|zip|rar)$/)) {
|
|
707
|
+
console.debug("[DAP] Selected viewer: web (by URL pattern)");
|
|
708
|
+
return { viewer: "web", mimeType: "text/html" };
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
console.debug("[DAP] Selected viewer: fallback");
|
|
713
|
+
return { viewer: "fallback", mimeType: mimeType };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Create Article Viewer - handles all document-based assets with inline preview and fallback
|
|
717
|
+
function createArticleViewer(articleContent: any): HTMLElement {
|
|
718
|
+
console.debug("[DAP] Rendering Article");
|
|
719
|
+
|
|
720
|
+
const container = document.createElement("div");
|
|
721
|
+
container.className = "dap-kb-article-viewer";
|
|
722
|
+
|
|
723
|
+
const url = articleContent.url || articleContent.presignedUrl || "";
|
|
724
|
+
const fileName = articleContent.fileName || "Document";
|
|
725
|
+
const title = articleContent.title || fileName;
|
|
726
|
+
const description = articleContent.description || "";
|
|
727
|
+
const content = articleContent.content || "";
|
|
728
|
+
|
|
729
|
+
// Add title
|
|
730
|
+
const titleEl = document.createElement("h4");
|
|
731
|
+
titleEl.className = "dap-article-title";
|
|
732
|
+
titleEl.textContent = title;
|
|
733
|
+
container.appendChild(titleEl);
|
|
734
|
+
|
|
735
|
+
// Add description if available
|
|
736
|
+
if (description) {
|
|
737
|
+
const descEl = document.createElement("p");
|
|
738
|
+
descEl.className = "dap-article-description";
|
|
739
|
+
descEl.textContent = description;
|
|
740
|
+
container.appendChild(descEl);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Handle different content scenarios
|
|
744
|
+
if (content && content.trim()) {
|
|
745
|
+
// Rich HTML content provided directly
|
|
746
|
+
console.debug("[DAP] Rendering direct HTML content");
|
|
747
|
+
const contentEl = document.createElement("div");
|
|
748
|
+
contentEl.className = "dap-article-content";
|
|
749
|
+
contentEl.innerHTML = content;
|
|
750
|
+
container.appendChild(contentEl);
|
|
751
|
+
|
|
752
|
+
// Add action buttons if URL is also provided
|
|
753
|
+
if (url) {
|
|
754
|
+
const actions = createDocumentActions(url, fileName);
|
|
755
|
+
container.appendChild(actions);
|
|
756
|
+
}
|
|
757
|
+
} else if (url) {
|
|
758
|
+
// URL-based content - create loading state first
|
|
759
|
+
const loadingEl = document.createElement("div");
|
|
760
|
+
loadingEl.className = "dap-article-loading";
|
|
761
|
+
loadingEl.innerHTML = `
|
|
762
|
+
<div class="dap-loading-spinner"></div>
|
|
763
|
+
<p>Loading article content...</p>
|
|
764
|
+
`;
|
|
765
|
+
container.appendChild(loadingEl);
|
|
766
|
+
|
|
767
|
+
// Resolve viewer type
|
|
768
|
+
const { viewer, mimeType } = resolveArticleViewer(articleContent);
|
|
769
|
+
console.debug("[DAP] Selected viewer type:", viewer, "for URL:", url);
|
|
770
|
+
|
|
771
|
+
// Remove loading state after a brief delay to show it
|
|
772
|
+
setTimeout(() => {
|
|
773
|
+
loadingEl.remove();
|
|
774
|
+
|
|
775
|
+
// Create appropriate viewer
|
|
776
|
+
switch (viewer) {
|
|
777
|
+
case "pdf":
|
|
778
|
+
const pdfViewer = createInlinePDFViewer(url, fileName);
|
|
779
|
+
container.appendChild(pdfViewer);
|
|
780
|
+
break;
|
|
781
|
+
|
|
782
|
+
case "document":
|
|
783
|
+
const docViewer = createInlineDocumentViewer(url, fileName, mimeType);
|
|
784
|
+
container.appendChild(docViewer);
|
|
785
|
+
break;
|
|
786
|
+
|
|
787
|
+
case "presentation":
|
|
788
|
+
const pptViewer = createInlinePresentationViewer(url, fileName, mimeType);
|
|
789
|
+
container.appendChild(pptViewer);
|
|
790
|
+
break;
|
|
791
|
+
|
|
792
|
+
case "web":
|
|
793
|
+
const webViewer = createWebContentViewer(url, title);
|
|
794
|
+
container.appendChild(webViewer);
|
|
795
|
+
break;
|
|
796
|
+
|
|
797
|
+
case "fallback":
|
|
798
|
+
default:
|
|
799
|
+
console.debug("[DAP] Fallback activated for URL:", url);
|
|
800
|
+
const fallbackViewer = createEnhancedFallbackViewer(articleContent, "This document cannot be previewed inline.");
|
|
801
|
+
container.appendChild(fallbackViewer);
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
}, 300);
|
|
805
|
+
} else {
|
|
806
|
+
// No content or URL provided
|
|
807
|
+
console.error("[DAP] No content or URL provided for Article");
|
|
808
|
+
const errorViewer = createEnhancedFallbackViewer(articleContent, "No article content available to display.");
|
|
809
|
+
container.appendChild(errorViewer);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return container;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Inline PDF Viewer - uses iframe for PDF preview
|
|
816
|
+
function createInlinePDFViewer(url: string, fileName: string): HTMLElement {
|
|
817
|
+
const container = document.createElement("div");
|
|
818
|
+
container.className = "dap-pdf-viewer-container";
|
|
819
|
+
|
|
820
|
+
// Try iframe first
|
|
821
|
+
const iframe = document.createElement("iframe");
|
|
822
|
+
iframe.className = "dap-pdf-iframe";
|
|
823
|
+
iframe.src = url;
|
|
824
|
+
iframe.style.width = "100%";
|
|
825
|
+
iframe.style.height = "500px";
|
|
826
|
+
iframe.style.border = "1px solid #ddd";
|
|
827
|
+
iframe.setAttribute("frameborder", "0");
|
|
828
|
+
|
|
829
|
+
// Add fallback handling
|
|
830
|
+
iframe.onerror = () => {
|
|
831
|
+
console.warn("[DAP] PDF iframe failed, showing fallback");
|
|
832
|
+
container.innerHTML = "";
|
|
833
|
+
const fallback = createFallbackViewer({ url, fileName, title: fileName },
|
|
834
|
+
"PDF preview failed. Please download or open in a new tab.");
|
|
835
|
+
container.appendChild(fallback);
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
container.appendChild(iframe);
|
|
839
|
+
|
|
840
|
+
// Add action buttons below PDF
|
|
841
|
+
const actions = createDocumentActions(url, fileName);
|
|
842
|
+
container.appendChild(actions);
|
|
843
|
+
|
|
844
|
+
return container;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Inline Document Viewer - attempts preview with Office Online, falls back gracefully
|
|
848
|
+
function createInlineDocumentViewer(url: string, fileName: string, mimeType?: string | null): HTMLElement {
|
|
849
|
+
const container = document.createElement("div");
|
|
850
|
+
container.className = "dap-document-viewer-container";
|
|
851
|
+
|
|
852
|
+
// Try Office Online viewer for DOC/DOCX
|
|
853
|
+
if (mimeType && (mimeType.includes("word") || mimeType.includes("msword"))) {
|
|
854
|
+
const iframe = document.createElement("iframe");
|
|
855
|
+
iframe.className = "dap-document-iframe";
|
|
856
|
+
iframe.src = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`;
|
|
857
|
+
iframe.style.width = "100%";
|
|
858
|
+
iframe.style.height = "500px";
|
|
859
|
+
iframe.style.border = "1px solid #ddd";
|
|
860
|
+
iframe.setAttribute("frameborder", "0");
|
|
861
|
+
|
|
862
|
+
// Add fallback handling
|
|
863
|
+
iframe.onerror = () => {
|
|
864
|
+
console.warn("[DAP] Office Online viewer failed, showing fallback");
|
|
865
|
+
container.innerHTML = "";
|
|
866
|
+
const fallback = createFallbackViewer({ url, fileName, title: fileName },
|
|
867
|
+
"Document preview is not available. Please download or open in a new tab.");
|
|
868
|
+
container.appendChild(fallback);
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
container.appendChild(iframe);
|
|
872
|
+
} else {
|
|
873
|
+
// Direct fallback for unsupported document types
|
|
874
|
+
const fallback = createFallbackViewer({ url, fileName, title: fileName },
|
|
875
|
+
"Document preview is not supported for this file type.");
|
|
876
|
+
container.appendChild(fallback);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Add action buttons
|
|
880
|
+
const actions = createDocumentActions(url, fileName);
|
|
881
|
+
container.appendChild(actions);
|
|
882
|
+
|
|
883
|
+
return container;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Inline Presentation Viewer - attempts preview for PPT/PPTX
|
|
887
|
+
function createInlinePresentationViewer(url: string, fileName: string, mimeType?: string | null): HTMLElement {
|
|
888
|
+
const container = document.createElement("div");
|
|
889
|
+
container.className = "dap-presentation-viewer-container";
|
|
890
|
+
|
|
891
|
+
// Try Office Online viewer for PPT/PPTX
|
|
892
|
+
if (mimeType && mimeType.includes("presentation")) {
|
|
893
|
+
const iframe = document.createElement("iframe");
|
|
894
|
+
iframe.className = "dap-presentation-iframe";
|
|
895
|
+
iframe.src = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`;
|
|
896
|
+
iframe.style.width = "100%";
|
|
897
|
+
iframe.style.height = "500px";
|
|
898
|
+
iframe.style.border = "1px solid #ddd";
|
|
899
|
+
iframe.setAttribute("frameborder", "0");
|
|
900
|
+
|
|
901
|
+
// Add fallback handling
|
|
902
|
+
iframe.onerror = () => {
|
|
903
|
+
console.warn("[DAP] Office Online presentation viewer failed, showing fallback");
|
|
904
|
+
container.innerHTML = "";
|
|
905
|
+
const fallback = createFallbackViewer({ url, fileName, title: fileName },
|
|
906
|
+
"Presentation preview is not available. Please download or open in a new tab.");
|
|
907
|
+
container.appendChild(fallback);
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
container.appendChild(iframe);
|
|
911
|
+
} else {
|
|
912
|
+
// Direct fallback for unsupported presentation types
|
|
913
|
+
const fallback = createFallbackViewer({ url, fileName, title: fileName },
|
|
914
|
+
"Presentation preview is not supported for this file type.");
|
|
915
|
+
container.appendChild(fallback);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Add action buttons
|
|
919
|
+
const actions = createDocumentActions(url, fileName);
|
|
920
|
+
container.appendChild(actions);
|
|
921
|
+
|
|
922
|
+
return container;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Fallback Viewer - shows clear message and action buttons when inline preview is not possible
|
|
926
|
+
function createFallbackViewer(articleContent: any, message: string): HTMLElement {
|
|
927
|
+
const container = document.createElement("div");
|
|
928
|
+
container.className = "dap-fallback-viewer";
|
|
929
|
+
|
|
930
|
+
const url = articleContent.url || articleContent.presignedUrl || "";
|
|
931
|
+
const fileName = articleContent.fileName || "Document";
|
|
932
|
+
|
|
933
|
+
// Message
|
|
934
|
+
const messageEl = document.createElement("div");
|
|
935
|
+
messageEl.className = "dap-fallback-message";
|
|
936
|
+
messageEl.innerHTML = `
|
|
937
|
+
<p><strong>${message}</strong></p>
|
|
938
|
+
<p>File: ${fileName}</p>
|
|
939
|
+
`;
|
|
940
|
+
container.appendChild(messageEl);
|
|
941
|
+
|
|
942
|
+
// Action buttons
|
|
943
|
+
const actions = createDocumentActions(url, fileName);
|
|
944
|
+
container.appendChild(actions);
|
|
945
|
+
|
|
946
|
+
return container;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Enhanced Fallback Viewer - improved version with better UX and error handling
|
|
950
|
+
function createEnhancedFallbackViewer(articleContent: any, message: string): HTMLElement {
|
|
951
|
+
const container = document.createElement("div");
|
|
952
|
+
container.className = "dap-enhanced-fallback-viewer";
|
|
953
|
+
|
|
954
|
+
const url = articleContent.url || articleContent.presignedUrl || "";
|
|
955
|
+
const fileName = articleContent.fileName || "Document";
|
|
956
|
+
const title = articleContent.title || fileName;
|
|
957
|
+
const fileExtension = fileName.split('.').pop()?.toUpperCase() || "";
|
|
958
|
+
|
|
959
|
+
// Icon based on file type
|
|
960
|
+
const iconEl = document.createElement("div");
|
|
961
|
+
iconEl.className = "dap-fallback-icon";
|
|
962
|
+
if (fileExtension.includes('PDF')) {
|
|
963
|
+
iconEl.innerHTML = "📄";
|
|
964
|
+
} else if (['DOC', 'DOCX'].includes(fileExtension)) {
|
|
965
|
+
iconEl.innerHTML = "📝";
|
|
966
|
+
} else if (['PPT', 'PPTX'].includes(fileExtension)) {
|
|
967
|
+
iconEl.innerHTML = "📊";
|
|
968
|
+
} else if (['XLS', 'XLSX'].includes(fileExtension)) {
|
|
969
|
+
iconEl.innerHTML = "📈";
|
|
970
|
+
} else {
|
|
971
|
+
iconEl.innerHTML = "📰";
|
|
972
|
+
}
|
|
973
|
+
container.appendChild(iconEl);
|
|
974
|
+
|
|
975
|
+
// Message with better formatting
|
|
976
|
+
const messageEl = document.createElement("div");
|
|
977
|
+
messageEl.className = "dap-enhanced-fallback-message";
|
|
978
|
+
messageEl.innerHTML = `
|
|
979
|
+
<h4>${title}</h4>
|
|
980
|
+
<p class="dap-fallback-primary">${message}</p>
|
|
981
|
+
${fileName !== title ? `<p class="dap-fallback-filename">File: ${fileName}</p>` : ''}
|
|
982
|
+
${fileExtension ? `<p class="dap-fallback-type">Type: ${fileExtension} Document</p>` : ''}
|
|
983
|
+
`;
|
|
984
|
+
container.appendChild(messageEl);
|
|
985
|
+
|
|
986
|
+
// Action buttons with better styling
|
|
987
|
+
if (url) {
|
|
988
|
+
const actions = createEnhancedDocumentActions(url, fileName);
|
|
989
|
+
container.appendChild(actions);
|
|
990
|
+
} else {
|
|
991
|
+
const noUrlMessage = document.createElement("p");
|
|
992
|
+
noUrlMessage.className = "dap-fallback-no-url";
|
|
993
|
+
noUrlMessage.textContent = "No document link available.";
|
|
994
|
+
container.appendChild(noUrlMessage);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return container;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Web Content Viewer - for displaying web pages in an iframe
|
|
1001
|
+
function createWebContentViewer(url: string, title: string): HTMLElement {
|
|
1002
|
+
const container = document.createElement("div");
|
|
1003
|
+
container.className = "dap-web-viewer-container";
|
|
1004
|
+
|
|
1005
|
+
// Create iframe for web content
|
|
1006
|
+
const iframe = document.createElement("iframe");
|
|
1007
|
+
iframe.className = "dap-web-iframe";
|
|
1008
|
+
iframe.src = url;
|
|
1009
|
+
iframe.style.width = "100%";
|
|
1010
|
+
iframe.style.height = "600px";
|
|
1011
|
+
iframe.style.border = "1px solid var(--dap-border)";
|
|
1012
|
+
iframe.style.borderRadius = "4px";
|
|
1013
|
+
iframe.setAttribute("frameborder", "0");
|
|
1014
|
+
iframe.setAttribute("loading", "lazy");
|
|
1015
|
+
|
|
1016
|
+
// Add error handling
|
|
1017
|
+
iframe.onerror = () => {
|
|
1018
|
+
console.warn("[DAP] Web content iframe failed, showing fallback");
|
|
1019
|
+
container.innerHTML = "";
|
|
1020
|
+
const fallback = createEnhancedFallbackViewer({ url, title, fileName: title },
|
|
1021
|
+
"Web content could not be loaded. Please open in a new tab.");
|
|
1022
|
+
container.appendChild(fallback);
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
// Add iframe to container
|
|
1026
|
+
container.appendChild(iframe);
|
|
1027
|
+
|
|
1028
|
+
// Add action buttons
|
|
1029
|
+
const actions = document.createElement("div");
|
|
1030
|
+
actions.className = "dap-web-actions";
|
|
1031
|
+
const openBtn = document.createElement("button");
|
|
1032
|
+
openBtn.className = "dap-action-btn dap-open-btn";
|
|
1033
|
+
openBtn.textContent = "Open in New Tab";
|
|
1034
|
+
openBtn.addEventListener("click", (e) => {
|
|
1035
|
+
e.preventDefault();
|
|
1036
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
1037
|
+
});
|
|
1038
|
+
actions.appendChild(openBtn);
|
|
1039
|
+
container.appendChild(actions);
|
|
1040
|
+
|
|
1041
|
+
return container;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Enhanced Document Actions - improved version with better UX
|
|
1045
|
+
function createEnhancedDocumentActions(url: string, fileName: string): HTMLElement {
|
|
1046
|
+
const actions = document.createElement("div");
|
|
1047
|
+
actions.className = "dap-enhanced-document-actions";
|
|
1048
|
+
|
|
1049
|
+
// Download button with icon
|
|
1050
|
+
const downloadBtn = document.createElement("button");
|
|
1051
|
+
downloadBtn.className = "dap-action-btn dap-download-btn dap-primary-btn";
|
|
1052
|
+
downloadBtn.innerHTML = `
|
|
1053
|
+
<span class="dap-btn-icon">⬇️</span>
|
|
1054
|
+
<span class="dap-btn-text">Download</span>
|
|
1055
|
+
`;
|
|
1056
|
+
downloadBtn.addEventListener("click", (e) => {
|
|
1057
|
+
e.preventDefault();
|
|
1058
|
+
e.stopPropagation();
|
|
1059
|
+
|
|
1060
|
+
// Create temporary download link
|
|
1061
|
+
const link = document.createElement("a");
|
|
1062
|
+
link.href = url;
|
|
1063
|
+
link.download = fileName || "download";
|
|
1064
|
+
link.target = "_blank";
|
|
1065
|
+
document.body.appendChild(link);
|
|
1066
|
+
link.click();
|
|
1067
|
+
document.body.removeChild(link);
|
|
1068
|
+
|
|
1069
|
+
// Show feedback
|
|
1070
|
+
downloadBtn.innerHTML = `<span class="dap-btn-icon">✅</span><span class="dap-btn-text">Downloaded</span>`;
|
|
1071
|
+
setTimeout(() => {
|
|
1072
|
+
downloadBtn.innerHTML = `<span class="dap-btn-icon">⬇️</span><span class="dap-btn-text">Download</span>`;
|
|
1073
|
+
}, 2000);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// Open in new tab button with icon
|
|
1077
|
+
const openBtn = document.createElement("button");
|
|
1078
|
+
openBtn.className = "dap-action-btn dap-open-btn dap-secondary-btn";
|
|
1079
|
+
openBtn.innerHTML = `
|
|
1080
|
+
<span class="dap-btn-icon">🔗</span>
|
|
1081
|
+
<span class="dap-btn-text">Open in New Tab</span>
|
|
1082
|
+
`;
|
|
1083
|
+
openBtn.addEventListener("click", (e) => {
|
|
1084
|
+
e.preventDefault();
|
|
1085
|
+
e.stopPropagation();
|
|
1086
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
actions.appendChild(downloadBtn);
|
|
1090
|
+
actions.appendChild(openBtn);
|
|
1091
|
+
|
|
1092
|
+
return actions;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Document Actions - reusable download and open buttons with proper event listeners
|
|
1096
|
+
function createDocumentActions(url: string, fileName: string): HTMLElement {
|
|
1097
|
+
const actions = document.createElement("div");
|
|
1098
|
+
actions.className = "dap-document-actions dap-modal-buttons";
|
|
1099
|
+
|
|
1100
|
+
// Download button
|
|
1101
|
+
const downloadBtn = document.createElement("button");
|
|
1102
|
+
downloadBtn.className = "dap-action-btn dap-download-btn dap-modal-button primary";
|
|
1103
|
+
downloadBtn.textContent = "Download";
|
|
1104
|
+
downloadBtn.addEventListener("click", (e) => {
|
|
1105
|
+
e.preventDefault();
|
|
1106
|
+
e.stopPropagation();
|
|
1107
|
+
|
|
1108
|
+
// Create temporary download link
|
|
1109
|
+
const link = document.createElement("a");
|
|
1110
|
+
link.href = url;
|
|
1111
|
+
link.download = fileName || "download";
|
|
1112
|
+
link.target = "_blank";
|
|
1113
|
+
document.body.appendChild(link);
|
|
1114
|
+
link.click();
|
|
1115
|
+
document.body.removeChild(link);
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
// Open in new tab button
|
|
1119
|
+
const openBtn = document.createElement("button");
|
|
1120
|
+
openBtn.className = "dap-action-btn dap-open-btn dap-modal-button secondary";
|
|
1121
|
+
openBtn.textContent = "Open in New Tab";
|
|
1122
|
+
openBtn.addEventListener("click", (e) => {
|
|
1123
|
+
e.preventDefault();
|
|
1124
|
+
e.stopPropagation();
|
|
1125
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
actions.appendChild(downloadBtn);
|
|
1129
|
+
actions.appendChild(openBtn);
|
|
1130
|
+
|
|
1131
|
+
return actions;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function createDocumentViewer(url: string, fileName?: string, type?: string): HTMLElement {
|
|
1135
|
+
const container = document.createElement("div");
|
|
1136
|
+
container.className = "dap-kb-document-container";
|
|
1137
|
+
|
|
1138
|
+
// For docs, provide download/open options since inline viewing is limited
|
|
1139
|
+
container.innerHTML = `
|
|
1140
|
+
<div class="dap-kb-document-info">
|
|
1141
|
+
<h4>${fileName || "Document"}</h4>
|
|
1142
|
+
<p>Document type: ${type?.toUpperCase()}</p>
|
|
1143
|
+
<div class="dap-kb-document-actions">
|
|
1144
|
+
<button class="dap-kb-download-btn" onclick="window.open('${url}', '_blank')">
|
|
1145
|
+
Open in New Tab
|
|
1146
|
+
</button>
|
|
1147
|
+
<button class="dap-kb-download-btn" onclick="downloadFile('${url}', '${fileName}')">
|
|
1148
|
+
Download
|
|
1149
|
+
</button>
|
|
1150
|
+
</div>
|
|
1151
|
+
</div>
|
|
1152
|
+
`;
|
|
1153
|
+
|
|
1154
|
+
return container;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function createYouTubeViewer(url: string): HTMLElement {
|
|
1158
|
+
const iframe = document.createElement("iframe");
|
|
1159
|
+
iframe.className = "dap-kb-youtube";
|
|
1160
|
+
iframe.src = url;
|
|
1161
|
+
iframe.style.width = "100%";
|
|
1162
|
+
iframe.style.height = "315px";
|
|
1163
|
+
iframe.setAttribute("frameborder", "0");
|
|
1164
|
+
iframe.setAttribute("allowfullscreen", "true");
|
|
1165
|
+
|
|
1166
|
+
return iframe;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function createLinkViewer(url: string, title?: string, description?: string): HTMLElement {
|
|
1170
|
+
const container = document.createElement("div");
|
|
1171
|
+
container.className = "dap-kb-link-container";
|
|
1172
|
+
|
|
1173
|
+
container.innerHTML = `
|
|
1174
|
+
<div class="dap-kb-link-info">
|
|
1175
|
+
<h4>${title || "External Link"}</h4>
|
|
1176
|
+
${description ? `<p>${description}</p>` : ''}
|
|
1177
|
+
<p><strong>URL:</strong> ${url}</p>
|
|
1178
|
+
<button class="dap-kb-external-btn" onclick="window.open('${url}', '_blank')">
|
|
1179
|
+
Open Link in New Tab
|
|
1180
|
+
</button>
|
|
1181
|
+
</div>
|
|
1182
|
+
`;
|
|
1183
|
+
|
|
1184
|
+
return container;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function detectContentType(url: string, fileName?: string): string {
|
|
1188
|
+
const path = fileName || url;
|
|
1189
|
+
const ext = path.split('.').pop()?.toLowerCase();
|
|
1190
|
+
|
|
1191
|
+
if (ext === 'pdf') return 'pdf';
|
|
1192
|
+
if (['doc', 'docx'].includes(ext || '')) return ext || 'doc';
|
|
1193
|
+
if (['mp4', 'webm', 'ogg', 'avi', 'mov'].includes(ext || '')) return 'video';
|
|
1194
|
+
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext || '')) return 'image';
|
|
1195
|
+
if (url.includes('youtube.com') || url.includes('youtu.be')) return 'youtube';
|
|
1196
|
+
|
|
1197
|
+
return 'link';
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// KB State Management - maintains explicit state across view transitions
|
|
1201
|
+
let kbState: {
|
|
1202
|
+
view: "list" | "item";
|
|
1203
|
+
items: any[];
|
|
1204
|
+
selectedItem: any | null;
|
|
1205
|
+
title: string;
|
|
1206
|
+
modalBodyRef: HTMLElement | null;
|
|
1207
|
+
} | null = null;
|
|
1208
|
+
|
|
1209
|
+
// Helper function to create file type badge
|
|
1210
|
+
function createFileTypeBadge(itemType: string, fileName?: string): HTMLElement {
|
|
1211
|
+
const badge = document.createElement("div");
|
|
1212
|
+
badge.className = `dap-file-type-badge ${itemType}`;
|
|
1213
|
+
|
|
1214
|
+
let icon = "";
|
|
1215
|
+
let label = "";
|
|
1216
|
+
|
|
1217
|
+
switch (itemType) {
|
|
1218
|
+
case "video":
|
|
1219
|
+
icon = "🎥";
|
|
1220
|
+
label = "Video";
|
|
1221
|
+
break;
|
|
1222
|
+
case "image":
|
|
1223
|
+
icon = "🖼️";
|
|
1224
|
+
label = "Image";
|
|
1225
|
+
break;
|
|
1226
|
+
case "pdf":
|
|
1227
|
+
icon = "📄";
|
|
1228
|
+
label = "PDF";
|
|
1229
|
+
break;
|
|
1230
|
+
case "docx":
|
|
1231
|
+
case "doc":
|
|
1232
|
+
icon = "📝";
|
|
1233
|
+
label = "Document";
|
|
1234
|
+
break;
|
|
1235
|
+
case "pptx":
|
|
1236
|
+
case "ppt":
|
|
1237
|
+
icon = "📊";
|
|
1238
|
+
label = "Presentation";
|
|
1239
|
+
break;
|
|
1240
|
+
case "xlsx":
|
|
1241
|
+
case "xls":
|
|
1242
|
+
icon = "📈";
|
|
1243
|
+
label = "Spreadsheet";
|
|
1244
|
+
break;
|
|
1245
|
+
case "article":
|
|
1246
|
+
default:
|
|
1247
|
+
icon = "📰";
|
|
1248
|
+
label = "Article";
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
badge.innerHTML = `<span>${icon}</span> ${label}`;
|
|
1253
|
+
|
|
1254
|
+
return badge;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function openKBItemInModal(item: any, kbTitle?: string) {
|
|
1258
|
+
console.debug("[DAP] Opening KB item in modal:", item);
|
|
1259
|
+
console.debug("[DAP] KB view changed to item");
|
|
1260
|
+
|
|
1261
|
+
// Find the current modal body
|
|
1262
|
+
const modalBody = document.querySelector('.dap-modal-body') as HTMLElement;
|
|
1263
|
+
if (!modalBody) {
|
|
1264
|
+
console.error("[DAP] Could not find modal body for KB item viewing");
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Update KB state for item view
|
|
1269
|
+
if (kbState) {
|
|
1270
|
+
kbState.view = "item";
|
|
1271
|
+
kbState.selectedItem = item;
|
|
1272
|
+
kbState.modalBodyRef = modalBody;
|
|
1273
|
+
|
|
1274
|
+
console.debug("[DAP] KB items count:", kbState.items.length);
|
|
1275
|
+
} else {
|
|
1276
|
+
console.error("[DAP] KB state not initialized when opening item");
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Create KB item viewer content
|
|
1281
|
+
const viewerContent = {
|
|
1282
|
+
kind: "kb-item-viewer",
|
|
1283
|
+
item: item,
|
|
1284
|
+
kbTitle: kbTitle
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
// Clear modal body and add viewer
|
|
1288
|
+
modalBody.innerHTML = '';
|
|
1289
|
+
const viewerEl = renderKBItemViewer(viewerContent);
|
|
1290
|
+
modalBody.appendChild(viewerEl);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function goBackToKBList() {
|
|
1294
|
+
console.debug("[DAP] Going back to KB list");
|
|
1295
|
+
console.debug("[DAP] KB view changed to list");
|
|
1296
|
+
|
|
1297
|
+
if (!kbState || !kbState.modalBodyRef) {
|
|
1298
|
+
console.error("[DAP] Cannot go back: missing KB state or modal reference");
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
console.debug("[DAP] KB items count:", kbState.items.length);
|
|
1303
|
+
|
|
1304
|
+
// Update KB state for list view
|
|
1305
|
+
kbState.view = "list";
|
|
1306
|
+
kbState.selectedItem = null;
|
|
1307
|
+
|
|
1308
|
+
// Prepare KB content for rendering
|
|
1309
|
+
const kbContent = {
|
|
1310
|
+
title: kbState.title,
|
|
1311
|
+
items: kbState.items
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
// Clear modal body and restore KB list using preserved state
|
|
1315
|
+
kbState.modalBodyRef.innerHTML = '';
|
|
1316
|
+
const kbListEl = renderKnowledgeBase(kbContent);
|
|
1317
|
+
kbState.modalBodyRef.appendChild(kbListEl);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Utility function for file downloads
|
|
1321
|
+
function downloadFile(url: string, fileName?: string) {
|
|
1322
|
+
const a = document.createElement('a');
|
|
1323
|
+
a.href = url;
|
|
1324
|
+
a.download = fileName || 'download';
|
|
1325
|
+
document.body.appendChild(a);
|
|
1326
|
+
a.click();
|
|
1327
|
+
document.body.removeChild(a);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function ensureStyles() {
|
|
1331
|
+
if (!document.getElementById("dap-modal-style")) {
|
|
1332
|
+
const style = document.createElement("style");
|
|
1333
|
+
style.id = "dap-modal-style";
|
|
1334
|
+
style.textContent = modalCssText;
|
|
1335
|
+
document.head.appendChild(style);
|
|
1336
|
+
}
|
|
1337
|
+
}
|