1/** 2 * Browser Check - Diagnose-Tool für Support 3 * Aufgabe: Browser- und OS-Einstellungen erkennen, die Darstellung beeinflussen 4 */ 5 6const BROWSER_CHECK_CSS_HREF = '/(cms)/module/static/default/content/libs/js/browser-check/static/browser-check.css'; 7const BROWSER_CHECK_FONT_HREF = 'https://fonts-api.viomassl.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap'; 8 9function ensureLinkLoaded(href, markerAttr, extraAttrs) { 10 if (document.querySelector(`link[${markerAttr}="1"]`)) return; 11 const existingByHref = document.querySelector(`link[rel="stylesheet"][href="${href}"]`); 12 if (existingByHref) { existingByHref.setAttribute(markerAttr, '1'); return; } 13 const link = document.createElement('link'); 14 link.rel = 'stylesheet'; 15 link.href = href; 16 link.setAttribute(markerAttr, '1'); 17 if (extraAttrs) Object.entries(extraAttrs).forEach(([k, v]) => link.setAttribute(k, v)); 18 document.head.appendChild(link); 19} 20 21function ensureBrowserCheckCssLoaded() { 22 ensureLinkLoaded(BROWSER_CHECK_FONT_HREF, 'data-browser-check-font', { crossorigin: 'anonymous' }); 23 ensureLinkLoaded(BROWSER_CHECK_CSS_HREF, 'data-browser-check-css'); 24} 25 26class BrowserCheck { 27 constructor() { 28 this.checks = []; 29 this.results = []; 30 this.reportId = this.generateReportId(); 31 this.timestamp = new Date().toISOString(); 32 } 33 34 /** 35 * Eindeutige ID für diesen Report 36 */ 37 generateReportId() { 38 const date = new Date(); 39 const dateStr = date.toISOString().split('T')[0].replace(/-/g, ''); 40 const random = Math.random().toString(36).substring(2, 6); 41 return `bc_${dateStr}_${random}`; 42 } 43 44 /** 45 * Alle Checks registrieren 46 */ 47 registerChecks() { 48 this.registerCheck({ 49 id: 'prefers-reduced-motion', 50 title: 'Bewegung reduzieren', 51 category: 'preferences', 52 test: () => this.testMediaQuery('(prefers-reduced-motion: reduce)'), 53 severity: 'warning', 54 confidence: 'high', 55 userMessage: () => 'In Ihrem Browser ist die Option âBewegungen reduzieren" aktiviert.', 56 userImpact: 'Animationen, Ãbergänge und automatisch startende Inhalte wie Bildgalerien oder Slider können dadurch deaktiviert oder stark verlangsamt sein. Das ist bewusst für Nutzer zur Barrierefreiheit gemacht, die keine Bewegungen wünschen.', 57 userHelp: [ 58 { os: 'Windows', steps: ['Einstellungen öffnen (Win + I)', 'Erleichterte Bedienung â Anzeige', 'âAnimationen in Windows anzeigen" einschalten'] }, 59 { os: 'macOS', steps: ['Systemeinstellungen öffnen', 'Bedienungshilfen â Anzeige', 'âBewegung reduzieren" deaktivieren'] }, 60 { os: 'iOS', steps: ['Einstellungen öffnen', 'Bedienungshilfen â Bewegung', 'âBewegung reduzieren" deaktivieren'] }, 61 { os: 'Android', steps: ['Einstellungen öffnen', 'Bedienungshilfen', 'âAnimationen entfernen" deaktivieren'] } 62 ], 63 devAction: 'Animations-, Transition- und Autoplay-Logik auf prefers-reduced-motion prüfen.' 64 }); 65 66 this.registerCheck({ 67 id: 'forced-colors', 68 title: 'Erzwungene Systemfarben', 69 category: 'preferences', 70 test: () => this.testMediaQuery('(forced-colors: active)'), 71 severity: 'warning', 72 confidence: 'high', 73 userMessage: () => 'In Ihrem Browser ist der Hochkontrastmodus oder ein Farbfilter aktiv.', 74 userImpact: 'Das Betriebssystem überschreibt Farben auf der gesamten Seite durch eigene Systemfarben. Bilder, Symbole, Hintergründe und Texte können stark von der normalen Darstellung abweichen.', 75 userHelp: [ 76 { os: 'Windows 10', steps: ['Einstellungen öffnen (Win + I)', 'Erleichterte Bedienung â Hoher Kontrast', 'Hohen Kontrast deaktivieren'] }, 77 { os: 'Windows 11', steps: ['Einstellungen öffnen (Win + I)', 'Barrierefreiheit â Kontrast-Designs', 'Kein Design auswählen und âAnwenden" klicken'] } 78 ], 79 devAction: 'Icons, Kontraste und kritische UI-Elemente gegen Forced-Colors testen.' 80 }); 81 82 this.registerCheck({ 83 id: 'prefers-contrast', 84 title: 'Kontrastpräferenz', 85 category: 'preferences', 86 test: () => this.testContrast(), 87 severity: 'warning', 88 confidence: 'high', 89 userMessage: (value) => value === 'more' 90 ? 'In Ihrem Browser ist erhöhter Kontrast aktiviert.' 91 : 'In Ihrem Browser ist reduzierter Kontrast aktiviert.', 92 userImpact: 'Farben und Kontrastverhältnisse auf dieser Seite können für Sie anders erscheinen als für die meisten anderen Besucher. Insbesondere höherer Kontrast kann zu sehr kräftigen Farben und stärkeren Unterschieden zwischen Elementen führen. Ihr normales CI wird also leichte Abweichungen vorweisen. Automatische Anpassungen entsprechen den Vorgaben der Barrierefreiheitsgesetze verschiedener Länder der Europäischen Union.', 93 userHelp: [ 94 { os: 'macOS', steps: ['Systemeinstellungen öffnen', 'Bedienungshilfen â Anzeige', 'âKontrast erhöhen" deaktivieren'] }, 95 { os: 'Windows', steps: ['Einstellungen öffnen (Win + I)', 'Erleichterte Bedienung â Anzeige', 'Kontrast-Option an
95passen oder deaktivieren'] } 96 ], 97 devAction: 'Kontrast-sensitive Komponenten auf beide Extremwerte testen.' 98 }); 99 100 this.registerCheck({ 101 id: 'prefers-reduced-transparency', 102 title: 'Transparenzen reduzieren', 103 category: 'preferences', 104 test: () => this.testMediaQuery('(prefers-reduced-transparency: reduce)'), 105 severity: 'warning', 106 confidence: 'high', 107 userMessage: () => 'In Ihrem Browser ist die Option âTransparenzen reduzieren" aktiviert.', 108 userImpact: 'Glasmorphism-Effekte, semi-transparente Overlays und durchscheinende Hintergründe werden blockiert oder durch undurchsichtige Varianten ersetzt. Das Aussehen der Website kann dadurch deutlich unterschiedlich sein.', 109 userHelp: [ 110 { os: 'Windows', steps: ['Einstellungen öffnen (Win + I)', 'Erleichterte Bedienung â Anzeige', 'âTransparenzeffekte in Windows deaktivieren" ausschalten'] }, 111 { os: 'macOS', steps: ['Systemeinstellungen öffnen', 'Bedienungshilfen â Anzeige', 'âTransparenzeffekte reduzieren" deaktivieren'] } 112 ], 113 devAction: 'Transparenzabhängige CSS-Effekte (backdrop-filter, opacity, rgba) gegen diese Einstellung testen.' 114 }); 115 116 this.registerCheck({ 117 id: 'prefers-reduced-data', 118 title: 'Datenverbrauch reduzieren', 119 category: 'preferences', 120 test: () => this.testMediaQuery('(prefers-reduced-data: reduce)'), 121 severity: 'warning', 122 confidence: 'high', 123 userMessage: () => 'In Ihrem Browser ist die Option âDatenverbrauch reduzieren" aktiviert.', 124 userImpact: 'Diese Einstellung teilt der Website mit, dass Sie eine langsame oder begrenzte Internet-Verbindung haben. Videos, hochauflösende Bilder, Auto-Play und andere Inhalte mit hohem Datenverbrauch können daher deaktiviert oder in geringerer Qualität geladen werden. Das ist absichtlich und spart Ihre Bandbreite.', 125 userHelp: [ 126 { os: 'Chrome/Edge', steps: ['Browser-Menü öffnen', 'Einstellungen â Datenschutz und Sicherheit', 'Datenschutz-Optionen zur Datenersparnis überprüfen'] }, 127 { os: 'Safari', steps: ['Safari â Einstellungen', 'Datenschutz-Tab überprüfen oder System-Datensparprogramm aktivieren'] }, 128 { os: 'iOS/Android', steps: ['Einstellungen öffnen', 'Datenschutz oder Datenoptionen', 'Datensparprogramm oder Niedrig-Bandbreite-Modus deaktivieren'] } 129 ], 130 devAction: 'Video-Autoplay, Bild-Auflösung, Asset-Loading und Lazy-Loading gegen diese Einstellung prüfen.' 131 }); 132 133 this.registerCheck({ 134 id: 'inverted-colors', 135 title: 'Invertierte Farben', 136 category: 'preferences', 137 test: () => this.testMediaQuery('(inverted-colors: inverted)'), 138 severity: 'warning', 139 confidence: 'high', 140 userMessage: () => 'In Ihrem Browser ist der Modus âFarben invertieren" aktiviert.', 141 userImpact: 'Der Browser invertiert die Farben aller Inhalte (Schwarz wird WeiÃ, Blau wird Orange, etc.). Dies ist üblicherweise für Nachtsicht-Modi gedacht, kann aber die Website unbrauchbar machen, wenn sie mit invertierten Farben nicht lesbar ist.', 142 userHelp: [ 143 { os: 'Windows', steps: ['Einstellungen öffnen (Win + I)', 'Erleichterte Bedienung, Bildschirm-Filter überprüfen', 'Browser-Erweiterung für Farbfilter deaktivieren'] }, 144 { os: 'macOS', steps: ['Systemeinstellungen öffnen', 'Bedienungshilfen â Anzeige', 'âFarben invertieren" deaktivieren'] }, 145 { os: 'Chrome/Edge', steps: ['Browser-Erweiterung (z.B. Dark Reader) überprüfen und deaktivieren', 'DevTools (F12) â Rendering â CSS media feature Einstellungen'] } 146 ], 147 devAction: 'SVG-Icons, Bilder und kritische Design-Elemente im inverted-colors Modus testen.' 148 }); 149 } 150 151 /** 152 * Media Query testen 153 */ 154 testMediaQuery(query) { 155 try { 156 const match = window.matchMedia(query).matches; 157 return { 158 matched: match, 159 value: match ? 'active' : 'inactive', 160 method: 'matchMedia' 161 }; 162 } catch (e) { 163 return { 164 matched: false, 165 value: 'error', 166 method: 'matchMedia', 167 error: e.message 168 }; 169 } 170 } 171 172 /** 173 * Kontrast-Präferenz testen 174 */ 175 testContrast() { 176 try { 177 const moreContrast = window.matchMedia('(prefers-contrast: more)').matches; 178 const lessContrast = window.matchMedia('(prefers-contrast: less)').matches; 179 180 let value = 'no-preference'; 181 if (moreContrast) value = 'more'; 182 if (lessContrast) value = 'less'; 183 184 return { 185 matched: moreContrast || lessContrast, 186 value: value, 187 method: 'matchMedia' 188 }; 189 } catch (e) { 190 return { 191 matched: false, 192 value: 'not-supported', 193 method: 'matchMedia', 194 error: e.message 195 }; 196 } 197 } 198 199 /** 200 * Check registrieren 201 */ 202 registerCheck(checkDef) { 203 this.checks.push(checkDef); 204 } 205 206 /** 207 * Alle Checks ausführen 208 */ 209 async runAllChecks() { 210 for (const check of this.checks) { 211 const testResult = check.test(); 212 const resolvedUserMessage = testResult.matched && check.userMessage 213 ? check.userMessage(testResult.value) 214 : null; 215 216 this.results.push({ 217 id: check.id, 218 title: check.title, 219 category: check.category, 220 severity: check.severity, 221 confidence: check.confidence, 222 matched: testResult.matched, 223 value: testResult.value, 224 userMessage: resolvedUserMessage, 225 userImpact: check.userImpact || null, 226 userHelp: check.userHelp || [], 227 devAction: check.devAction, 228 evidence: { 229 method: testResult.method, 230 error: testResult.error || null 231 } 232 }); 233 } 234 } 235 236 /** 237 * Ergebnis-Report generieren 238 */ 239 getReport() {
240 const systemInfo = this.getSystemInfo(); 241 const warnings = this.results.filter(r => r.severity === 'warning' && r.matched).length; 242 const criticals = this.results.filter(r => r.severity === 'critical' && r.matched).length; 243 const topFindings = this.results 244 .filter(r => r.matched) 245 .sort((a, b) => { 246 const severityOrder = { critical: 0, warning: 1, info: 2 }; 247 return severityOrder[a.severity] - severityOrder[b.severity]; 248 }) 249 .slice(0, 3) 250 .map(r => r.title); 251 252 return { 253 reportId: this.reportId, 254 timestamp: this.timestamp, 255 url: window.location.href, 256 summary: { 257 totalChecks: this.results.length, 258 matched: this.results.filter(r => r.matched).length, 259 warningCount: warnings, 260 criticalCount: criticals, 261 topFindings: topFindings 262 }, 263 checks: this.results, 264 systemProfile: systemInfo, 265 quickChecks: this.getQuickChecks(systemInfo) 266 }; 267 } 268 269 /** 270 * Einfacher Text-Report für Copy 271 */ 272 getTextReport() { 273 const report = this.getReport(); 274 let text = `Browser Check Report\n`; 275 text += `===================\n\n`; 276 text += `Diagnose-ID: ${report.reportId}\n`; 277 text += `Zeit: ${report.timestamp}\n`; 278 text += `URL: ${report.url}\n\n`; 279 280 text += `Zusammenfassung:\n`; 281 text += `- Ãberprüfte Einstellungen: ${report.summary.totalChecks}\n`; 282 text += `- Aktive Einstellungen: ${report.summary.matched}\n`; 283 text += `- Warnungen: ${report.summary.warningCount}\n`; 284 text += `- Kritisch: ${report.summary.criticalCount}\n\n`; 285 286 if (report.summary.topFindings.length > 0) { 287 text += `Wichtigste Hinweise:\n`;
288 report.summary.topFindings.forEach((finding, i) => { 289 text += `${i + 1}. ${finding}\n`; 290 }); 291 text += `\n`; 292 } 293 294 if (report.quickChecks && report.quickChecks.length > 0) { 295 text += `Sofort-Checks:\n`; 296 report.quickChecks.forEach(item => { 297 text += `- ${item.label}: ${item.value}`; 298 if (item.note) text += ` (${item.note})`; 299 text += `\n`; 300 }); 301 text += `\n`; 302 } 303 304 if (report.systemProfile) { 305 const sys = report.systemProfile; 306 text += `Systemprofil:\n`; 307 text += `- Browser: ${sys.browserName} ${sys.browserVersion}\n`; 308 text += `- OS: ${this.formatOsLabel(sys)}\n`; 309 text += `- Viewport: ${sys.width} x ${sys.height}px\n`; 310 text += `- Zoom: ${sys.zoom}% (${this.getZoomMethodLabel(sys.zoomMethod)})\n`; 311 text += `- DPR: ${sys.dpr}\n`; 312 text += `- Default Font: ${sys.rootFontSize}\n`; 313 text += `- Touch: ${sys.touchCapable ? 'Ja' : 'Nein'} (${sys.maxTouchPoints})\n`; 314 text += `- Sprache/Land: ${sys.language} / ${sys.country}\n`; 315 text += `- Zeit: ${sys.timeLabel} (${sys.timezone})\n`; 316 text += `- Body-Klassen: ${sys.bodyBrowserClasses.length > 0 ? sys.bodyBrowserClasses.join(', ') : 'Keine browser-* Klasse erkannt'}\n`; 317 text += `- User Agent: ${sys.userAgent}\n\n`; 318 } 319 320 text += `Details:\n`; 321 text += `--------\n`; 322 report.checks.forEach(check => { 323 if (check.matched) { 324 text += `\n[${check.severity.toUpperCase()}] ${check.title}\n`; 325 text += `Wert: ${check.value}\n`; 326 if (check.userMessage) text += `Hinweis: ${check.userMessage}\n`; 327 if (check.userImpact) text += `Auswirkung: ${check.userImpact}\n`; 328 } 329 }); 330 331 return text; 332 } 333 334 /** 335 * JSON-Report 336 */ 337 getJsonReport() { 338 return JSON.stringify(this.getReport(), null, 2); 339 } 340 341 /** 342 * Systeminformationen sammeln 343 */ 344 getSystemInfo() { 345 // Standard Font Size aus Element style ermitteln 346 const rootFontSize = window.getComputedStyle(document.documentElement).fontSize; 347 348 // Zoom-Faktor konservativ bestimmen. 349 // Hinweis: Browser expose Desktop-Seitenzoom nur sehr eingeschraenkt. 350 // outerWidth / innerWidth wird durch angedockte DevTools oder Sidebars verfälscht. 351 let zoomFactor = 1; 352 let zoomMethod = 'default'; 353 354 if (window.visualViewport && Number.isFinite(window.visualViewport.scale) && window.visualViewport.scale > 0) { 355 const viewportScale = window.visualViewport.scale; 356 if (Math.abs(viewportScale - 1) > 0.01) { 357 zoomFactor = viewportScale; 358 zoomMethod = 'visualViewport-scale'; 359 } else { 360 zoomMethod = 'visualViewport'; 361 } 362 } 363 364 if (zoomMethod === 'default' && window.outerWidth && window.innerWidth) { 365 const outerInnerFactor = window.outerWidth / window.innerWidth; 366 const widthDifference = window.outerWidth - window.innerWidth; 367 const likelyDockedSidebar = widthDifference > 420; 368 369 if (!likelyDockedSidebar && Number.isFinite(outerInnerFactor) && outerInnerFactor > 0.5 && outerInnerFactor < 1.4) { 370 zoomFactor = outerInnerFactor; 371 zoomMethod = 'outer-inner'; 372 } else if (likelyDockedSidebar) { 373 zoomMethod = 'devtools-unreliable'; 374 } 375 } 376 377 const zoomPercent = Math.round(zoomFactor * 100); 378 379 const language = navigator.language || 'Nicht erkannt'; 380 const country = language.includes('-') ? language.split('-')[1].toUpperCase() : 'Nicht gesetzt'; 381 const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 382 const timeLabel = new Intl.DateTimeFormat(undefined, { 383 hour: '2-digit', 384 minute: '2-digit' 385 }).format(new Date()); 386 387 const browserInfo = this.getBrowserInfo(); 388 const osInfo = this.getOsInfo(); 389 390 const bodyClasses = Array.from(document.body?.classList || []); 391 const bodyBrowserClasses = bodyClasses.filter(cls => cls.startsWith('browser-') || cls.startsWith('browswer-')); 392 393 const maxTouchPoints = Number(navigator.maxTouchPoints || navigator.msMaxTouchPoints || 0); 394 const touchCapable = ('ontouchstart' in window) || maxTouchPoints > 0; 395 396 return { 397 width: window.innerWidth, 398 height: window.innerHeight, 399 dpr: window.devicePixelRatio, 400 rootFontSize: rootFontSize, 401 zoom: zoomPercent, 402 zoomMethod: zoomMethod, 403 browserName: browserInfo.name, 404 browserVersion: browserInfo.version, 405 osName: osInfo.name, 406 osVersion: osInfo.version, 407 osVersionSource: osInfo.versionSource || 'ua', 408 language: language, 409 country: country, 410 timeLabel: timeLabel, 411 timezone: timezone, 412 touchCapable: touchCapable, 413 maxTouchPoints: maxTouchPoints, 414 bodyBrowserClasses: bodyBrowserClasses, 415 userAgent: navigator.userAgent 416 }; 417 } 418 419 /** 420 * OS aus User Agent herauslesen 421 */ 422 getOsInfo() { 423 const ua = navigator.userAgent; 424 let m; 425 426 if ((m = ua.match(/iPad.*CPU OS ([\d_]+)/))) return { name: 'iPadOS', version: m[1].replace(/_/g, '.') }; 427 if ((m = ua.match(/iPhone.*CPU.*OS ([\d_]+)/))) return { name: 'iOS', version: m[1].replace(/_/g, '.') }; 428 if ((m = ua.match(/Android ([\d.]+)/))) return { name: 'Android', version: m[1] }; 429 if ((m = ua.match(/Mac OS X ([\d_]+)/))) { 430 const macUaVersion = m[1].replace(/_/g, '.'); 431 const isFrozenUaVersion = macUaVersion === '10.15.7'; 432 return { 433 name: 'macOS', 434 version: isFrozenUaVersion ? '' : macUaVersion, 435 versionSource: isFrozenUaVersion ? 'ua-frozen' : 'ua', 436 }; 437 } 438 439 const winNtMap = { '10.0': '10/11', '6.3': '8.1', '6.2': '8', '6.1': '7', '6.0': 'Vista', '5.2': 'XP x64', '5.1': 'XP', '5.0': '2000' }; 440 if ((m = ua.match(/Windows NT ([\d.]+)/))) return { name: 'Windows', version: winNtMap[m[1]] || m[1], versionSource: 'ua' }; 441 if (ua.includes('Win 9x 4.90')) return { name: 'Windows', version: 'ME', versionSource: 'ua' }; 442 if (ua.includes('Windows 98')) return { name: 'Windows', version: '98', versionSource: 'ua' }; 443 if (ua.includes('Windows 95')) return { name: 'Windows', version: '95', versionSource: 'ua' }; 444 if (ua.includes('Linux')) return { name: 'Linux', version: '', versionSource: 'ua' }; 445 446 return { name: 'Unbekannt', version: '', versionSource: 'ua' }; 447 } 448 449 /** 450 * Browser aus User Agent herauslesen 451 */ 452 getBrowserInfo() { 453 const ua = navigator.userAgent; 454 const patterns = [
455 { name: 'Edge', regex: /Edg\/(\d+(?:\.\d+)?)/ }, 456 { name: 'Opera', regex: /OPR\/(\d+(?:\.\d+)?)/ }, 457 { name: 'Chrome', regex: /Chrome\/(\d+(?:\.\d+)?)/ }, 458 { name: 'Firefox', regex: /Firefox\/(\d+(?:\.\d+)?)/ }, 459 { name: 'Safari', regex: /Version\/(\d+(?:\.\d+)?).*Safari\// } 460 ]; 461 462 for (const entry of patterns) { 463 const match = ua.match(entry.regex); 464 if (match) { 465 return { name: entry.name, version: match[1] }; 466 } 467 } 468 469 return { name: 'Unbekannt', version: '-' }; 470 } 471 472 /** 473 * Zoom-Status prüfen 474 */ 475 detectZoomWarning() { 476 const sysInfo = this.getSystemInfo(); 477 // Ab ca. 105% oder 95% wird es fuer Layout-/Supportthemen relevant 478 return Math.abs(sysInfo.zoom - 100) > 5; 479 } 480 481 getZoomStatus(zoomPercent) { 482 if (zoomPercent >= 125 || zoomPercent <= 85) { 483 return { severity: 'critical', note: 'stark abweichend' }; 484 } 485 if (zoomPercent >= 106 || zoomPercent <= 94) { 486 return { severity: 'warning', note: 'abweichend' }; 487 } 488 return { severity: 'ok', note: 'normal' }; 489 } 490 491 getZoomMethodLabel(method) { 492 if (method === 'visualViewport-scale') return 'Viewport-Scale'; 493 if (method === 'visualViewport') return 'Viewport'; 494 if (method === 'outer-inner') return 'Fenster-Proportion'; 495 if (method === 'devtools-unreliable') return 'ungenaue Messung (DevTools?)'; 496 return 'Standard'; 497 } 498 499 formatOsLabel(systemInfo = this.getSystemInfo()) { 500 if (!systemInfo) return 'Unbekannt'; 501 502 if (systemInfo.osName === 'macOS' && systemInfo.osVersionSource === 'ua-frozen') { 503 return 'macOS'; 504 } 505 506 return systemInfo.osName + (systemInfo.osVersion ? ` ${systemInfo.osVersion}` : ''); 507 } 508 509 getQuickChecks(systemInfo = this.getSystemInfo()) { 510 const zoomStatus = this.getZoomStatus(systemInfo.zoom); 511 const quickChecks = [ 512 { 513 id: 'browser', 514 label: 'Browser + OS', 515 value: `${systemInfo.browserName} ${systemInfo.browserVersion}`, 516 note: this.formatOsLabel(systemInfo), 517 severity: 'info' 518 }, 519 { 520 id: 'viewport', 521 label: 'Viewport', 522 value: `${systemInfo.width} x ${systemInfo.height}`, 523 severity: 'info' 524 } 525 ]; 526 527 if (zoomStatus.severity !== 'ok') { 528 quickChecks.push({ 529 id: 'zoom', 530 label: 'Zoom', 531 value: `${systemInfo.zoom}%`, 532 note: zoomStatus.note, 533 severity: zoomStatus.severity 534 }); 535 } else { 536 quickChecks.push({ 537 id: 'touch', 538 label: 'Touch', 539 value: systemInfo.touchCapable ? 'Ja' : 'Nein', 540 severity: 'info' 541 }); 542 } 543 544 return quickChecks; 545 } 546 547 getGregorianEasterSunday(year) { 548 // Meeus/Jones/Butcher-Algorithmus (gregorianischer Kalender) 549 const a = year % 19; 550 const b = Math.floor(year / 100); 551 const c = year % 100; 552 const d = Math.floor(b / 4); 553 const e = b % 4; 554 const f = Math.floor((b + 8) / 25); 555 const g = Math.floor((b - f + 1) / 3); 556 const h = (19 * a + b - d - g + 15) % 30; 557 const i = Math.floor(c / 4); 558 const k = c % 4; 559 const l = (32 + 2 * e + 2 * i - h - k) % 7; 560 const m = Math.floor((a + 11 * h + 22 * l) / 451);
561 const month = Math.floor((h + l - 7 * m + 114) / 31); // 3=Maerz, 4=April 562 const day = ((h + l - 7 * m + 114) % 31) + 1; 563 return { year, month, day }; 564 } 565 566 shiftDateUtc(year, month, day, offsetDays) { 567 const date = new Date(Date.UTC(year, month - 1, day + offsetDays)); 568 return { 569 year: date.getUTCFullYear(), 570 month: date.getUTCMonth() + 1, 571 day: date.getUTCDate(), 572 }; 573 } 574 575 toDateKey(year, month, day) { 576 const m = String(month).padStart(2, '0'); 577 const d = String(day).padStart(2, '0'); 578 return `${year}-${m}-${d}`; 579 } 580 581 getBadenWuerttembergHolidayName(year, month, day) { 582 const fixedHolidays = { 583 '01-01': 'Neujahr', 584 '01-06': 'Heilige Drei Koenige', 585 '05-01': 'Tag der Arbeit', 586 '10-03': 'Tag der Deutschen Einheit', 587 '11-01': 'Allerheiligen', 588 '12-25': '1. Weihnachtstag', 589 '12-26': '2. Weihnachtstag', 590 }; 591 592 const fixedKey = `${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; 593 if (fixedHolidays[fixedKey]) return fixedHolidays[fixedKey]; 594 595 const easter = this.getGregorianEasterSunday(year); 596 const movableHolidays = [ 597 { offset: -2, name: 'Karfreitag' }, 598 { offset: 1, name: 'Ostermontag' }, 599 { offset: 39, name: 'Christi Himmelfahrt' }, 600 { offset: 50, name: 'Pfingstmontag' }, 601 { offset: 60, name: 'Fronleichnam' }, 602 ]; 603 604 const targetKey = this.toDateKey(year, month, day); 605 for (const holiday of movableHolidays) { 606 const moved = this.shiftDateUtc(easter.year, easter.month, easter.day, holiday.offset); 607 if (this.toDateKey(moved.year, moved.month, moved.day) === targetKey) { 608 return holiday.name; 609 } 610 } 611 612 return ''; 613 } 614 615 getSupportContext() { 616 const supportPortalUrl = 'https://support.vioma.de/portal'; 617 const supportPhone = '+49 781 31055-500'; 618 const supportHoursLabel = 'werktags 9-17 Uhr'; 619 const supportTimeZone = 'Europe/Berlin'; 620 621 const now = new Date(); 622 const supportTimeParts = new Intl.DateTimeFormat('de-DE', { 623 weekday: 'short', 624 year: 'numeric', 625 month: '2-digit', 626 day: '2-digit', 627 hour: '2-digit', 628 minute: '2-digit', 629 hour12: false, 630 timeZone: supportTimeZone 631 }).formatToParts(now); 632 const getPart = (type) => { 633 const part = supportTimeParts.find(entry => entry.type === type); 634 return part ? part.value : ''; 635 }; 636 637 const weekdayShort = getPart('weekday').replace('.', ''); 638 const year = Number(getPart('year'));
639 const month = Number(getPart('month')); 640 const day = Number(getPart('day')); 641 const hour = Number(getPart('hour')); 642 const minute = Number(getPart('minute')); 643 const timeLabel = new Intl.DateTimeFormat('de-DE', { 644 hour: '2-digit', 645 minute: '2-digit', 646 hour12: false, 647 timeZone: supportTimeZone 648 }).format(now); 649 650 const isWeekday = ['Mo', 'Di', 'Mi', 'Do', 'Fr'].includes(weekdayShort); 651 const isWithinHours = hour >= 9 && (hour < 17 || (hour === 17 && minute === 0)); 652 const holidayName = this.getBadenWuerttembergHolidayName(year, month, day); 653 const isHoliday = Boolean(holidayName); 654 const isPhoneAvailable = isWeekday && isWithinHours && !isHoliday; 655 656 const hash = window.location.hash || ''; 657 const cidMatch = hash.match(/^#cid-(\d+)$/i); 658 const cid = cidMatch ? cidMatch[1] : ''; 659 const pageUrl = `${window.location.origin}${window.location.pathname}${window.location.search}`; 660 const cidUrl = cid ? `${pageUrl}#cid-${cid}` : ''; 661 662 return { 663 supportPortalUrl, 664 supportPhone, 665 supportHoursLabel, 666 supportTimeZone, 667 isPhoneAvailable, 668 isHoliday, 669 holidayName, 670 supportTimeLabel: `${weekdayShort}, ${timeLabel}`, 671 pageUrl, 672 cid, 673 cidUrl, 674 locationLabel: cid ? `Element ${cid}` : 'Kein konkretes Seitenelement automatisch erkannt' 675 }; 676 } 677 678 getSupportTicketText(cidOverride = null, details = null) { 679 const report = this.getReport(); 680 const support = this.getSupportContext(); 681 const sys = report.systemProfile; 682 const activeChecks = report.checks.filter(check => check.matched); 683 684 const resolvedCid = cidOverride ? cidOverride.cid : support.cid; 685 const resolvedCidUrl = resolvedCid ? `${support.pageUrl}#cid-${resolvedCid}` : ''; 686 const resolvedLabel = cidOverride ? cidOverride.label : null; 687 const observation = String(details?.observation || '').trim() || '[Hier einfügen]'; 688 const expected = String(details?.expected || '').trim() || '[Hier einfügen]'; 689 690 let text = 'Support-Zusammenfassung (aus der Website Hilfe)\n'; 691 text += '====================================\n\n'; 692 text += 'Bitte hier kurz beschreiben, was nicht wie erwartet funktioniert:\n\n'; 693 text += 'Beobachtung / was genau ist problematisch?\n'; 694 text += `${observation}\n\n`; 695 text += 'Stattdessen war erwartet worden:\n'; 696 text += `${expected}\n\n`; 697 698 text += '\n\nTicket-Kontext:\n'; 699 text += `- Diagnose-ID: ${report.reportId}\n`; 700 text += `- Seite: ${support.pageUrl}\n`; 701 if (resolvedCidUrl) { 702 text += `- Problemstelle (Direktlink): ${resolvedCidUrl}\n`; 703 if (resolvedLabel) text += `- Problemstelle (Bezeichnung): ${resolvedLabel}\n`; 704 text += `- Element in Backend: ${window.location.origin}/condeon/frontend.php?type=in&page_content=${resolvedCid}\n`; 705 } else { 706 text += '- Problemstelle: Kein konkretes Seitenelement ausgewählt\n'; 707 } 708 text += `- Zeitpunkt: ${support.supportTimeLabel} (${support.supportTimeZone})\n\n`; 709 710 text += 'Schnellprofil:\n'; 711 text += `- Browser: ${sys.browserName} ${sys.browserVersion}\n`; 712 text += `- OS: ${this.formatOsLabel(sys)}\n`; 713 text += `- Viewport: ${sys.width} x ${sys.height}px\n`; 714 text += `- Zoom: ${sys.zoom}% (${this.getZoomMethodLabel(sys.zoomMethod)})\n`; 715 text += `- DPR: ${sys.dpr}\n`; 716 text += `- Touch: ${sys.touchCapable ? 'Ja' : 'Nein'} (${sys.maxTouchPoints})\n\n`; 717 718 text += `Auffällige Einstellungen (${activeChecks.length}):\n`; 719 if (activeChecks.length === 0) { 720 text += '- Keine darstellungsrelevanten Browser-Einstellungen aktiv\n'; 721 } else { 722 activeChecks.forEach(check => { 723 text += `- [${check.severity.toUpperCase()}] ${check.title}: ${check.value}\n`; 724 }); 725 } 726 727 return text; 728 } 729 730 getSupportSelfHelpItems(report = this.getReport()) { 731 const baseItems = [ 732 { 733 key: 'incognito', 734 title: 'Seite im Inkognito- oder Privatmodus getestet?', 735 text: 'So lässt sich schnell prüfen, ob Browser-Erweiterungen oder zwischengespeicherte Daten die Ursache sind.' 736 }, 737 { 738 key: 'reload', 739 title: 'Seite vollständig neu geladen?', 740 text: 'Laden Sie die Seite einmal neu und prüfen Sie, ob das Problem weiter besteht. Unter Windows: Strg + F5, unter Mac: Cmd + Shift + R.' 741 } 742 ]; 743 const items = []; 744 745 const dynamicByCheckId = { 746 'forced-colors': { 747 key: 'forced-colors', 748 title: 'Haben Sie die Seite ohne Hochkontrastmodus geöffnet?', 749 text: 'Wenn Hochkontrast oder erzwungene Systemfarben aktiv sind, können Farben, Icons und Hintergründe stark abweichen.' 750 }, 751 'prefers-contrast': { 752 key: 'prefers-contrast', 753 title: 'Haben Sie die Seite ohne Kontrastpräferenz geöffnet?', 754 text: 'Erhöhter oder reduzierter Kontrast kann Farben und Hervorhebungen sichtbar verändern.' 755 }, 756 'prefers-reduced-motion': { 757 key: 'prefers-reduced-motion', 758 title: 'Haben Sie die Seite ohne reduzierte Bewegungen geöffnet?', 759 text: 'Animationen, Slider oder automatische Ãbergänge können sonst bewusst abgeschwächt oder deaktiviert sein.' 760 }, 761 'prefers-reduced-transparency': { 762 key: 'prefers-reduced-transparency', 763 title: 'Haben Sie die Seite ohne reduzierte Transparenzen geöffnet?', 764 text: 'Transparente Flächen, Overlays oder Glaseffekte können dadurch anders aussehen als vorgesehen.' 765 }, 766 'prefers-reduced-data': { 767 key: 'prefers-reduced-data', 768 title: 'Haben Sie die Seite ohne Datensparmodus geöffnet?', 769 text: 'Bilder, Videos oder weitere Inhalte können in reduzierter Form geladen oder bewusst weggelassen werden.' 770 }, 771 'inverted-colors': { 772 key: 'inverted-colors', 773 title: 'Haben Sie die Seite ohne invertierte Farben geöffnet?',
774 text: 'Bei invertierten Farben können Symbole, Markenfarben und Kontraste stark verfälscht erscheinen.' 775 } 776 }; 777 778 report.checks 779 .filter(check => check.matched) 780 .forEach(check => { 781 const item = dynamicByCheckId[check.id]; 782 if (item && !items.some(entry => entry.key === item.key)) { 783 items.push(item); 784 } 785 }); 786 787 const zoomStatus = this.getZoomStatus(report.systemProfile.zoom); 788 if (zoomStatus.severity !== 'ok' && !items.some(entry => entry.key === 'zoom')) { 789 items.push({ 790 key: 'zoom', 791 title: 'Haben Sie die Seite bei 100 % Zoom geöffnet?', 792 text: `Aktuell wurde ein Zoom von ${report.systemProfile.zoom}% erkannt. Das kann Abstände, Umbrüche und GröÃen sichtbar verändern.` 793 }); 794 } 795 796 baseItems.forEach(item => { 797 if (!items.some(entry => entry.key === item.key)) { 798 items.push(item); 799 } 800 }); 801 802 return items; 803 } 804} 805 806/** 807 * Browser Check UI Manager 808 */ 809class BrowserCheckUI { 810 constructor(browserCheck) { 811 this.browserCheck = browserCheck; 812 this.container = null; 813 this.shadowHost = null; 814 this.onClose = null; 815 this.closeTimeoutId = null; 816 this.supportSelfHelpState = {}; 817 this.supportFlowStarted = false; 818 this.supportDraft = { observation: '', expected: '' }; 819 this.cidPickerState = { state: 'idle', cid: null, label: null, lastSupportText: null }; 820 this._pickerHandlers = null; 821 this.portalLaunchAt = 0; 822 this.portalStepTimer = null; 823 this.portalStepStorageKey = this.getPortalStepStorageKey(); 824 825 try { 826 const storedPortalLaunchAt = Number(sessionStorage.getItem(this.portalStepStorageKey)); 827 if (Number.isFinite(storedPortalLaunchAt) && storedPortalLaunchAt > 0) { 828 this.portalLaunchAt = storedPortalLaunchAt; 829 } 830 // One-shot restore: beim naechsten Aufruf einmal lesen und sofort leeren. 831 sessionStorage.removeItem(this.portalStepStorageKey); 832 // Altlast aus frueherer globaler Speicherung bereinigen. 833 sessionStorage.removeItem('bc_support_portal_clicked_at'); 834 } catch (e) { /* ignore */ } 835 } 836 837 getPortalStepStorageKey() { 838 const pageKey = `${window.location.pathname}${window.location.search}`; 839 return `bc_support_portal_clicked_at::${pageKey}`; 840 } 841 842 getSeverityLabel(severity) { 843 if (severity === 'warning') return 'Warnung'; 844 if (severity === 'critical') return 'Kritisch'; 845 return 'Info'; 846 } 847 848 formatNumber(value) { 849 const numericValue = Number(value); 850 if (!Number.isFinite(numericValue)) return String(value); 851 return String(Number(numericValue.toFixed(2))); 852 } 853 854 escapeHtml(value) { 855 return String(value || '') 856 .replace(/&/g, '&') 857 .replace(/</g, '<') 858 .replace(/>/g, '>') 859 .replace(/"/g, '"') 860 .replace(/'/g, '''); 861 } 862 863 getDraftValue(key) { 864 return String(this.supportDraft?.[key] || '').trim(); 865 } 866 867 isSupportDraftValid() { 868 return this.getDraftValue('observation').length >= 30 && this.getDraftValue('expected').length >= 30; 869 } 870 871 getZoomMethodLabel(method) { 872 return this.browserCheck.getZoomMethodLabel(method); 873 } 874 875 getIcon(name, className = '') { 876 const classAttr = className ? ` class="${className}"` : ''; 877 878 if (name === 'search') { 879 return `<svg${classAttr} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"></circle><path d="m21 21-4.3-4.3"></path></svg>`; 880 } 881 882 if (name === 'close') { 883 return `<svg${classAttr} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>`; 884 } 885 886 if (name === 'chevron-right') { 887 return `<svg${classAttr} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
887<path d="m9 18 6-6-6-6"></path></svg>`; 888 } 889 890 if (name === 'circle-check') { 891 return `<svg${classAttr} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>`; 892 } 893 894 if (name === 'info') { 895 return `<svg${classAttr} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>`; 896 } 897 898 if (name === 'pencil') { 899 return `<svg${classAttr} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m17 3 4 4M3 17.25V21h3.75L17.81 9.94M21 21H3v-3.75L17.81 2.19l4 4" /></svg>`; 900 } 901 902 return ''; 903 } 904 905 syncSupportSelfHelpState(items) { 906 items.forEach(item => { 907 if (!(item.key in this.supportSelfHelpState)) { 908 this.supportSelfHelpState[item.key] = false; 909 } 910 }); 911 912 Object.keys(this.supportSelfHelpState).forEach(key => { 913 if (!items.some(item => item.key === key)) { 914 delete this.supportSelfHelpState[key]; 915 } 916 }); 917 } 918 919 isSupportSelfHelpComplete(items) { 920 if (!items || items.length === 0) return true; 921 return items.every(item => this.supportSelfHelpState[item.key]); 922 } 923 924 updateSupportUnlockState() { 925 if (!this.container) return; 926 927 const checklistButtons = Array.from(this.container.querySelectorAll('[data-selfhelp-key]')); 928 const remaining = checklistButtons.filter(button => button.getAttribute('aria-checked') !== 'true').length; 929 const allChecked = remaining === 0; 930 const stage = this.container.querySelector('.bc-support-stage'); 931 const selfHelpCard = this.container.querySelector('.bc-support-selfhelp-card'); 932 const nextStepSection = this.container.querySelector('.bc-support-nextstep-card'); 933 const note = this.container.querySelector('.bc-support-selfhelp-note'); 934 935 if (note) { 936 if (allChecked) { 937 note.textContent = 'Alles geprüft. Die nächsten Support-Schritte sind jetzt freigeschaltet.'; 938 note.classList.add('is-complete'); 939 } else { 940 note.textContent = `Bitte prüfen Sie zuerst alle offenen Punkte und haken Sie diese nach Ihrem Test ab. Noch ${remaining} offen.`; 941 note.classList.remove('is-complete'); 942 } 943 } 944 945 if (!stage || !selfHelpCard || !nextStepSection) return; 946 947 if (allChecked) { 948 stage.classList.add('is-unlocked'); 949 selfHelpCard.classList.add('is-leaving'); 950 nextStepSection.classList.add('is-visible'); 951 nextStepSection.setAttribute('aria-hidden', 'false'); 952 return; 953 } 954 955 stage.classList.remove('is-unlocked'); 956 selfHelpCard.classList.remove('is-leaving'); 957 nextStepSection.classList.remove('is-visible'); 958 nextStepSection.setAttribute('aria-hidden', 'true'); 959 } 960 961 962 startSupportFlow() { 963 if (!this.container || this.supportFlowStarted) return; 964 965 this.supportFlowStarted = true; 966 const guide = this.container.querySelector('.bc-support-guide'); 967 const startCard = this.container.querySelector('.bc-support-start'); 968 const flow = this.container.querySelector('.bc-support-flow'); 969 970 if (guide) guide.classList.add('is-started'); 971 if (startCard) startCard.classList.add('is-hidden'); 972 if (flow) { 973 flow.classList.add('is-visible'); 974 flow.setAttribute('aria-hidden', 'false'); 975 } 976 977 this.updateSupportUnlockState(); 978 this.updatePickerButtonState(); 979 } 980 981 updateSupportDraftField(field, value) { 982 if (!['observation', 'expected'].includes(field)) return; 983 this.supportDraft[field] = value; 984 if (this.cidPickerState.state === 'copied') { 985 this.cidPickerState.state = 'selected'; 986 } 987 const counter = this.container?.querySelector(`[data-counter-for="${field}"]`); 988 if (counter) { 989 const len = value.trim().length; 990 const valid = len >= 30; 991 counter.textContent = valid ? `${len}\u2009\u2713` : `${len}\u00a0/\u00a030`; 992 counter.classList.toggle('is-valid', valid); 993 } 994 this.updatePickerButtonState(); 995 } 996 997 focusFirstIncompleteSupportField() { 998 if (!this.container) return; 999 1000 const nextField = ['observation', 'expected'].find((field) => this.getDraftValue(field).length < 30); 1001 if (!nextField) return; 1002 1003 const target = this.container.querySelector(`textarea[data-support-field="${nextField}"]`); 1004 if (!target) return; 1005
1006 target.focus(); 1007 const valueLength = target.value.length; 1008 if (typeof target.setSelectionRange === 'function') { 1009 target.setSelectionRange(valueLength, valueLength); 1010 } 1011 } 1012 1013 isPortalStepDone() { 1014 return this.portalLaunchAt > 0 && (Date.now() - this.portalLaunchAt) >= 60000; 1015 } 1016 1017 schedulePortalStepUpdate() { 1018 if (this.portalStepTimer) { 1019 window.clearTimeout(this.portalStepTimer); 1020 this.portalStepTimer = null; 1021 } 1022 1023 if (!this.portalLaunchAt || this.isPortalStepDone()) return; 1024 1025 const waitMs = Math.max(0, 60000 - (Date.now() - this.portalLaunchAt)); 1026 this.portalStepTimer = window.setTimeout(() => { 1027 this.portalStepTimer = null; 1028 this.updatePickerButtonState(); 1029 }, waitMs + 20); 1030 } 1031 1032 markPortalStepStarted() { 1033 this.portalLaunchAt = Date.now(); 1034 try { 1035 sessionStorage.setItem(this.portalStepStorageKey, String(this.portalLaunchAt)); 1036 } catch (e) { /* ignore */ } 1037 1038 this.schedulePortalStepUpdate(); 1039 this.updatePickerButtonState(); 1040 } 1041 1042 composeSupportSummary() { 1043 if (!['selected', 'copied'].includes(this.cidPickerState.state)) return; 1044 if (!this.isSupportDraftValid()) return; 1045 1046 const cidOverride = this.cidPickerState.cid 1047 ? { cid: this.cidPickerState.cid, label: this.cidPickerState.label } 1048 : null; 1049 const text = this.browserCheck.getSupportTicketText(cidOverride, { 1050 observation: this.getDraftValue('observation'), 1051 expected: this.getDraftValue('expected'), 1052 }); 1053 1054 this.cidPickerState.lastSupportText = text; 1055 this.cidPickerState.state = 'copied'; 1056 1057 navigator.clipboard.writeText(text).catch(err => console.error('Fehler beim Kopieren:', err)); 1058 try { 1059 sessionStorage.setItem('bc_support_text', text); 1060 if (this.cidPickerState.cid) { 1061 sessionStorage.setItem('bc_support_cid', this.cidPickerState.cid); 1062 } else { 1063 sessionStorage.removeItem('bc_support_cid'); 1064 } 1065 } catch (e) { /* ignore */ } 1066 1067 this.updatePickerButtonState(); 1068 } 1069 1070 getFrontendEditBridge() { 1071 return window.__condeonAssist?.frontendEdit || null; 1072 } 1073 1074 getPickableCidElements() { 1075 const bridge = this.getFrontendEditBridge(); 1076 const bridgedElements = bridge?.getSupportCidElements?.(); 1077 if (Array.isArray(bridgedElements) && bridgedElements.length > 0) { 1078 return bridgedElements; 1079 } 1080 return Array.from(document.querySelectorAll('[data-cid]')); 1081 } 1082 toggleSupportSelfHelpItem(button) { 1083 const key = button.dataset.selfhelpKey; 1084 const nextState = !(button.getAttribute('aria-checked') === 'true'); 1085 1086 this.supportSelfHelpState[key] = nextState; 1087 button.setAttribute('aria-checked', nextState ? 'true' : 'false'); 1088 button.classList.toggle('is-checked', nextState); 1089 1090 this.updateSupportUnlockState(); 1091 } 1092 1093 startElementPickMode() { 1094 const bridge = this.getFrontendEditBridge(); 1095 const panelContainer = document.getElementById('bc-panel-container'); 1096 1097 // Ensure the same effect as clicking "Elemente bearbeiten" 1098 // so selectable elements are visibly marked in support pick mode. 1099 if (sessionStorage.getItem('fe_edit_enabled') !== 'true') { 1100 sessionStorage.setItem('fe_edit_enabled', 'true'); 1101 bridge?.enable?.(); 1102 this.updateFrontendEditButtonState(); 1103 } 1104 1105 const cidElements = this.getPickableCidElements(); 1106 if (cidElements.length === 0) { 1107 this.copySupportWithoutCid(); 1108 return; 1109 } 1110 1111 const previousPickerState = { 1112 state: this.cidPickerState.state, 1113 cid: this.cidPickerState.cid, 1114 label: this.cidPickerState.label, 1115 }; 1116 this.cidPickerState.state = 'selecting'; 1117 if (panelContainer) panelContainer.classList.add('bc-pick-mode-hidden'); 1118 1119 if (bridge?.startSupportPickMode) { 1120 bridge.startSupportPickMode( 1121 (el) => { 1122 panelContainer?.classList.remove('bc-pick-mode-hidden'); 1123 this.onCidPicked(el.dataset.cid, this.extractElementLabel(el)); 1124 }, 1125 () => { 1126 panelContainer?.classList.remove('bc-pick-mode-hidden'); 1127 this.copySupportWithoutCid(); 1128 }, 1129 () => { 1130 panelContainer?.classList.remove('bc-pick-mode-hidden'); 1131 this.cidPickerState.state = previousPickerState.state || 'idle'; 1132 this.cidPickerState.cid = previousPickerState.cid || null; 1133 this.cidPickerState.label = previousPickerState.label || null; 1134 this.updatePickerButtonState(); 1135 }, 1136 cidElements 1137 ); 1138 return; 1139 } 1140 1141 // Fallback: eigener DOM-Picker (wenn frontend_edit.js nicht geladen ist) 1142 bridge?.suspend?.(); 1143 const eligibleCidElements = new Set(cidElements); 1144 document.body.classList.add('bc-pick-mode'); 1145 1146 const highlight = document.createElement('div'); 1147 highlight.id = 'bc-picker-highlight';
1148 document.body.appendChild(highlight); 1149 1150 const labelEl = document.createElement('div'); 1151 labelEl.id = 'bc-picker-highlight-label'; 1152 document.body.appendChild(labelEl); 1153 1154 const bar = document.createElement('div'); 1155 bar.id = 'bc-picker-bar'; 1156 bar.innerHTML = ` 1157 <span>Klicken Sie auf die Problemstelle auf der Seite</span> 1158 <button id="bc-picker-no-cid" type="button">Ohne Element fortfahren</button> 1159 <button id="bc-picker-cancel" type="button">Abbrechen (Esc)</button> 1160 `; 1161 document.body.appendChild(bar); 1162 1163 let currentCidEl = null; 1164 1165 const positionHighlight = (el) => { 1166 const rect = el.getBoundingClientRect(); 1167 highlight.style.top = `${rect.top}px`; 1168 highlight.style.left = `${rect.left}px`; 1169 highlight.style.width = `${rect.width}px`; 1170 highlight.style.height = `${rect.height}px`; 1171 highlight.style.display = 'block'; 1172 1173 labelEl.textContent = `CID ${el.dataset.cid} â klicken zum Auswählen`; 1174 labelEl.style.display = 'block'; 1175 1176 const labelWidth = labelEl.offsetWidth; 1177 const labelHeight = labelEl.offsetHeight; 1178 const viewportPadding = 8; 1179 1180 let top = rect.top - labelHeight - 8; 1181 if (top < viewportPadding) { 1182 top = rect.bottom + 8; 1183 } 1184 if (top + labelHeight > window.innerHeight - viewportPadding) { 1185 top = Math.max(viewportPadding, window.innerHeight - labelHeight - viewportPadding); 1186 } 1187 1188 let left = rect.left; 1189 if (left < viewportPadding) left = viewportPadding; 1190 if (left + labelWidth > window.innerWidth - viewportPadding) { 1191 left = Math.max(viewportPadding, window.innerWidth - labelWidth - viewportPadding); 1192 } 1193 1194 labelEl.style.top = `${top}px`; 1195 labelEl.style.left = `${left}px`; 1196 }; 1197 1198 const onMouseMove = (e) => { 1199 if (e.target.closest('#bc-picker-bar')) return; 1200 const el = e.target.closest('[data-cid]'); 1201 const eligibleEl = el && eligibleCidElements.has(el) ? el : null; 1202 if (eligibleEl !== currentCidEl) { 1203 currentCidEl = eligibleEl; 1204 if (eligibleEl) { 1205 positionHighlight(eligibleEl); 1206 } else { 1207 highlight.style.display = 'none'; 1208 labelEl.style.display = 'none'; 1209 } 1210 } 1211 }; 1212 1213 const onScroll = () => { 1214 if (currentCidEl) positionHighlight(currentCidEl); 1215 }; 1216 1217 const onClick = (e) => { 1218 if (e.target.closest('#bc-picker-bar')) return; 1219 const el = e.target.closest('[data-cid]'); 1220 if (el && eligibleCidElements.has(el)) { 1221 e.preventDefault(); 1222 e.stopPropagation(); 1223 const cid = el.dataset.cid; 1224 const label = this.extractElementLabel(el); 1225 this.stopElementPickMode(true); 1226 this.onCidPicked(cid, label); 1227 } 1228 }; 1229 1230 const onKeyDown = (e) => { 1231 if (e.key === 'Escape') this.stopElementPickMode(false, previousPickerState); 1232 }; 1233 1234 bar.querySelector('#bc-picker-no-cid').addEventListener('click', () => { 1235 this.stopElementPickMode(true); 1236 this.copySupportWithoutCid(); 1237 }); 1238 bar.querySelector('#bc-picker-cancel').addEventListener('click', () => this.stopElementPickMode(false, previousPickerState)); 1239 document.addEventListener('mousemove', onMouseMove, true); 1240 document.addEventListener('scroll', onScroll, true); 1241 document.addEventListener('click', onClick, true); 1242 document.addEventListener('keydown', onKeyDown, true); 1243 1244 this._pickerHandlers = { onMouseMove, onScroll, onClick, onKeyDown }; 1245 } 1246 1247 stopElementPickMode(captured, previousState = null) { 1248 if (this._pickerHandlers) { 1249 document.removeEventListener('mousemove', this._pickerHandlers.onMouseMove, true); 1250 document.removeEventListener('scroll', this._pickerHandlers.onScroll, true); 1251 document.removeEventListener('click', this._pickerHandlers.onClick, true); 1252 document.removeEventListener('keydown', this._pickerHandlers.onKeyDown, true); 1253 this._pickerHandlers = null; 1254 } 1255 document.getElementById('bc-picker-highlight')?.remove(); 1256 document.getElementById('bc-picker-highlight-label')?.remove(); 1257 document.getElementById('bc-picker-bar')?.remove(); 1258 document.body.classList.remove('bc-pick-mode'); 1259 document.getElementById('bc-panel-container')?.classList.remove('bc-pick-mode-hidden'); 1260 this.getFrontendEditBridge()?.resume?.(); 1261 if (!captured) { 1262 if (previousState) { 1263 this.cidPickerState.state = previousState.state || 'idle'; 1264 this.cidPickerState.cid = previousState.cid || null; 1265 this.cidPickerState.label = previousState.label || null; 1266 } else { 1267 this.cidPickerState.state = 'idle'; 1268 } 1269 this.updatePickerButtonState(); 1270 } 1271 } 1272 1273 extractElementLabel(el) { 1274 const tag = el.tagName.toLowerCase(); 1275 const ariaLabel = el.getAttribute('aria-label'); 1276 if (ariaLabel) return `${tag}: "${ariaLabel.substring(0, 60)}"`; 1277 const heading = el.querySelector('h1, h2, h3, h4, h5, h6'); 1278 if (heading && heading.textContent.trim()) { 1279 return `${tag}: "${heading.textContent.trim().substring(0, 60)}"`; 1280 } 1281 const text = el.textContent.replace(/\s+/g, ' ').trim().substring(0, 60); 1282 if (text) return `${tag}: "${text}"`; 1283 return tag; 1284 } 1285 1286 onCidPicked(cid, label) { 1287 this.cidPickerState.state = 'selected'; 1288 this.cidPickerState.cid = cid; 1289 this.cidPickerState.label = label; 1290 this.supportFlowStarted = true; 1291 this.updatePickerButtonState(); 1292 } 1293 1294 copySupportWithoutCid() { 1295 this.cidPickerState.state = 'selected'; 1296 this.cidPickerState.cid = null; 1297 this.cidPickerState.label = null; 1298 this.supportFlowStarted = true; 1299 this.updatePickerButtonState(); 1300 } 1301 1302 updatePickerButtonState() { 1303 if (!this.container) return; 1304 1305 const pickBtn = this.container.querySelector('[data-action="pick-and-copy"], [data-action="reselect-element"]'); 1306 const composeBtn = this.container.querySelector('[data-action="compose-support"]'); 1307 const portalBtn = this.container.querySelector('[data-action="open-support"]'); 1308 const actionsEl = this.container.querySelector('.bc-support-actions'); 1309 const formStep = this.container.querySelector('.bc-support-describe'); 1310 const composeActions = this.container.querySelector('.bc-support-compose-actions'); 1311 const portalActions = this.container.querySelector('.bc-support-portal-actions'); 1312 const selected = this.cidPickerState.state === 'selected' || this.cidPickerState.state === 'copied'; 1313 const copied = this.cidPickerState.state === 'copied'; 1314 const draftValid = this.isSupportDraftValid(); 1315 const portalDone = this.isPortalStepDone(); 1316 const showComposeArea = draftValid || copied; 1317 1318 if (pickBtn) { 1319 pickBtn.dataset.action = this.cidPickerState.cid ? 'reselect-element' : 'pick-and-copy'; 1320 pickBtn.textContent = this.cidPickerState.cid ? 'Problemstelle ändern' : 'Problemstelle au
1320swählen'; 1321 pickBtn.classList.toggle('bc-btn-secondary', selected); 1322 } 1323 1324 const stepPick = this.container.querySelector('[data-step="pick"] .bc-support-checkitem'); 1325 const stepDescribe = this.container.querySelector('[data-step="describe"] .bc-support-checkitem'); 1326 const stepPortal = this.container.querySelector('[data-step="portal"] .bc-support-checkitem'); 1327 if (stepPick) stepPick.classList.toggle('is-checked', selected); 1328 if (stepDescribe) stepDescribe.classList.toggle('is-checked', draftValid); 1329 if (stepPortal) stepPortal.classList.toggle('is-checked', portalDone); 1330 1331 this.schedulePortalStepUpdate(); 1332 1333 if (formStep) { 1334 const wasHidden = formStep.getAttribute('aria-hidden') === 'true'; 1335 formStep.classList.toggle('is-active', selected); 1336 formStep.setAttribute('aria-hidden', selected ? 'false' : 'true'); 1337 if (selected && wasHidden) { 1338 const firstTA = formStep.querySelector('textarea'); 1339 if (firstTA) setTimeout(() => firstTA.focus(), 50); 1340 } 1341 } 1342 1343 if (composeActions) { 1344 composeActions.classList.toggle('is-active', showComposeArea); 1345 composeActions.setAttribute('aria-hidden', showComposeArea ? 'false' : 'true'); 1346 } 1347 1348 if (portalActions) { 1349 portalActions.classList.toggle('is-active', copied); 1350 portalActions.setAttribute('aria-hidden', copied ? 'false' : 'true'); 1351 } 1352 1353 if (composeBtn) { 1354 if (copied) { 1355 composeBtn.disabled = false; 1356 composeBtn.textContent = 'Zusammenfassung erneut kopieren'; 1357 composeBtn.classList.add('bc-btn-secondary'); 1358 } else { 1359 composeBtn.disabled = !draftValid; 1360 composeBtn.textContent = 'Zusammenfassung kopieren'; 1361 composeBtn.classList.remove('bc-btn-secondary'); 1362 } 1363 } 1364 1365 if (portalBtn) { 1366 if (copied) { 1367 portalBtn.removeAttribute('disabled'); 1368 portalBtn.classList.add('bc-cta-ready'); 1369 } else { 1370 portalBtn.setAttribute('disabled', 'disabled'); 1371 portalBtn.classList.remove('bc-cta-ready'); 1372 } 1373 } 1374 1375 if (actionsEl) { 1376 let note = actionsEl.querySelector('.bc-support-cid-note'); 1377 if (!note) { 1378 note = document.createElement('div'); 1379 note.className = 'bc-support-cid-note'; 1380 actionsEl.appendChild(note); 1381 } 1382 1383 if (copied) { 1384 if (this.cidPickerState.cid) { 1385 note.innerHTML = `Problemstelle: CID ${this.cidPickerState.cid}. <button type="button" class="bc-support-cid-change">Ãndern</button>`; 1386 note.querySelector('.bc-support-cid-change').addEventListener('click', () => this.handleAction('reselect-element')); 1387 } else { 1388 note.textContent = 'Zusammenfassung kopiert (ohne konkrete Elementauswahl).'; 1389 } 1390 } else if (selected) { 1391 note.textContent = this.cidPickerState.cid 1392 ? `Problemstelle ausgewählt: CID\u00a0${this.cidPickerState.cid}.` 1393 : ''; 1394 } else { 1395 note.textContent = ''; 1396 } 1397 } 1398 } 1399 1400 /** 1401 * UI rendern 1402 */ 1403 render(targetElement) { 1404 if (!targetElement) { 1405 console.warn('BrowserCheck: Kein Target-Element angegeben'); 1406 return; 1407 } 1408 1409 if (sessionStorage.getItem('fe_edit_enabled') === null) { 1410 sessionStorage.setItem('fe_edit_enabled', 'false'); 1411 } 1412 1413 const shadowHost = document.createElement('browser-check-panel'); 1414 1415 this.container = document.createElement('div'); 1416 this.container.className = 'bc-panel'; 1417 this.container.innerHTML = this.getHTML(); 1418 1419 targetElement.innerHTML = ''; 1420 shadowHost.shadowRoot.appendChild(this.container); 1421 targetElement.appendChild(shadowHost); 1422 this.shadowHost = shadowHost; 1423 this.attachEventListeners(); 1424 } 1425 1426 renderLoading(targetElement, statusText = 'Browser-Umgebung wird analysiert...') { 1427 if (!targetElement) return; 1428 1429 const shadowHost = document.createElement('browser-check-panel'); 1430 const loadingPanel = document.createElement('div'); 1431 loadingPanel.className = 'bc-panel bc-panel-loading'; 1432 loadingPanel.innerHTML = ` 1433 <div class="bc-header"> 1434 <h2><span class="bc-title-icon">${this.getIcon('search', 'bc-icon bc-icon-search')}</span><span>Website Hilfe</span></h2> 1435 </div> 1436 <div class="bc-loading"> 1437 <div class="bc-loading-title">Analyse läuft</div> 1438 <div class="bc-loading-text">${statusText}</div> 1439 <div class="bc-loading-bar" role="progressbar" aria-label="Browser Check läuft">
1440 <span class="bc-loading-bar-fill"></span> 1441 </div> 1442 </div> 1443 `; 1444 1445 targetElement.innerHTML = ''; 1446 shadowHost.shadowRoot.appendChild(loadingPanel); 1447 targetElement.appendChild(shadowHost); 1448 } 1449 1450 /** 1451 * HTML für die UI generieren 1452 */ 1453 getHTML() { 1454 const report = this.browserCheck.getReport(); 1455 const support = this.browserCheck.getSupportContext(); 1456 const currentYear = new Date().getFullYear(); 1457 this.supportFlowStarted = this.cidPickerState.state === 'selected' || this.cidPickerState.state === 'copied'; 1458 const selfHelpItems = this.browserCheck.getSupportSelfHelpItems(report); 1459 this.syncSupportSelfHelpState(selfHelpItems); 1460 const selfHelpComplete = this.isSupportSelfHelpComplete(selfHelpItems); 1461 const activeChecks = report.checks.filter(check => check.matched); 1462 const inactiveChecks = report.checks.filter(check => !check.matched); 1463 const hasWarnings = report.summary.warningCount > 0; 1464 const ampelClass = hasWarnings ? 'warning' : 'ok'; 1465 let revealIndex = 0; 1466 const nextRevealDelay = () => `${90 + (revealIndex++ * 85)}ms`; 1467 const quickChecks = report.quickChecks || []; 1468 1469 let html = ` 1470 <div class="bc-header"> 1471 <h2><span class="bc-title-icon">${this.getIcon('search', 'bc-icon bc-icon-search')}</span><span>Website Hilfe</span></h2> 1472 <div style="display: flex; gap: 8px; align-items: center;"> 1473 <button class="bc-toggle-fe-edit" data-action="toggle-fe-edit" aria-label="Frontend Edit umschalten" title="Frontend Edit aktivieren/deaktivieren" style="background: transparent; border: 1px solid var(--bc-line); border-radius: 16px; height: 34px; padding: 0 12px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 150ms ease; color: var(--bc-text-soft); font-size: 13px; font-weight: 500; gap: 6px;">${this.getIcon('pencil', 'bc-icon')} <span>Elemente bearbeiten</span></button> 1474 <button class="bc-close" data-action="close" aria-label="Website Hilfe schlieÃen">${this.getIcon('close', 'bc-icon bc-icon-close')}</button> 1475 </div> 1476 </div> 1477 1478 <div class="bc-summary"> 1479 <div class="bc-ampel bc-ampel-${ampelClass}"></div> 1480 <div class="bc-summary-text"> 1481 <strong>Diagnose-ID:</strong> ${report.reportId}<br> 1482 <strong>Ãberprüfte Einstellungen:</strong> ${report.summary.matched} von ${report.summary.totalChecks} problematisch 1483 </div> 1484 </div> 1485 `; 1486 1487 if (quickChecks.length > 0) { 1488 html += `<div class="bc-quickchecks">`; 1489 quickChecks.forEach(item => { 1490 const severity = item.severity || 'info'; 1491 const noteHtml = item.note ? `<small>${item.note}</small>` : ''; 1492 html += ` 1493 <div class="bc-quickcheck bc-quickcheck-${severity}" style="--bc-stagger-delay: ${nextRevealDelay()};"> 1494 <span class="bc-quickcheck-label">${item.label}</span> 1495 <span class="bc-quickcheck-value">${item.value}</span> 1496 ${noteHtml} 1497 </div> 1498 `; 1499 }); 1500 html += `</div>`; 1501 } 1502 1503 html += `<div class="bc-results">`; 1504 if (activeChecks.length === 0) { 1505 html += `<div class="bc-check bc-check-ok" style="--bc-stagger-delay: ${nextRevealDelay()};"><div class="bc-allok-inner">${this.getIcon('circle-check', 'bc-icon bc-allok-icon')}<div class="bc-check-message"><strong>Alles unauffällig.</strong><span class="bc-check-subtext">Der Browser und Betriebssystem greift nicht in die Darstellung ein.</span></div></div></div>`; 1506 } 1507 1508 activeChecks.forEach(check => { 1509 const matchClass = 'matched'; 1510 const severityClass = `bc-check-severity-${check.severity}`; 1511 const severityBadge = `<span class="bc-badge bc-severity-${check.severity}">${this.getSeverityLabel(check.severity)}</span>`; 1512 1513 let helpHtml = ''; 1514 if (check.matched && check.userHelp && check.userHelp.length > 0) { 1515 const stepsHtml = check.userHelp.map(entry => ` 1516 <div class="bc-help-os"> 1517 <span class="bc-help-os-label">${entry.os}</span> 1518 <ol class="bc-help-steps">${entry.steps.map(s => `<li>${s}</li>`).join('')}</ol> 1519 </div> 1520 `).join(''); 1521 helpHtml = `
1522 <details class="bc-help"> 1523 <summary class="bc-help-toggle">${this.getIcon('chevron-right', 'bc-icon bc-icon-chevron')}<span>Wie kann ich das deaktivieren?</span></summary> 1524 <div class="bc-help-body">${stepsHtml}</div> 1525 </details> 1526 `; 1527 } 1528 1529 html += ` 1530 <div class="bc-check bc-check-${matchClass} ${severityClass}" style="--bc-stagger-delay: ${nextRevealDelay()};"> 1531 <div class="bc-check-header"> 1532 <strong>${check.title}</strong> 1533 ${check.matched ? severityBadge : ''} 1534 </div> 1535 ${check.matched && check.userMessage ? `<div class="bc-check-message">${check.userMessage}</div>` : ''} 1536 ${check.matched && check.userImpact ? `<div class="bc-check-impact">${check.userImpact}</div>` : ''} 1537 ${helpHtml} 1538 </div> 1539 `; 1540 }); 1541 1542 if (inactiveChecks.length > 0) { 1543 html += ` 1544 <details class="bc-ok-group" style="--bc-stagger-delay: ${nextRevealDelay()};"> 1545 <summary class="bc-ok-group-toggle">${this.getIcon('chevron-right', 'bc-icon bc-icon-chevron')}<span>Alles ok (${inactiveChecks.length})</span></summary> 1546 <ul class="bc-ok-list"> 1547 ${inactiveChecks.map(check => `<li><span class="bc-ok-title">${check.title}</span> <span class="bc-ok-meta">nicht aktiv</span></li>`).join('')} 1548 </ul> 1549 </details> 1550 `; 1551 } 1552 1553 html += `</div>`; 1554 const sysInfo = report.systemProfile || this.browserCheck.getSystemInfo(); 1555 const hasZoomWarning = this.browserCheck.getZoomStatus(sysInfo.zoom).severity !== 'ok'; 1556 const zoomWarningHtml = hasZoomWarning ? `<div class="bc-device-warning"><strong>Hinweis:</strong> Zoom erkannt (${sysInfo.zoom}%). Das kann die Darstellung beeinflussen.</div>` : ''; 1557 const touchLabel = sysInfo.touchCapable ? 'Ja' : 'Nein'; 1558 const dprLabel = this.formatNumber(sysInfo.dpr); 1559 const zoomLabel = this.formatNumber(sysInfo.zoom); 1560 const maxTouchPointsLabel = this.formatNumber(sysInfo.maxTouchPoints); 1561 const rootFontNumeric = parseFloat(sysInfo.rootFontSize); 1562 const rootFontLabel = Number.isFinite(rootFontNumeric) 1563 ? `${this.formatNumber(rootFontNumeric)}px` 1564 : sysInfo.rootFontSize; 1565 const timeLabel = `${sysInfo.timeLabel} <small>(${sysInfo.timezone})</small>`; 1566 let infoCardIndex = 0; 1567 const nextInfoDelay = () => `${620 + (infoCardIndex++ * 70)}ms`; 1568 const renderInfoCard = (key, value, extraClass = '') => ` 1569 <div class="bc-info-card ${extraClass}" style="--bc-stagger-delay: ${nextInfoDelay()};"> 1570 <span class="bc-info-key">${key}</span> 1571 <span class="bc-info-value">${value}</span> 1572 </div> 1573 `; 1574 const bodyClassesLabel = sysInfo.bodyBrowserClasses.length > 0 1575 ? sysInfo.bodyBrowserClasses.map(cls => `<span class="bc-chip">${cls}</span>`).join('') 1576 : '<span class="bc-chip bc-chip-muted">Keine browser-* Klasse erkannt</span>'; 1577 1578 const technicalDetailsHtml = ` 1579 <details class="bc-device-group" style="--bc-stagger-delay: ${nextRevealDelay()};"> 1580 <summary class="bc-device-group-toggle">${this.getIcon('chevron-right', 'bc-icon bc-icon-chevron')}<span>Systemprofil</span><small>Technische Rohdaten und Browserumgebung</small></summary> 1581 <div class="bc-device-info"> 1582 ${zoomWarningHtml} 1583 1584 <div class="bc-device-grid"> 1585 ${renderInfoCard('Browser', `${sysInfo.browserName} ${sysInfo.browserVersion}`)} 1586 ${renderInfoCard('OS', `${this.browserCheck.formatOsLabel(sysInfo)}`)} 1587 ${renderInfoCard('Viewport', `${sysInfo.width} x ${sysInfo.height}px`)} 1588 ${renderInfoCard('Zoom', `${zoomLabel}% <small>(${this.getZoomMethodLabel(sysInfo.zoomMethod)})</small>`, hasZoomWarning ? 'is-warning' : '')} 1589 ${renderInfoCard('DPR', dprLabel)} 1590 ${renderInfoCard('Default Font', rootFontLabel)} 1591 ${renderInfoCard('Touch', `${touchLabel} (${maxTouchPointsLabel})`)} 1592 ${renderInfoCard('Sprache', sysInfo.language)} 1593 ${renderInfoCard('Land', sysInfo.country)} 1594 ${renderInfoCard('Zeit', timeLabel)} 1595 </div> 1596 1597 <div class="bc-device-classes">
1598 <span class="bc-device-label">Body-Klassen:</span> 1599 <div class="bc-chip-list">${bodyClassesLabel}</div> 1600 </div> 1601 </div> 1602 </details> 1603 1604 <div class="bc-device-useragent-block" style="--bc-stagger-delay: ${nextRevealDelay()};"> 1605 <div class="bc-device-useragent"> 1606 <span class="bc-device-label">User Agent:</span> 1607 <code>${sysInfo.userAgent}</code> 1608 </div> 1609 </div> 1610 `; 1611 1612 const supportStatusClass = support.isPhoneAvailable ? 'is-open' : 'is-closed'; 1613 const supportStatusText = support.isPhoneAvailable 1614 ? `Notfall? Unsere Hotline ist jetzt erreichbar (${support.supportHoursLabel}).` 1615 : support.isHoliday 1616 ? `Unsere Hotline ist heute wegen Feiertag (${support.holidayName}) nicht besetzt (${support.supportHoursLabel}).` 1617 : `Unsere Hotline ist aktuell nicht besetzt (${support.supportHoursLabel}).`; 1618 const supportSubText = support.isPhoneAvailable 1619 ? `Telefon: ${support.supportPhone}` 1620 : `Telefon: ${support.supportPhone} · Tickets können Sie rund um die Uhr im Support-Portal anlegen.`; 1621 const supportFlowStarted = this.supportFlowStarted; 1622 const selfHelpHtml = selfHelpItems.map(item => ` 1623 <li> 1624 <button type="button" class="bc-support-checkitem ${this.supportSelfHelpState[item.key] ? 'is-checked' : ''}" data-selfhelp-key="${item.key}" role="checkbox" aria-checked="${this.supportSelfHelpState[item.key] ? 'true' : 'false'}"> 1625 <span class="bc-support-checkcontrol" aria-hidden="true"></span> 1626 <span class="bc-support-checktext"><strong>${item.title}</strong><small>${item.text}</small></span> 1627 </button> 1628 </li> 1629 `).join(''); 1630 1631 html += ` 1632 <section class="bc-support-guide ${supportFlowStarted ? 'is-started' : ''}" style="--bc-stagger-delay: ${nextRevealDelay()};"> 1633 <h3>Benötigen Sie Hilfe?</h3> 1634 <p class="bc-support-intro">Helfen Sie uns, Ihnen zu helfen. Prüfen Sie zuerst kurz die Selbsthilfe und nutzen Sie anschlieÃend bevorzugt unser Support-Portal.</p> 1635 1636 <div class="bc-support-start ${supportFlowStarted ? 'is-hidden' : ''}" aria-hidden="${supportFlowStarted ? 'true' : 'false'}"> 1637 <strong>Supportvorgang starten</strong> 1638 <p>Ãffnen Sie den geführten Ablauf mit Checkliste und Ticket-Ãbergabe.</p> 1639 <button class="bc-btn" data-action="start-support-flow">Supportvorgang starten</button> 1640 </div> 1641 1642 <div class="bc-support-flow ${supportFlowStarted ? 'is-visible' : ''}" aria-hidden="${supportFlowStarted ? 'false' : 'true'}"> 1643 1644 <div class="bc-support-stage ${selfHelpComplete ? 'is-unlocked' : ''}"> 1645 <div class="bc-support-selfhelp bc-support-selfhelp-card ${selfHelpComplete ? 'is-leaving' : ''}"> 1646 <div class="bc-support-selfhelp-title">Schnelle Hilfe zur Selbsthilfe</div> 1647 <div class="bc-support-selfhelp-note ${selfHelpComplete ? 'is-complete' : ''}">${selfHelpComplete ? 'Alles geprüft. Die nächsten Support-Schritte sind jetzt freigeschaltet.' : `Bitte prüfen Sie zuerst alle offenen Punkte und haken Sie diese nach Ihrem Test ab. Noch ${selfHelpItems.length} offen.`}</div> 1648 <ul class="bc-support-selfhelp-list" aria-label="Selbsthilfe-Checkliste"> 1649 ${selfHelpHtml} 1650 </ul> 1651 </div> 1652 1653 <div class="bc-support-nextstep-card ${selfHelpComplete ? 'is-visible' : ''}" aria-hidden="${selfHelpComplete ? 'false' : 'true'}"> 1654 1655 1656 <ol class="bc-support-steps" aria-label="Support in drei Schritten"> 1657 <li data-step="pick"> 1658 <div class="bc-support-checkitem bc-support-stepitem" role="presentation"> 1659 <span class="bc-support-checkcontrol" aria-hidden="true"></span> 1660 <span class="bc-support-checktext"><strong>Problemstelle auswählen</strong><small>Markieren Sie das betroffene Element auf der Seite (optional auch ohne Element).</small></span> 1661 </div> 1662 </li> 1663 <li data-step="describe"> 1664 <div class="bc-support-checkitem bc-support-stepitem" role="presentation">
1665 <span class="bc-support-checkcontrol" aria-hidden="true"></span> 1666 <span class="bc-support-checktext"><strong>Problem kurz beschreiben</strong><small>Füllen Sie Beobachtung und Erwartung aus, damit der Support direkt starten kann.</small></span> 1667 </div> 1668 </li> 1669 <li data-step="portal"> 1670 <div class="bc-support-checkitem bc-support-stepitem" role="presentation"> 1671 <span class="bc-support-checkcontrol" aria-hidden="true"></span> 1672 <span class="bc-support-checktext"><strong>Ticket im Portal anlegen</strong><small>Fügen Sie den kopierten Text ins Feld "Ausführliche Beschreibung" ein.</small></span> 1673 </div> 1674 </li> 1675 </ol> 1676 1677 <div class="bc-support-actions"> 1678 <button class="bc-btn" data-action="pick-and-copy">Problemstelle auswählen</button> 1679 </div> 1680 1681 <div class="bc-support-describe" aria-hidden="true"> 1682 <label class="bc-support-field"> 1683 <span class="bc-support-field-header"> 1684 <span>Beobachtung / was genau ist hier problematisch?</span> 1685 ${(() => { const n = this.getDraftValue('observation').length; return `<span class="bc-support-field-counter${n >= 30 ? ' is-valid' : ''}" data-counter-for="observation">${n >= 30 ? n + '\u2009\u2713' : n + '\u00a0/\u00a030'}</span>`; })()} 1686 </span> 1687 <textarea data-support-field="observation" rows="3" placeholder="Zum Beispiel: Das gepflegte Bild an der Stelle ist unscharf. \nOder: Der Text an der Stelle sieht sehr groà aus, das passt nicht gut ins Gesamtbild der Seite.">${this.escapeHtml(this.supportDraft.observation)}</textarea> 1688 </label> 1689 <label class="bc-support-field"> 1690 <span class="bc-support-field-header"> 1691 <span>Erwartet haben Sie aber:</span> 1692 ${(() => { const n = this.getDraftValue('expected').length; return `<span class="bc-support-field-counter${n >= 30 ? ' is-valid' : ''}" data-counter-for="expected">${n >= 30 ? n + '\u2009\u2713' : n + '\u00a0/\u00a030'}</span>`; })()} 1693 </span> 1694 <textarea data-support-field="expected" rows="3" placeholder="Zum Beispiel: Das Bild sollte scharf sein. \nOder: Der Text sollte etwas kleiner als die normale SchriftgröÃe sein.">${this.escapeHtml(this.supportDraft.expected)}</textarea> 1695 </label> 1696 </div> 1697 1698 <div class="bc-support-compose-actions" aria-hidden="true"> 1699 <button class="bc-btn bc-btn-secondary" data-action="compose-support">Zusammenfassung kopieren</button> 1700 </div> 1701 1702 <div class="bc-support-portal-actions" aria-hidden="true"> 1703 <strong>Ticket anlegen</strong> 1704 <p>Sie können jetzt zu HubSpot wechseln und dort ein neues Ticket anlegen. Fügen Sie die kopierte Zusammenfassung in das Feld âAusführliche Beschreibung" ein.</p> 1705 <button class="bc-btn" data-action="open-support" disabled>Ticket erstellen</button> 1706 </div> 1707 1708 <div class="bc-support-status ${supportStatusClass}"> 1709 ${this.getIcon('info', 'bc-icon bc-support-status-icon')} 1710 <div> 1711 <strong>${supportStatusText}</strong> 1712 <small>${supportSubText}</small> 1713 </div> 1714 </div> 1715 </div> 1716 </div> 1717 </div> 1718 </section> 1719 `; 1720 1721 html += technicalDetailsHtml; 1722 1723 html += ` 1724 <div class="bc-actions"> 1725 <button class="bc-btn" data-action="copy-text">Technischen Bericht kopieren</button> 1726 <button class="bc-btn bc-btn-secondary" data-action="copy-json">JSON kopieren</button> 1727 </div> 1728 1729 <footer class="bc-footer"> 1730 <div class="bc-footer-copy">© ${currentYear} vioma GmbH</div> 1731 <picture class="bc-footer-logo"> 1732 <source media="(prefers-color-scheme: dark)" srcset="https://www.vioma.de/media/87043/vioma-logo-vec-white-rgb-3016109.svg"> 1733 <img src="https://www.vioma.de/media/87043/vioma-logo-vec-black-rgb-3016107.svg" alt="vioma GmbH" width="112" height="28"> 1734 </picture> 1735 </footer> 1736 `; 1737 1738 return html; 1739 } 1740 1741 1742 toggleFrontendEdit() { 1743 const isEnabled = sessionStorage.getItem('fe_edit_enabled') === 'true'; 1744 sessionStorage.setItem('fe_edit_enabled', !isEnabled ? 'true' : 'false'); 1745 1746 this.updateFrontendEditButtonState(); 1747 1748 if (window.__condeonAssist?.frontendEdit) { 1749 if (isEnabled) { 1750 window.__condeonAssist.frontendEdit.disable?.(); 1751 } else { 1752 window.__condeonAssist.frontendEdit.enable?.(); 1753 } 1754 } 1755 } 1756 1757 updateFrontendEditButtonState() { 1758 const toggleBtn = this.container?.querySelector('[data-action="toggle-fe-edit"]'); 1759 if (toggleBtn) { 1760 const isEnabled = sessionStorage.getItem('fe_edit_enabled') === 'true'; 1761 toggleBtn.style.borderColor = isEnabled ? 'var(--bc-accent)' : 'var(--bc-line)'; 1762 toggleBtn.style.color = isEnabled ? 'var(--bc-accent)' : 'var(--bc-text-soft)'; 1763 toggleBtn.style.backgroundColor = isEnabled ? 'rgba(33, 150, 243, 0.12)' : 'transparent'; 1764 } 1765 } 1766 1767 attachEventListeners() { 1768 this.updateFrontendEditButtonState(); 1769 1770 const buttons = this.container.querySelectorAll('[data-action]'); 1771 buttons.forEach(btn => { 1772 btn.addEventListener('click', (e) => { 1773 const action = e.currentTarget.dataset.action; 1774 this.handleAction(action); 1775 }); 1776 }); 1777 1778 const pickStepItem = this.container.querySelector('[data-step="pick"]'); 1779 if (pickStepItem) { 1780 pickStepItem.addEventListener('click', () => { 1781 const action = this.cidPickerState.cid ? 'reselect-element' : 'pick-and-copy'; 1782 this.handleAction(action); 1783 }); 1784 1785 pickStepItem.addEventListener('touchend', (e) => { 1786 e.preventDefault(); 1787 const action = this.cidPickerState.cid ? 'reselect-element' : 'pick-and-copy'; 1788 this.handleAction(action); 1789 }, { passive: false }); 1790 } 1791 1792 const describeStepItem = this.container.querySelector('[data-step="describe"]'); 1793 if (describeStepItem) { 1794 describeStepItem.addEventListener('click', () => { 1795 this.focusFirstIncompleteSupportField(); 1796 }); 1797 1798 describeStepItem.addEventListener('touchend', (e) => { 1799 e.preventDefault(); 1800 this.focusFirstIncompleteSupportField(); 1801 }, { passive: false }); 1802 } 1803 1804 const selfHelpButtons = this.container.querySelectorAll('[data-selfhelp-key]'); 1805 selfHelpButtons.forEach(button => { 1806 button.addEventListener('click', (e) => { 1807 this.toggleSupportSelfHelpItem(e.currentTarget); 1808 }); 1809 }); 1810 1811 const draftFields = this.container.querySelectorAll('[data-support-field]'); 1812 draftFields.forEach(field => { 1813 field.addEventListener('input', (e) => { 1814 this.updateSupportDraftField(e.currentTarget.dataset.supportField, e.currentTarget.value); 1815 }); 1816 }); 1817 1818 this.updateSupportUnlockState(); 1819 this.updatePickerButtonState(); 1820 } 1821 1822 /** 1823 * Aktionen handhaben 1824 */ 1825 handleAction(action) { 1826 switch (action) { 1827 case 'close': 1828 if (!this.container || this.container.classList.contains('is-closing')) break; 1829 1830 this.container.classList.add('is-closing'); 1831 if (this.closeTimeoutId) window.clearTimeout(this.closeTimeoutId); 1832 1833 // Sofort: Container raussliden + Badge wieder zeigen 1834 if (typeof this.onClose === 'function') this.onClose(); 1835 1836 // Inhalt still im Hintergrund leeren, nachdem Animation abgeklungen ist 1837 this.closeTimeoutId = window.setTimeout(() => { 1838 if (this.container) this.container.innerHTML = ''; 1839 this.closeTimeoutId = null; 1840 }, 500); 1841 break; 1842 case 'toggle-fe-edit': 1843 this.toggleFrontendEdit(); 1844 break; 1845 case 'copy-text': 1846 this.copyToClipboard(this.browserCheck.getTextReport(), 'Text-Report kopiert!'); 1847 break; 1848 case 'copy-json': 1849 this.copyToClipboard(this.browserCheck.getJsonReport(), 'JSON kopiert!'); 1850 break; 1851 case 'copy-support':
1852 this.copyToClipboard(this.browserCheck.getSupportTicketText(), 'Support-Zusammenfassung kopiert!'); 1853 break; 1854 case 'pick-and-copy': 1855 this.startElementPickMode(); 1856 break; 1857 case 'recopy-support': { 1858 const textToRecopy = this.cidPickerState.lastSupportText || ''; 1859 if (textToRecopy) { 1860 navigator.clipboard.writeText(textToRecopy).catch(err => console.error('Fehler beim Kopieren:', err)); 1861 } 1862 break; 1863 } 1864 case 'reselect-element': 1865 this.startElementPickMode(); 1866 break; 1867 case 'open-support': 1868 this.markPortalStepStarted(); 1869 window.open('https://support.vioma.de/portal/new', '_blank', 'noopener'); 1870 break; 1871 case 'compose-support': 1872 this.composeSupportSummary(); 1873 break; 1874 case 'start-support-flow': 1875 this.startSupportFlow(); 1876 break; 1877 } 1878 } 1879 1880 /** 1881 * In Zwischenablage kopieren 1882 */ 1883 copyToClipboard(text, message) { 1884 navigator.clipboard.writeText(text).then(() => { 1885 alert(message); 1886 }).catch(err => { 1887 console.error('Fehler beim Kopieren:', err); 1888 }); 1889 } 1890} 1891 1892/** 1893 * Custom Element für Shadow DOM Isolation des Panels 1894 * Verhindert, dass Template-CSS (z. B. :is(h1,h2,...) { color: ... }) in den Panel-Inhalt eindringt. 1895 */ 1896class BrowserCheckPanel extends HTMLElement { 1897 constructor() { 1898 super(); 1899 const shadow = this.attachShadow({ mode: 'open' }); 1900 1901 // Box-sizing-Reset + no-motion-Suppressor für Shadow-Inhalte 1902 const style = document.createElement('style'); 1903 style.textContent = [ 1904 '* { box-sizing: border-box; }', 1905 ':host(.bc-no-motion), :host(.bc-no-motion) * {', 1906 ' transition-duration: 0s !important;', 1907 ' animation-duration: 0s !important;', 1908 ' animation-delay: 0s !important;', 1909 '}', 1910 ].join('\n'); 1911 shadow.appendChild(style); 1912 1913 const fontLink = document.createElement('link'); 1914 fontLink.rel = 'stylesheet'; 1915 fontLink.href = BROWSER_CHECK_FONT_HREF; 1916 fontLink.setAttribute('crossorigin', 'anonymous'); 1917 shadow.appendChild(fontLink); 1918 1919 const cssLink = document.createElement('link'); 1920 cssLink.rel = 'stylesheet'; 1921 cssLink.href = BROWSER_CHECK_CSS_HREF; 1922 shadow.appendChild(cssLink); 1923 } 1924} 1925 1926if (!customElements.get('browser-check-panel')) { 1927 customElements.define('browser-check-panel', BrowserCheckPanel); 1928} 1929 1930/** 1931 * Initialisierung 1932 */ 1933function ensureFrontendEditLoaded() { 1934 if (window.__condeonAssist?.frontendEdit) return Promise.resolve(); 1935 return new Promise((resolve) => { 1936 const script = document.createElement('script'); 1937 script.src = '/(cms)/module/static/default/content/libs/js/nst/frontend_edit.js'; 1938 script.onload = resolve; 1939 script.onerror = resolve; // auch bei Fehler nicht blockieren 1940 document.head.appendChild(script); 1941 }); 1942} 1943 1944async function initBrowserCheck() { 1945 // Browser-Check defaults Frontend Edit to disabled on first run. 1946 // This must happen before frontend_edit.js is loaded. 1947 if (sessionStorage.getItem('fe_edit_enabled') === null) { 1948 sessionStorage.setItem('fe_edit_enabled', 'false'); 1949 } 1950 1951 await ensureFrontendEditLoaded(); 1952 1953 const panel = document.createElement('div'); 1954 panel.id = 'bc-panel-container'; 1955 document.body.appendChild(panel); 1956 1957 const helpBadge = document.createElement('button'); 1958 helpBadge.id = 'bc-help-badge'; 1959 helpBadge.type = 'button'; 1960 helpBadge.setAttribute('aria-label', 'Website Hilfe öffnen'); 1961 helpBadge.textContent = 'Website Hilfe'; 1962 document.body.appendChild(helpBadge); 1963 1964 const check = new BrowserCheck(); 1965 const ui = new BrowserCheckUI(check); 1966 check.registerChecks(); 1967 await check.runAllChecks(); 1968 1969 const openOverlay = async () => { 1970 // Inhalt off-screen rendern (Container liegt noch bei translateX(100%)) 1971 ui.render(panel); 1972 helpBadge.classList.add('is-hidden'); 1973 1974 // Warten bis Shadow-DOM-CSS geladen, damit kein FOUC beim Slide-in sichtbar ist 1975 await new Promise(resolve => { 1976 const cssLink = ui.shadowHost?.shadowRoot.querySelector('link[href*="browser-check"]'); 1977 if (!cssLink || cssLink.sheet) { 1978 resolve(); 1979 } else { 1980 cssLink.addEventListener('load', resolve, { once: true }); 1981 setTimeout(resolve, 100); // Fallback 1982 } 1983 }); 1984 1985 panel.classList.add('is-open'); 1986 }; 1987 1988 const closeOverlayToBadge = () => { 1989 panel.classList.remove('is-open'); 1990 helpBadge.classList.remove('is-hidden'); 1991 }; 1992 1993 helpBadge.addEventListener('click', openOverlay); 1994 ui.onClose = closeOverlayToBadge; 1995 1996 window.__browserCheck = check; 1997 window.__browserCheckUI = ui; 1998 1999 console.log('BrowserCheck initialisiert. Report verfügbar unter window.__browserCheck'); 2000} 2001 2002ensureBrowserCheckCssLoaded(); 2003 2004if (document.readyState === 'loading') { 2005 document.addEventListener('DOMContentLoaded', initBrowserCheck); 2006} else { 2007 initBrowserCheck(); 2008}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.