Fehler melden mit Screenshot und Markierung, Sammlung für Bearbeiter
Käfer-Button unten rechts: nimmt den sichtbaren Bildschirm mit html2canvas (selbst gehostet) auf, Vorschau mit roten Rechtecken, Beschreibung, Name optional, Senden ohne Anmeldung. Aus offenen Dialogen heraus wird der Dialog selbst aufgenommen. Bearbeiter sehen die Meldungen unter „Fehler" mit Zähler, kopieren einen fertigen Claude-Prompt und den Screenshot. Serverseitig: Pfade nur in exakter Schreibweise, /api standardmäßig nur für Bearbeiter mit ausdrücklicher Freigabeliste, Bremse zählt jeden Versuch vor dem Lesen großer Bodies, X-Forwarded-For nur von vertrauten Proxys, Nutzertext im Prompt eingezäunt. HTTP-Tests für die Sperren. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
6a3c68aaac
commit
4ec23ccd25
15 files changed
+877
-36
No files matched your search
+322
-2
@@ -51,7 +51,7 @@ async function api(path, { method = 'GET', body, raw } = {}) {
|
||||
}
|
||||
|
||||
// ---------- Anmeldung zum Bearbeiten ----------
|
||||
const EDITOR_VIEWS = ['personen', 'abwesenheiten', 'anfragen', 'einstellungen'];
|
||||
const EDITOR_VIEWS = ['personen', 'abwesenheiten', 'anfragen', 'fehler', 'einstellungen'];
|
||||
function isEditor() { return !state.data || state.data.auth?.editor !== false; }
|
||||
function applyAuthUI() {
|
||||
const a = state.data?.auth || { protected: false, editor: true };
|
||||
@@ -67,6 +67,10 @@ function applyAuthUI() {
|
||||
const badge = $('#requests-count');
|
||||
badge.hidden = !a.editor || open === 0;
|
||||
badge.textContent = open;
|
||||
const openBugs = (state.data?.bugs || []).filter((b) => b.status === 'offen').length;
|
||||
const bugBadge = $('#bugs-count');
|
||||
bugBadge.hidden = !a.editor || openBugs === 0;
|
||||
bugBadge.textContent = openBugs;
|
||||
if (!a.editor && EDITOR_VIEWS.includes(location.hash.slice(1))) location.hash = '#kalender';
|
||||
}
|
||||
function openLogin() {
|
||||
@@ -144,7 +148,7 @@ function avatarEl(p, cls = '') {
|
||||
|
||||
// ---------- Navigation ----------
|
||||
function showView(name) {
|
||||
const known = ['kalender', 'personen', 'abwesenheiten', 'anfragen', 'einstellungen'];
|
||||
const known = ['kalender', 'personen', 'abwesenheiten', 'anfragen', 'fehler', 'einstellungen'];
|
||||
if (!known.includes(name)) name = 'kalender';
|
||||
if (!isEditor() && EDITOR_VIEWS.includes(name)) name = 'kalender';
|
||||
if (name === 'abwesenheiten' && state.data) prepareAbsenceImport();
|
||||
@@ -1068,6 +1072,7 @@ function renderSettings() {
|
||||
f.historyWindowDays.value = s.historyWindowDays;
|
||||
f.calendarDisplayName.value = s.calendarDisplayName;
|
||||
f.inviteAttendees.checked = Boolean(s.inviteAttendees);
|
||||
f.bugPromptTemplate.value = s.bugPromptTemplate || '';
|
||||
if (!$('#holiday-generate').elements.year.value) $('#holiday-generate').elements.year.value = new Date().getFullYear();
|
||||
|
||||
const list = $('#holiday-list');
|
||||
@@ -1111,6 +1116,7 @@ settingsForm.addEventListener('submit', async (e) => {
|
||||
await api('api/settings', { method: 'PUT', body: {
|
||||
meetingTime: f.meetingTime.value, durationMinutes: Number(f.durationMinutes.value), bundesland: f.bundesland.value,
|
||||
historyWindowDays: Number(f.historyWindowDays.value), calendarDisplayName: f.calendarDisplayName.value, inviteAttendees: f.inviteAttendees.checked,
|
||||
bugPromptTemplate: f.bugPromptTemplate.value,
|
||||
} });
|
||||
await load();
|
||||
toast('Einstellungen gespeichert.');
|
||||
@@ -1134,6 +1140,319 @@ $('#holiday-add').addEventListener('submit', async (e) => {
|
||||
} catch (err) { toast(err.message, true); }
|
||||
});
|
||||
|
||||
// ---------- Fehler melden (für alle) ----------
|
||||
const bugPanel = $('#bug-panel');
|
||||
const bugCanvas = $('#bug-canvas');
|
||||
const bugState = { base: null, rects: [], draft: null, sending: false, capture: null, closeTimer: null, target: null };
|
||||
|
||||
function viewName() {
|
||||
const v = location.hash.slice(1) || 'kalender';
|
||||
const names = { kalender: 'Kalender', personen: 'Personen', abwesenheiten: 'Abwesenheiten', anfragen: 'Anfragen', fehler: 'Fehler', einstellungen: 'Einstellungen' };
|
||||
const month = v === 'kalender' ? ` (${MONTHS[state.month.getMonth()]} ${state.month.getFullYear()})` : '';
|
||||
return `${names[v] || v}${month}${isEditor() ? ', angemeldet' : ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sichtbaren Bildschirm als Canvas aufnehmen – das Meldefenster selbst bleibt außen vor.
|
||||
* html2canvas zeichnet die Seite aus dem DOM nach. Zwei Dinge kann es nicht: position: sticky (die
|
||||
* Kopfzeile) und box-shadow-Ringe (sie übermalen das Kennzeichen). Beides wird in der Kopie des
|
||||
* Dokuments korrigiert, die html2canvas rendert – die echte Seite bleibt unberührt.
|
||||
*/
|
||||
async function captureScreenshot(target = null) {
|
||||
if (typeof window.html2canvas !== 'function') throw new Error('Die Screenshot-Funktion ist nicht geladen.');
|
||||
// Auflösung begrenzen: höchstens 1,5-fach und höchstens 2560 px breit, damit das Bild unter 6 MB bleibt
|
||||
const scale = Math.min(window.devicePixelRatio || 1, 1.5, Math.max(1, 2560 / window.innerWidth));
|
||||
// Schatten malt html2canvas als Fläche über das Element – in der Kopie weglassen
|
||||
const extraStyle = '.slot .badge, .req-mark, dialog, .chip, .card { box-shadow: none; } .logo .arc { animation: none; }';
|
||||
if (target) {
|
||||
// Ein offener Dialog: nur ihn aufnehmen, er ist das, was der Melder gerade sieht
|
||||
return window.html2canvas(target, {
|
||||
backgroundColor: '#ffffff',
|
||||
scale,
|
||||
logging: false,
|
||||
useCORS: true,
|
||||
ignoreElements: (el) => el.id === 'bug-widget' || el.id === 'toast' || el.classList?.contains('dialog-bug'),
|
||||
onclone: (doc) => { const style = doc.createElement('style'); style.textContent = extraStyle; doc.head.appendChild(style); },
|
||||
});
|
||||
}
|
||||
const scrollX = window.scrollX;
|
||||
const scrollY = window.scrollY;
|
||||
const topbarHeight = $('.topbar').offsetHeight;
|
||||
return window.html2canvas(document.documentElement, {
|
||||
backgroundColor: '#ffffff',
|
||||
scale,
|
||||
logging: false,
|
||||
useCORS: true,
|
||||
ignoreElements: (el) => el.id === 'bug-widget' || el.id === 'toast',
|
||||
x: scrollX,
|
||||
y: scrollY,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
windowWidth: document.documentElement.clientWidth,
|
||||
windowHeight: document.documentElement.clientHeight,
|
||||
onclone: (doc) => {
|
||||
const bar = doc.querySelector('.topbar');
|
||||
if (bar) {
|
||||
// Kopfzeile dorthin setzen, wo sie gerade klebt; der freigewordene Platz oben wird aufgefüllt
|
||||
bar.style.position = 'absolute';
|
||||
bar.style.top = `${scrollY}px`;
|
||||
bar.style.left = '0';
|
||||
bar.style.right = '0';
|
||||
doc.body.style.paddingTop = `${topbarHeight}px`;
|
||||
}
|
||||
const style = doc.createElement('style');
|
||||
style.textContent = extraStyle;
|
||||
doc.head.appendChild(style);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function drawBugCanvas() {
|
||||
const c = bugCanvas;
|
||||
if (!bugState.base) return;
|
||||
const ctx = c.getContext('2d');
|
||||
ctx.clearRect(0, 0, c.width, c.height);
|
||||
ctx.drawImage(bugState.base, 0, 0);
|
||||
const lw = Math.max(3, Math.round(c.width / 400));
|
||||
ctx.lineWidth = lw;
|
||||
ctx.strokeStyle = '#B42318';
|
||||
ctx.lineJoin = 'miter';
|
||||
for (const r of [...bugState.rects, ...(bugState.draft ? [bugState.draft] : [])]) {
|
||||
ctx.strokeRect(r.x + lw / 2, r.y + lw / 2, Math.max(0, r.w - lw), Math.max(0, r.h - lw));
|
||||
}
|
||||
$('#bug-clear-marks').hidden = bugState.rects.length === 0;
|
||||
}
|
||||
|
||||
function canvasPoint(e) {
|
||||
const rect = bugCanvas.getBoundingClientRect();
|
||||
const x = Math.min(Math.max(0, (e.clientX - rect.left) / rect.width * bugCanvas.width), bugCanvas.width);
|
||||
const y = Math.min(Math.max(0, (e.clientY - rect.top) / rect.height * bugCanvas.height), bugCanvas.height);
|
||||
return { x, y };
|
||||
}
|
||||
let bugDrawStart = null;
|
||||
bugCanvas.addEventListener('pointerdown', (e) => {
|
||||
if (!bugState.base) return;
|
||||
e.preventDefault();
|
||||
bugCanvas.setPointerCapture(e.pointerId);
|
||||
bugDrawStart = canvasPoint(e);
|
||||
bugState.draft = { x: bugDrawStart.x, y: bugDrawStart.y, w: 0, h: 0 };
|
||||
});
|
||||
bugCanvas.addEventListener('pointermove', (e) => {
|
||||
if (!bugDrawStart) return;
|
||||
const p = canvasPoint(e);
|
||||
bugState.draft = { x: Math.min(p.x, bugDrawStart.x), y: Math.min(p.y, bugDrawStart.y), w: Math.abs(p.x - bugDrawStart.x), h: Math.abs(p.y - bugDrawStart.y) };
|
||||
drawBugCanvas();
|
||||
});
|
||||
const endBugDraw = () => {
|
||||
if (!bugDrawStart) return;
|
||||
const d = bugState.draft;
|
||||
const min = bugCanvas.width / 150;
|
||||
if (d && d.w > min && d.h > min) bugState.rects.push(d);
|
||||
bugState.draft = null;
|
||||
bugDrawStart = null;
|
||||
drawBugCanvas();
|
||||
};
|
||||
bugCanvas.addEventListener('pointerup', endBugDraw);
|
||||
bugCanvas.addEventListener('pointercancel', endBugDraw);
|
||||
$('#bug-clear-marks').addEventListener('click', () => { bugState.rects = []; drawBugCanvas(); });
|
||||
|
||||
function closeBugPanel() {
|
||||
if (bugPanel.open) bugPanel.close(); // löst 'close' aus, dort wird aufgeräumt
|
||||
}
|
||||
// Aufräumen bei jedem Schließen: Escape, Abbrechen, automatisch nach dem Senden
|
||||
bugPanel.addEventListener('close', () => {
|
||||
clearTimeout(bugState.closeTimer);
|
||||
bugState.closeTimer = null;
|
||||
bugState.capture = null;
|
||||
bugState.base = null;
|
||||
bugState.rects = [];
|
||||
bugState.draft = null;
|
||||
bugState.target = null;
|
||||
$('#bug-button').setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
|
||||
/** Panel öffnen und aufnehmen. target = offener Dialog, der stattdessen fotografiert wird. */
|
||||
async function openBugPanel(target = null) {
|
||||
if (bugPanel.open) return;
|
||||
clearTimeout(bugState.closeTimer);
|
||||
bugState.closeTimer = null;
|
||||
bugState.target = target;
|
||||
$('#bug-button').setAttribute('aria-expanded', 'true');
|
||||
$('#bug-shot').hidden = true;
|
||||
$('#bug-shot-hint').hidden = true;
|
||||
$('#bug-result').hidden = true;
|
||||
$('#bug-submit').disabled = false;
|
||||
$('#bug-text').value = '';
|
||||
try { $('#bug-reporter').value = localStorage.getItem('rotatory.reporter') || ''; } catch { /* kein Speicher */ }
|
||||
const status = $('#bug-status');
|
||||
status.hidden = false;
|
||||
status.textContent = 'Bildschirm wird aufgenommen …';
|
||||
bugState.base = null;
|
||||
bugState.rects = [];
|
||||
bugPanel.showModal(); // auch über anderen offenen Dialogen bedienbar
|
||||
$('#bug-text').focus();
|
||||
const capture = captureScreenshot(target);
|
||||
bugState.capture = capture;
|
||||
try {
|
||||
const shot = await capture;
|
||||
if (bugState.capture !== capture) return; // inzwischen geschlossen oder neu geöffnet
|
||||
bugState.base = shot;
|
||||
bugCanvas.width = shot.width;
|
||||
bugCanvas.height = shot.height;
|
||||
drawBugCanvas();
|
||||
status.hidden = true;
|
||||
$('#bug-shot').hidden = false;
|
||||
$('#bug-shot-hint').hidden = false;
|
||||
} catch (err) {
|
||||
if (bugState.capture !== capture) return;
|
||||
status.textContent = `Screenshot nicht möglich (${err.message}). Beschreibe den Fehler bitte trotzdem.`;
|
||||
} finally {
|
||||
if (bugState.capture === capture) bugState.capture = null;
|
||||
}
|
||||
}
|
||||
$('#bug-button').addEventListener('click', () => { if (bugPanel.open) closeBugPanel(); else openBugPanel(); });
|
||||
$('#bug-cancel').addEventListener('click', closeBugPanel);
|
||||
// Käfer im Kopf jedes Dialogs: nimmt den Dialog auf und öffnet das Panel darüber
|
||||
document.addEventListener('click', (e) => {
|
||||
const trigger = e.target.closest('.dialog-bug');
|
||||
if (trigger) openBugPanel(trigger.closest('dialog'));
|
||||
});
|
||||
$('#bug-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
if (bugState.sending) return;
|
||||
const out = $('#bug-result');
|
||||
const text = $('#bug-text').value.trim();
|
||||
if (!text) { out.className = 'result error'; out.textContent = 'Bitte beschreibe kurz, was falsch läuft.'; out.hidden = false; return; }
|
||||
bugState.sending = true;
|
||||
$('#bug-submit').disabled = true;
|
||||
out.className = 'result';
|
||||
out.textContent = 'Wird gesendet …';
|
||||
out.hidden = false;
|
||||
const reporter = $('#bug-reporter').value.trim();
|
||||
try { localStorage.setItem('rotatory.reporter', reporter); } catch { /* kein Speicher */ }
|
||||
// Läuft die Aufnahme noch, erst abwarten – sonst ginge die Meldung ohne Bild raus
|
||||
if (bugState.capture) {
|
||||
out.textContent = 'Bildschirm wird noch aufgenommen …';
|
||||
try { await bugState.capture; } catch { /* dann eben ohne Screenshot */ }
|
||||
out.textContent = 'Wird gesendet …';
|
||||
}
|
||||
const page = bugState.target ? `${viewName()}, Dialog „${bugState.target.querySelector('h2')?.textContent.trim() || ''}"` : viewName();
|
||||
try {
|
||||
let screenshot = bugState.base ? bugCanvas.toDataURL('image/png') : '';
|
||||
if (screenshot.length > 8_000_000) screenshot = bugCanvas.toDataURL('image/jpeg', 0.85); // großes Display: kleiner als JPEG
|
||||
await api('api/bugs', { method: 'POST', body: { text, reporter, page, url: location.href, viewport: `${window.innerWidth}×${window.innerHeight}`, userAgent: navigator.userAgent, screenshot } });
|
||||
} catch (err) {
|
||||
out.className = 'result error';
|
||||
out.textContent = err.message;
|
||||
$('#bug-submit').disabled = false;
|
||||
bugState.sending = false;
|
||||
return;
|
||||
}
|
||||
bugState.sending = false;
|
||||
out.textContent = 'Danke, der Fehler ist gemeldet.';
|
||||
bugState.closeTimer = setTimeout(() => { bugState.closeTimer = null; closeBugPanel(); }, 2000);
|
||||
if (isEditor()) load().catch((err) => toast(err.message, true));
|
||||
});
|
||||
|
||||
// ---------- Fehlermeldungen bearbeiten (nur Bearbeiter) ----------
|
||||
const canCopyImages = () => Boolean(window.isSecureContext && navigator.clipboard && navigator.clipboard.write && window.ClipboardItem);
|
||||
async function copyText(text) {
|
||||
if (window.isSecureContext && navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return; }
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.setAttribute('readonly', '');
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.left = '-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
const ok = document.execCommand('copy');
|
||||
ta.remove();
|
||||
if (!ok) throw new Error('Der Browser hat das Kopieren abgelehnt.');
|
||||
}
|
||||
async function copyImage(url) {
|
||||
const blob = await (await fetch(url)).blob();
|
||||
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
|
||||
}
|
||||
function browserLabel(ua) {
|
||||
const u = String(ua || '');
|
||||
const m = u.match(/(Edg|OPR|Firefox|Chrome|Safari|Version)\/(\d+)/g) || [];
|
||||
const pick = (name) => m.find((x) => x.startsWith(name + '/'));
|
||||
const b = pick('Edg') ? `Edge ${pick('Edg').split('/')[1]}` : pick('OPR') ? `Opera ${pick('OPR').split('/')[1]}` : pick('Firefox') ? `Firefox ${pick('Firefox').split('/')[1]}` : pick('Chrome') ? `Chrome ${pick('Chrome').split('/')[1]}` : pick('Version') && /Safari/.test(u) ? `Safari ${pick('Version').split('/')[1]}` : 'Browser';
|
||||
const os = /Windows/.test(u) ? 'Windows' : /Mac OS X/.test(u) && !/iPhone|iPad/.test(u) ? 'macOS' : /iPhone|iPad/.test(u) ? 'iOS' : /Android/.test(u) ? 'Android' : /Linux/.test(u) ? 'Linux' : '';
|
||||
return os ? `${b}, ${os}` : b;
|
||||
}
|
||||
function bugRow(b) {
|
||||
const li = document.createElement('li');
|
||||
if (b.hasScreenshot) {
|
||||
const a = document.createElement('a');
|
||||
a.className = 'bug-thumb';
|
||||
a.href = `bugs/${b.id}.png`;
|
||||
a.target = '_blank';
|
||||
a.rel = 'noopener';
|
||||
a.title = 'Screenshot in voller Größe öffnen';
|
||||
const img = document.createElement('img');
|
||||
img.src = `bugs/${b.id}.png`;
|
||||
img.alt = 'Screenshot zur Fehlermeldung';
|
||||
img.loading = 'lazy';
|
||||
a.appendChild(img);
|
||||
li.appendChild(a);
|
||||
}
|
||||
const grow = document.createElement('div');
|
||||
grow.className = 'grow';
|
||||
const text = document.createElement('div');
|
||||
text.className = 'bug-text';
|
||||
text.textContent = b.text;
|
||||
grow.appendChild(text);
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'bug-meta';
|
||||
meta.textContent = [b.reporter || 'anonym', new Date(b.createdAt).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short' }), b.page, b.viewport, browserLabel(b.userAgent), b.version ? `Version ${b.version}` : '', b.status === 'erledigt' ? 'erledigt' : ''].filter(Boolean).join(' · ');
|
||||
grow.appendChild(meta);
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'bug-actions';
|
||||
const btn = (label, cls, fn) => { const el = document.createElement('button'); el.type = 'button'; el.className = cls; el.textContent = label; el.addEventListener('click', fn); actions.appendChild(el); return el; };
|
||||
btn('Prompt kopieren', 'primary', async () => {
|
||||
try {
|
||||
const res = await fetch(`api/bugs/${b.id}/prompt`);
|
||||
if (!res.ok) throw new Error('Prompt konnte nicht geladen werden.');
|
||||
await copyText(await res.text());
|
||||
toast('Prompt kopiert. Jetzt in Claude Code einfügen, Screenshot dazu.');
|
||||
} catch (err) { toast(err.message, true); }
|
||||
});
|
||||
if (b.hasScreenshot) {
|
||||
if (canCopyImages()) {
|
||||
btn('Screenshot kopieren', '', async () => {
|
||||
try { await copyImage(`bugs/${b.id}.png`); toast('Screenshot kopiert.'); } catch (err) { toast(`Screenshot konnte nicht kopiert werden: ${err.message}`, true); }
|
||||
});
|
||||
} else {
|
||||
btn('Screenshot öffnen', '', () => window.open(`bugs/${b.id}.png`, '_blank', 'noopener'));
|
||||
}
|
||||
}
|
||||
if (b.status === 'offen') btn('Erledigt', 'ghost', () => run(() => api(`api/bugs/${b.id}`, { method: 'PUT', body: { status: 'erledigt' } }), 'Als erledigt markiert.'));
|
||||
else btn('Wieder öffnen', 'ghost', () => run(() => api(`api/bugs/${b.id}`, { method: 'PUT', body: { status: 'offen' } })));
|
||||
btn('Löschen', 'ghost danger', () => { if (confirm('Diese Fehlermeldung samt Screenshot löschen?')) run(() => api(`api/bugs/${b.id}`, { method: 'DELETE' }), 'Meldung gelöscht.'); });
|
||||
grow.appendChild(actions);
|
||||
li.appendChild(grow);
|
||||
return li;
|
||||
}
|
||||
function renderBugs() {
|
||||
const all = state.data.bugs || [];
|
||||
const open = all.filter((b) => b.status === 'offen').sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
const done = all.filter((b) => b.status !== 'offen').sort((a, b) => (b.resolvedAt || '').localeCompare(a.resolvedAt || ''));
|
||||
$('#bugs-clipboard-hint').hidden = canCopyImages();
|
||||
const list = $('#bug-list');
|
||||
list.textContent = '';
|
||||
if (open.length === 0) list.innerHTML = '<li class="hint">Keine offenen Fehlermeldungen.</li>';
|
||||
for (const b of open) list.appendChild(bugRow(b));
|
||||
const doneList = $('#bug-list-done');
|
||||
doneList.textContent = '';
|
||||
for (const b of done) doneList.appendChild(bugRow(b));
|
||||
doneList.hidden = !$('#bugs-show-done').checked || done.length === 0;
|
||||
$('#bugs-toggle').hidden = done.length === 0;
|
||||
}
|
||||
$('#bugs-show-done').addEventListener('change', renderBugs);
|
||||
|
||||
// ---------- Start ----------
|
||||
function renderAll() {
|
||||
applyAuthUI();
|
||||
@@ -1143,6 +1462,7 @@ function renderAll() {
|
||||
renderPhotoField();
|
||||
renderAbsences();
|
||||
renderRequests();
|
||||
renderBugs();
|
||||
renderSettings();
|
||||
if (!$('#view-abwesenheiten').hidden) prepareAbsenceImport();
|
||||
} else {
|
||||
|
||||
+43
-6
@@ -17,6 +17,7 @@
|
||||
<a href="#personen" data-view="personen">Personen</a>
|
||||
<a href="#abwesenheiten" data-view="abwesenheiten">Abwesenheiten</a>
|
||||
<a href="#anfragen" data-view="anfragen">Anfragen <span id="requests-count" class="count" hidden></span></a>
|
||||
<a href="#fehler" data-view="fehler">Fehler <span id="bugs-count" class="count" hidden></span></a>
|
||||
<a href="#einstellungen" data-view="einstellungen">Einstellungen</a>
|
||||
</nav>
|
||||
<button type="button" id="auth-button" class="topbar-action" hidden>Anmelden</button>
|
||||
@@ -157,6 +158,16 @@
|
||||
<ul id="request-list-done" class="list" hidden></ul>
|
||||
</section>
|
||||
|
||||
<!-- Fehlermeldungen (nur Bearbeiter) -->
|
||||
<section id="view-fehler" class="view" hidden aria-labelledby="bugs-title">
|
||||
<h2 id="bugs-title">Fehlermeldungen</h2>
|
||||
<p class="hint">Gemeldet über den Käfer-Button unten rechts. „Prompt kopieren" legt eine fertige Anweisung für Claude in die Zwischenablage, „Screenshot kopieren" das Bild dazu. Beides zusammen in Claude Code einfügen, dann wird der Fehler direkt bearbeitet.</p>
|
||||
<p class="hint" id="bugs-clipboard-hint" hidden>Diese Adresse ist nicht verschlüsselt (http). Bilder kann der Browser hier nicht direkt in die Zwischenablage legen: „Screenshot öffnen" nutzen und dort mit Rechtsklick „Bild kopieren" wählen. Über eine https-Adresse geht es mit einem Klick.</p>
|
||||
<ul id="bug-list" class="list bug-list"></ul>
|
||||
<label class="inline" id="bugs-toggle"><input type="checkbox" id="bugs-show-done"> Erledigte anzeigen</label>
|
||||
<ul id="bug-list-done" class="list bug-list" hidden></ul>
|
||||
</section>
|
||||
|
||||
<!-- Einstellungen -->
|
||||
<section id="view-einstellungen" class="view" hidden aria-labelledby="settings-title">
|
||||
<h2 id="settings-title">Einstellungen</h2>
|
||||
@@ -167,6 +178,8 @@
|
||||
<label>Fairness-Fenster in Tagen <input type="number" name="historyWindowDays" min="0" max="365"></label>
|
||||
<label>Kalendername in Nextcloud <input name="calendarDisplayName" maxlength="60"></label>
|
||||
<label class="inline wide"><input type="checkbox" name="inviteAttendees"> Moderatoren als Teilnehmer eintragen (E-Mail nötig; Nextcloud verschickt dann Einladungen)</label>
|
||||
<label class="wide">Vorlage für den Fehler-Prompt <textarea name="bugPromptTemplate" rows="7" maxlength="4000"></textarea></label>
|
||||
<p class="hint">Platzhalter: {text} Beschreibung, {reporter} Name, {date} Zeitpunkt, {page} Ansicht, {browser}, {viewport}, {version}, {url}. Leer lassen stellt die Vorlage wieder her.</p>
|
||||
<div class="form-actions"><button type="submit" class="primary">Speichern</button></div>
|
||||
</form>
|
||||
|
||||
@@ -192,7 +205,7 @@
|
||||
<!-- Dialog: Tag bearbeiten -->
|
||||
<dialog id="day-dialog">
|
||||
<form method="dialog" class="dialog-form" id="day-form">
|
||||
<h2 id="day-dialog-title">Tag</h2>
|
||||
<div class="dialog-head"><h2 id="day-dialog-title">Tag</h2><button type="button" class="ghost icon-button dialog-bug" aria-label="Fehler in diesem Dialog melden" title="Fehler melden"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="6.5" r="2.5"/><path d="M7 14a5 5 0 0 1 10 0v2a5 5 0 0 1-10 0z"/><path d="M12 9v12"/><path d="M7 13H3.5"/><path d="M7 17H4l-1 2.5"/><path d="M17 13h3.5"/><path d="M17 17h3l1 2.5"/><path d="M8.5 9.5 5.5 7"/><path d="M15.5 9.5 18.5 7"/></svg></button></div>
|
||||
<p id="day-dialog-info" class="hint"></p>
|
||||
<label>Hauptmoderation <select name="main"></select></label>
|
||||
<label>Nebenmoderation <select name="co"></select></label>
|
||||
@@ -211,7 +224,7 @@
|
||||
<!-- Dialog: Auslosen -->
|
||||
<dialog id="plan-dialog">
|
||||
<form method="dialog" class="dialog-form" id="plan-form">
|
||||
<h2>Moderation auslosen</h2>
|
||||
<div class="dialog-head"><h2>Moderation auslosen</h2><button type="button" class="ghost icon-button dialog-bug" aria-label="Fehler in diesem Dialog melden" title="Fehler melden"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="6.5" r="2.5"/><path d="M7 14a5 5 0 0 1 10 0v2a5 5 0 0 1-10 0z"/><path d="M12 9v12"/><path d="M7 13H3.5"/><path d="M7 17H4l-1 2.5"/><path d="M17 13h3.5"/><path d="M17 17h3l1 2.5"/><path d="M8.5 9.5 5.5 7"/><path d="M15.5 9.5 18.5 7"/></svg></button></div>
|
||||
<label>Von Monat <input type="month" name="from" required></label>
|
||||
<label>Bis Monat <input type="month" name="to" required></label>
|
||||
<label class="inline"><input type="checkbox" name="overwrite"> Bereits ausgeloste, nicht fixierte Tage neu auslosen</label>
|
||||
@@ -229,7 +242,7 @@
|
||||
<!-- Dialog: Nextcloud -->
|
||||
<dialog id="nextcloud-dialog">
|
||||
<form method="dialog" class="dialog-form" id="nextcloud-form">
|
||||
<h2>In Nextcloud schreiben</h2>
|
||||
<div class="dialog-head"><h2>In Nextcloud schreiben</h2><button type="button" class="ghost icon-button dialog-bug" aria-label="Fehler in diesem Dialog melden" title="Fehler melden"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="6.5" r="2.5"/><path d="M7 14a5 5 0 0 1 10 0v2a5 5 0 0 1-10 0z"/><path d="M12 9v12"/><path d="M7 13H3.5"/><path d="M7 17H4l-1 2.5"/><path d="M17 13h3.5"/><path d="M17 17h3l1 2.5"/><path d="M8.5 9.5 5.5 7"/><path d="M15.5 9.5 18.5 7"/></svg></button></div>
|
||||
<p id="nextcloud-info" class="hint"></p>
|
||||
<label>Zielkalender
|
||||
<select name="target"><option value="">– Kalender laden …</option></select>
|
||||
@@ -251,7 +264,7 @@
|
||||
<!-- Dialog: Kontakte importieren -->
|
||||
<dialog id="contacts-dialog">
|
||||
<form method="dialog" class="dialog-form" id="contacts-form">
|
||||
<h2>Personen aus Nextcloud</h2>
|
||||
<div class="dialog-head"><h2>Personen aus Nextcloud</h2><button type="button" class="ghost icon-button dialog-bug" aria-label="Fehler in diesem Dialog melden" title="Fehler melden"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="6.5" r="2.5"/><path d="M7 14a5 5 0 0 1 10 0v2a5 5 0 0 1-10 0z"/><path d="M12 9v12"/><path d="M7 13H3.5"/><path d="M7 17H4l-1 2.5"/><path d="M17 13h3.5"/><path d="M17 17h3l1 2.5"/><path d="M8.5 9.5 5.5 7"/><path d="M15.5 9.5 18.5 7"/></svg></button></div>
|
||||
<label>Adressbuch <select name="addressbook"></select></label>
|
||||
<p id="contacts-info" class="hint">Lade Kontakte …</p>
|
||||
<div class="contacts-tools">
|
||||
@@ -271,7 +284,7 @@
|
||||
<!-- Dialog: Tausch anfragen (für alle) -->
|
||||
<dialog id="request-dialog">
|
||||
<form method="dialog" class="dialog-form" id="request-form">
|
||||
<h2>Tausch anfragen</h2>
|
||||
<div class="dialog-head"><h2>Tausch anfragen</h2><button type="button" class="ghost icon-button dialog-bug" aria-label="Fehler in diesem Dialog melden" title="Fehler melden"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="6.5" r="2.5"/><path d="M7 14a5 5 0 0 1 10 0v2a5 5 0 0 1-10 0z"/><path d="M12 9v12"/><path d="M7 13H3.5"/><path d="M7 17H4l-1 2.5"/><path d="M17 13h3.5"/><path d="M17 17h3l1 2.5"/><path d="M8.5 9.5 5.5 7"/><path d="M15.5 9.5 18.5 7"/></svg></button></div>
|
||||
<p id="request-info" class="hint"></p>
|
||||
<label>Ich bin <select name="personId" required></select></label>
|
||||
<label>Tag, an dem ich nicht kann <input type="date" name="date" required></label>
|
||||
@@ -290,7 +303,7 @@
|
||||
<!-- Dialog: Anmelden zum Bearbeiten -->
|
||||
<dialog id="login-dialog">
|
||||
<form method="dialog" class="dialog-form" id="login-form">
|
||||
<h2>Zum Bearbeiten anmelden</h2>
|
||||
<div class="dialog-head"><h2>Zum Bearbeiten anmelden</h2><button type="button" class="ghost icon-button dialog-bug" aria-label="Fehler in diesem Dialog melden" title="Fehler melden"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="6.5" r="2.5"/><path d="M7 14a5 5 0 0 1 10 0v2a5 5 0 0 1-10 0z"/><path d="M12 9v12"/><path d="M7 13H3.5"/><path d="M7 17H4l-1 2.5"/><path d="M17 13h3.5"/><path d="M17 17h3l1 2.5"/><path d="M8.5 9.5 5.5 7"/><path d="M15.5 9.5 18.5 7"/></svg></button></div>
|
||||
<p class="hint">Ansehen kann jeder. Auslosen, Personen, Abwesenheiten und Nextcloud erfordern das Bearbeitungspasswort.</p>
|
||||
<label>Passwort <input type="password" name="password" id="login-password" autocomplete="current-password" required></label>
|
||||
<p id="login-result" class="result error" hidden></p>
|
||||
@@ -302,7 +315,31 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Fehler melden: Käfer-Button und Panel, für alle sichtbar -->
|
||||
<div id="bug-widget">
|
||||
<button type="button" id="bug-button" class="bug-button" aria-label="Fehler melden" title="Fehler melden" aria-expanded="false" aria-controls="bug-panel"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="6.5" r="2.5"/><path d="M7 14a5 5 0 0 1 10 0v2a5 5 0 0 1-10 0z"/><path d="M12 9v12"/><path d="M7 13H3.5"/><path d="M7 17H4l-1 2.5"/><path d="M17 13h3.5"/><path d="M17 17h3l1 2.5"/><path d="M8.5 9.5 5.5 7"/><path d="M15.5 9.5 18.5 7"/></svg></button>
|
||||
<dialog id="bug-panel" class="bug-panel" aria-labelledby="bug-title">
|
||||
<h2 id="bug-title">Fehler melden</h2>
|
||||
<p id="bug-status" class="hint">Bildschirm wird aufgenommen …</p>
|
||||
<div class="bug-shot" id="bug-shot" hidden>
|
||||
<canvas id="bug-canvas" aria-label="Screenshot der Seite. Markiere die Stelle mit einem roten Rechteck."></canvas>
|
||||
</div>
|
||||
<p class="hint bug-shot-hint" id="bug-shot-hint" hidden>Ziehe ein rotes Rechteck auf, um die Stelle zu markieren. <button type="button" class="link" id="bug-clear-marks" hidden>Markierungen entfernen</button></p>
|
||||
<form id="bug-form" class="bug-form" autocomplete="off">
|
||||
<label>Was läuft falsch? <textarea name="text" id="bug-text" rows="3" maxlength="2000" required placeholder="z. B. Beim Klick auf Auslosen passiert nichts"></textarea></label>
|
||||
<label>Dein Name (optional) <input name="reporter" id="bug-reporter" maxlength="80" autocomplete="name"></label>
|
||||
<p id="bug-result" class="result" hidden></p>
|
||||
<div class="form-actions">
|
||||
<span class="spacer"></span>
|
||||
<button type="button" class="ghost" id="bug-cancel">Abbrechen</button>
|
||||
<button type="submit" class="primary" id="bug-submit">Senden</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
|
||||
<script src="vendor/html2canvas.min.js" defer></script>
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+49
-7
@@ -88,14 +88,17 @@ svg.icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-w
|
||||
.topbar nav { flex-wrap: nowrap; overflow-x: auto; width: 100%; margin: 0 -16px; padding: 0 8px; scrollbar-width: none; }
|
||||
.topbar nav::-webkit-scrollbar { display: none; }
|
||||
.topbar nav a { min-height: 44px; padding: 0 10px; font-size: 13px; white-space: nowrap; }
|
||||
.topbar-action { margin-left: auto; margin-block: 8px; }
|
||||
/* Titel und Anmelden in einer Zeile, Navigation darunter: zwei Zeilen statt drei */
|
||||
.topbar h1 { order: 0; }
|
||||
.topbar-action { order: 1; margin-left: auto; margin-block: 8px; }
|
||||
.topbar nav { order: 2; }
|
||||
}
|
||||
button:focus-visible, input:focus-visible, select:focus-visible, .button:focus-visible, .day:focus-visible, .avatar[tabindex]:focus-visible {
|
||||
button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, .button:focus-visible, .day:focus-visible, .avatar[tabindex]:focus-visible {
|
||||
outline: 2px solid var(--tradeo-blue); outline-offset: 2px;
|
||||
}
|
||||
|
||||
main { padding: 24px 16px 48px; max-width: 1120px; margin: 0 auto; }
|
||||
@media (min-width: 720px) { main { padding: 32px 24px 64px; } }
|
||||
main { padding: 24px 16px 96px; max-width: 1120px; margin: 0 auto; }
|
||||
@media (min-width: 720px) { main { padding: 32px 24px 96px; } }
|
||||
.view > h2 { margin-bottom: 24px; }
|
||||
h3 { margin: 32px 0 12px; }
|
||||
|
||||
@@ -106,13 +109,16 @@ h3 { margin: 32px 0 12px; }
|
||||
.form-grid h3 { margin: 0; }
|
||||
label { display: flex; flex-direction: column; gap: 6px; font-size: 13px; font-weight: 500; color: var(--text-2); }
|
||||
label.inline { flex-direction: row; align-items: center; gap: 10px; color: var(--text); font-weight: 400; font-size: 14px; }
|
||||
label > input:not([type="checkbox"]), label > select { width: 100%; min-width: 0; max-width: 100%; }
|
||||
label > input:not([type="checkbox"]), label > select, label > textarea { width: 100%; min-width: 0; max-width: 100%; }
|
||||
textarea { resize: vertical; line-height: 1.45; }
|
||||
button.link { display: inline; min-height: 0; padding: 0; border: 0; background: none; font-size: inherit; color: var(--tradeo-blue); text-decoration: underline; text-underline-offset: 2px; }
|
||||
button.link:hover { background: none; }
|
||||
.form-grid > *, .dialog-form > *, .row > label { min-width: 0; }
|
||||
input, select, button, .button {
|
||||
input, select, textarea, button, .button {
|
||||
font: inherit; font-size: 15px; color: var(--text); background: var(--white);
|
||||
border: 1px solid var(--line-2); border-radius: var(--radius); padding: 8px 12px; min-height: 40px;
|
||||
}
|
||||
input::placeholder { color: var(--text-3); }
|
||||
input::placeholder, textarea::placeholder { color: var(--text-3); }
|
||||
input[type="checkbox"] { min-height: 0; width: 18px; height: 18px; margin: 0; accent-color: var(--tradeo-blue); }
|
||||
fieldset.checks { border: 1px solid var(--line); border-radius: var(--radius); display: flex; flex-wrap: wrap; gap: 12px 24px; padding: 12px 16px 16px; margin: 0; }
|
||||
fieldset.checks legend { font-size: 13px; font-weight: 500; color: var(--text-2); padding: 0 4px; }
|
||||
@@ -264,6 +270,42 @@ dialog::backdrop { background: rgba(18, 46, 69, .4); }
|
||||
.contacts-list label { flex-direction: row; align-items: center; gap: 12px; color: var(--text); flex: 1; cursor: pointer; font-weight: 400; font-size: 14px; }
|
||||
.contacts-tools { display: flex; gap: 8px; }
|
||||
|
||||
/* Fehler melden: Käfer-Button unten rechts und kompaktes Panel darüber */
|
||||
.bug-button {
|
||||
position: fixed; right: 16px; bottom: calc(16px + env(safe-area-inset-bottom, 0px)); z-index: 25;
|
||||
width: 48px; height: 48px; padding: 0; border-radius: 50%; border: 0;
|
||||
background: var(--tradeo-blue); color: var(--white); box-shadow: 0 4px 12px rgba(18, 46, 69, .24);
|
||||
}
|
||||
.bug-button:hover { background: var(--tradeo-blue); box-shadow: 0 6px 16px rgba(18, 46, 69, .32); }
|
||||
.bug-button svg { width: 24px; height: 24px; }
|
||||
.bug-button[aria-expanded="true"] { box-shadow: inset 0 0 0 999px rgba(255, 255, 255, .12), 0 4px 12px rgba(18, 46, 69, .24); }
|
||||
/* Das Panel ist ein modaler Dialog: so liegt es auch über anderen offenen Dialogen und Escape schließt es */
|
||||
dialog.bug-panel {
|
||||
position: fixed; inset: auto 16px calc(76px + env(safe-area-inset-bottom, 0px)) auto; margin: 0;
|
||||
width: min(400px, calc(100vw - 32px)); max-height: calc(100vh - 100px); overflow: auto;
|
||||
padding: 16px; display: grid; gap: 10px;
|
||||
}
|
||||
dialog.bug-panel:not([open]) { display: none; }
|
||||
dialog.bug-panel::backdrop { background: rgba(18, 46, 69, .18); }
|
||||
.dialog-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.dialog-head h2 { margin: 0; }
|
||||
.dialog-bug { width: 36px; min-height: 36px; padding: 0; color: var(--text-2); }
|
||||
.dialog-bug svg { width: 20px; height: 20px; }
|
||||
.bug-panel h2 { font-size: 16px; }
|
||||
.bug-panel .hint { margin: 0; }
|
||||
.bug-shot { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--tint-1); }
|
||||
.bug-shot canvas { display: block; width: auto; max-width: 100%; max-height: 45vh; height: auto; margin: 0 auto; cursor: crosshair; touch-action: none; }
|
||||
.bug-form { display: grid; gap: 10px; }
|
||||
.bug-form textarea { min-height: 72px; }
|
||||
.bug-list li { align-items: flex-start; }
|
||||
.bug-thumb { display: block; width: 160px; max-width: 100%; flex-shrink: 0; }
|
||||
.bug-thumb img { display: block; width: 100%; height: auto; border: 1px solid var(--line); border-radius: 4px; }
|
||||
.bug-text { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.bug-meta { color: var(--text-2); font-size: 13px; margin-top: 4px; }
|
||||
.bug-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
|
||||
.bug-actions button { min-height: 34px; padding: 0 12px; font-size: 13px; }
|
||||
@media (max-width: 560px) { .bug-thumb { width: 100%; } }
|
||||
|
||||
.toast {
|
||||
position: fixed; bottom: calc(16px + env(safe-area-inset-bottom, 0px)); left: 50%; transform: translateX(-50%);
|
||||
background: var(--tradeo-blue); color: var(--white); padding: 10px 16px; border-radius: var(--radius); font-size: 14px; z-index: 20; max-width: 90vw;
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
Copyright (c) 2012 Niklas von Hertzen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
/*!
|
||||
* html2canvas 1.4.1 <https://html2canvas.hertzen.com>
|
||||
* Copyright (c) 2022 Niklas von Hertzen <https://hertzen.com>
|
||||
* Released under MIT License
|
||||
*/
|
||||
!function(A,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(A="undefined"!=typeof globalThis?globalThis:A||self).html2canvas=e()}(this,function(){"use strict";
|
||||
/*! *****************************************************************************
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
***************************************************************************** */var r=function(A,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(A,e){A.__proto__=e}||function(A,e){for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&(A[t]=e[t])})(A,e)};function A(A,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function t(){this.constructor=A}r(A,e),A.prototype=null===e?Object.create(e):(t.prototype=e.prototype,new t)}var h=function(){return(h=Object.assign||function(A){for(var e,t=1,r=arguments.length;t<r;t++)for(var B in e=arguments[t])Object.prototype.hasOwnProperty.call(e,B)&&(A[B]=e[B]);return A}).apply(this,arguments)};function a(A,s,o,i){return new(o=o||Promise)(function(t,e){function r(A){try{n(i.next(A))}catch(A){e(A)}}function B(A){try{n(i.throw(A))}catch(A){e(A)}}function n(A){var e;A.done?t(A.value):((e=A.value)instanceof o?e:new o(function(A){A(e)})).then(r,B)}n((i=i.apply(A,s||[])).next())})}function H(t,r){var B,n,s,o={label:0,sent:function(){if(1&s[0])throw s[1];return s[1]},trys:[],ops:[]},A={next:e(0),throw:e(1),return:e(2)};return"function"==typeof Symbol&&(A[Symbol.iterator]=function(){return this}),A;function e(e){return function(A){return function(e){if(B)throw new TypeError("Generator is already executing.");for(;o;)try{if(B=1,n&&(s=2&e[0]?n.return:e[0]?n.throw||((s=n.return)&&s.call(n),0):n.next)&&!(s=s.call(n,e[1])).done)return s;switch(n=0,(e=s?[2&e[0],s.value]:e)[0]){case 0:case 1:s=e;break;case 4:return o.label++,{value:e[1],done:!1};case 5:o.label++,n=e[1],e=[0];continue;case 7:e=o.ops.pop(),o.trys.pop();continue;default:if(!(s=0<(s=o.trys).length&&s[s.length-1])&&(6===e[0]||2===e[0])){o=0;continue}if(3===e[0]&&(!s||e[1]>s[0]&&e[1]<s[3])){o.label=e[1];break}if(6===e[0]&&o.label<s[1]){o.label=s[1],s=e;break}if(s&&o.label<s[2]){o.label=s[2],o.ops.push(e);break}s[2]&&o.ops.pop(),o.trys.pop();continue}e=r.call(t,o)}catch(A){e=[6,A],n=0}finally{B=s=0}if(5&e[0])throw e[1];return{value:e[0]?e[1]:void 0,done:!0}}([e,A])}}}function t(A,e,t){if(t||2===arguments.length)for(var r,B=0,n=e.length;B<n;B++)!r&&B in e||((r=r||Array.prototype.slice.call(e,0,B))[B]=e[B]);return A.concat(r||e)}var d=(B.prototype.add=function(A,e,t,r){return new B(this.left+A,this.top+e,this.width+t,this.height+r)},B.fromClientRect=function(A,e){return new B(e.left+A.windowBounds.left,e.top+A.windowBounds.top,e.width,e.height)},B.fromDOMRectList=function(A,e){e=Array.from(e).find(function(A){return 0!==A.width});return e?new B(e.left+A.windowBounds.left,e.top+A.windowBounds.top,e.width,e.height):B.EMPTY},B.EMPTY=new B(0,0,0,0),B);function B(A,e,t,r){this.left=A,this.top=e,this.width=t,this.height=r}for(var f=function(A,e){return d.fromClientRect(A,e.getBoundingClientRect())},Q=function(A){for(var e=[],t=0,r=A.length;t<r;){var B,n=A.charCodeAt(t++);55296<=n&&n<=56319&&t<r?56320==(64512&(B=A.charCodeAt(t++)))?e.push(((1023&n)<<10)+(1023&B)+65536):(e.push(n),t--):e.push(n)}return e},g=function(){for(var A=[],e=0;e<arguments.length;e++)A[e]=arguments[e];if(String.fromCodePoint)return String.fromCodePoint.apply(String,A);var t=A.length;if(!t)return"";for(var r=[],B=-1,n="";++B<t;){var s=A[B];s<=65535?r.push(s):(s-=65536,r.push(55296+(s>>10),s%1024+56320)),(B+1===t||16384<r.length)&&(n+=String.fromCharCode.apply(String,r),r.length=0)}return n},e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n="undefined"==typeof Uint8Array?[]:new Uint8Array(256),s=0;s<e.length;s++)n[e.charCodeAt(s)]=s;for(var o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",c="undefined"==typeof Uint8Array?[]:new Uint8Array(256),i=0;i<o.length;i++)c[o.charCodeAt(i)]=i;function w(A,e,t){return A.slice?A.slice(e,t):new Uint16Array(Array.prototype.slice.call(A,e,t))}var U=(l.prototype.get=function(A){var e;if(0<=A){if(A<55296||56319<A&&A<=65535)return e=this.index[A>>5],this.data[e=(e<<2)+(31&A)];if(A<=65535)return e=this.index[2048+(A-55296>>5)],this.data[e=(e<<2)+(31&A)];if(A<this.highStart)return e=this.index[e=2080+(A>>11)],e=this.index[e+=A>>5&63],this.data[e=(e<<2)+(31&A)];if(A<=1114111)return this.data[this.highValueIndex]}return this.errorValue},l);function l(A,e,t,r,B,n){this.initialValue=A,this.errorValue=e,this.highStart=t,this.highValueIndex=r,this.index=B,this.data=n}for(var C="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",u="undefined"==typeof Uint8Array?[]:new Uint8Array(256),F=0;F<C.length;F++)u[C.charCodeAt(F)]=F;function p(A,e,t,r){var B=r[t];if(Array.isArray(A)?-1!==A.indexOf(B):A===B)for(var n=t;n<=r.length;){if((o=r[++n])===e)return 1;if(o!==D)break}if(B===D)for(n=t;0<n;){var s=r[--n];if(Array.isArray(A)?-1!==A.indexOf(s):A===s)for(var o,i=t;i<=r.length;){if((o=r[++i])===e)return 1;if(o!==D)break}if(s!==D)break}}function E(A,e){for(var t=A;0<=t;){var r=e[t];if(r!==D)return r;t--}return 0}function I(t,A){var e=(B=function(A,r){void 0===r&&(r="strict");var B=[],n=[],sLine truncated
|
||||
Reference in new issue
Block a user