Module: im Abo-Modell Reiter mit Kurzlabels an die Etage melden (C2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b91ba1d5cb
commit
a50c5ef6c9
6 files changed
+366
-17
No files matched your search
+20
-6
@@ -7,6 +7,7 @@
|
|||||||
import { fehlerKarte, h, infoKarte, request } from './shop-dom.js'
|
import { fehlerKarte, h, infoKarte, request } from './shop-dom.js'
|
||||||
import { statusKarte } from './funktion-bestellen.js'
|
import { statusKarte } from './funktion-bestellen.js'
|
||||||
import { anfrageKarte, renderAnfrageFormular } from './buchungen-anfrage.js'
|
import { anfrageKarte, renderAnfrageFormular } from './buchungen-anfrage.js'
|
||||||
|
import { meldeReiter, reiterEintraege } from './fn-reiter.js'
|
||||||
|
|
||||||
/** Die Reiter: jeder ein Modul mit render(panel, ctx), erst beim ersten Oeffnen geladen. */
|
/** Die Reiter: jeder ein Modul mit render(panel, ctx), erst beim ersten Oeffnen geladen. */
|
||||||
export const REITER = [
|
export const REITER = [
|
||||||
@@ -27,6 +28,10 @@ export function vergissAuswahl() {
|
|||||||
let S = null // aktuelle Sitzung; eine alte Antwort darf eine neuere nicht ueberschreiben
|
let S = null // aktuelle Sitzung; eine alte Antwort darf eine neuere nicht ueberschreiben
|
||||||
|
|
||||||
const textOr = (x) => (typeof x === 'string' && x.trim() ? x : '')
|
const textOr = (x) => (typeof x === 'string' && x.trim() ? x : '')
|
||||||
|
// Abo-Modell (Antwort von GET /api/funktionen, strikt): die Reiter zeichnet die Etage, die Ansicht meldet sie (fn-reiter.js)
|
||||||
|
const istAbo = (s) => s.modell === 'abo'
|
||||||
|
/** Eine Ansicht ohne Reiter meldet die leere Liste, damit die Etage nicht die Reiter der vorigen stehen laesst. */
|
||||||
|
const keineReiter = (s) => { if (istAbo(s)) meldeReiter(s.root, { eintraege: [] }) }
|
||||||
const zustandVon = (projekt) => projekt?.features?.unterkunft?.state || 'off'
|
const zustandVon = (projekt) => projekt?.features?.unterkunft?.state || 'off'
|
||||||
|
|
||||||
export function initBuchungen(root, { projectId } = {}) {
|
export function initBuchungen(root, { projectId } = {}) {
|
||||||
@@ -38,7 +43,7 @@ export function initBuchungen(root, { projectId } = {}) {
|
|||||||
|
|
||||||
async function starte(root, projectId) {
|
async function starte(root, projectId) {
|
||||||
if (projectId) auswahl.projectId = projectId
|
if (projectId) auswahl.projectId = projectId
|
||||||
const s = { root, projects: [], projekt: null, status: null, lauf: 0, buehne: null, meldungEl: null }
|
const s = { root, projects: [], modell: undefined, projekt: null, status: null, lauf: 0, buehne: null, meldungEl: null }
|
||||||
S = s
|
S = s
|
||||||
let funktionen
|
let funktionen
|
||||||
try {
|
try {
|
||||||
@@ -48,12 +53,15 @@ async function starte(root, projectId) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (S !== s) return
|
if (S !== s) return
|
||||||
|
s.modell = funktionen?.modell
|
||||||
if (!funktionen?.unterkunft?.verfuegbar) {
|
if (!funktionen?.unterkunft?.verfuegbar) {
|
||||||
|
keineReiter(s)
|
||||||
root.replaceChildren(infoKarte(NICHT_VERFUEGBAR, 'Sobald er für dich bereit ist, findest du ihn hier.'))
|
root.replaceChildren(infoKarte(NICHT_VERFUEGBAR, 'Sobald er für dich bereit ist, findest du ihn hier.'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.projects = Array.isArray(funktionen.projects) ? funktionen.projects : []
|
s.projects = Array.isArray(funktionen.projects) ? funktionen.projects : []
|
||||||
if (!s.projects.length) {
|
if (!s.projects.length) {
|
||||||
|
keineReiter(s)
|
||||||
root.replaceChildren(h('div', { class: 'kb-panel kb-empty' }, 'Noch keine Website-Projekte zugewiesen.'))
|
root.replaceChildren(h('div', { class: 'kb-panel kb-empty' }, 'Noch keine Website-Projekte zugewiesen.'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -99,6 +107,7 @@ const ladeStatus = (projectId) => request(`/api/unterkunft/status?projectId=${en
|
|||||||
async function zeigeProjekt(s) {
|
async function zeigeProjekt(s) {
|
||||||
const lauf = ++s.lauf
|
const lauf = ++s.lauf
|
||||||
const aktuell = () => S === s && s.lauf === lauf
|
const aktuell = () => S === s && s.lauf === lauf
|
||||||
|
keineReiter(s)
|
||||||
s.buehne.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
|
s.buehne.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
|
||||||
let status
|
let status
|
||||||
try {
|
try {
|
||||||
@@ -113,6 +122,7 @@ async function zeigeProjekt(s) {
|
|||||||
|
|
||||||
function zeichneAnsicht(s, { status, aktuell }) {
|
function zeichneAnsicht(s, { status, aktuell }) {
|
||||||
s.status = status
|
s.status = status
|
||||||
|
keineReiter(s) // die Ansicht mit Reitern (zeigeReiter) meldet ihre Liste gleich danach
|
||||||
const zustand = status?.state || zustandVon(s.projekt)
|
const zustand = status?.state || zustandVon(s.projekt)
|
||||||
if (zustand === 'setup' || zustand === 'active' || zustand === 'disabled') zeigeReiter(s, { status, aktuell, zustand })
|
if (zustand === 'setup' || zustand === 'active' || zustand === 'disabled') zeigeReiter(s, { status, aktuell, zustand })
|
||||||
else if (zustand === 'ordered') zeigeBestellung(s)
|
else if (zustand === 'ordered') zeigeBestellung(s)
|
||||||
@@ -171,15 +181,16 @@ function zeigeReiter(s, { status, aktuell, zustand }) {
|
|||||||
zeigeMeldung: (meldung, art) => { if (aktuell()) zeigeMeldung(s, meldung, art) },
|
zeigeMeldung: (meldung, art) => { if (aktuell()) zeigeMeldung(s, meldung, art) },
|
||||||
neuLaden,
|
neuLaden,
|
||||||
}
|
}
|
||||||
|
const abo = istAbo(s) // Abo-Modell: keine eigene Leiste, die Etage zeichnet sie (kn:reiter)
|
||||||
const panel = h('div', { class: 'sh-panel', role: 'tabpanel', id: 'bk-panel', tabindex: '0' })
|
const panel = h('div', { class: 'sh-panel', role: 'tabpanel', id: 'bk-panel', tabindex: '0' })
|
||||||
let aktiv = null
|
let aktiv = null
|
||||||
let zaehler = 0
|
let zaehler = 0
|
||||||
const knoepfe = REITER.map((r) => h('button', {
|
const knoepfe = abo ? [] : REITER.map((r) => h('button', {
|
||||||
type: 'button', role: 'tab', class: 'sh-reiter', id: `bk-tab-${r.key}`, 'aria-controls': 'bk-panel', 'data-reiter': r.key,
|
type: 'button', role: 'tab', class: 'sh-reiter', id: `bk-tab-${r.key}`, 'aria-controls': 'bk-panel', 'data-reiter': r.key,
|
||||||
onclick: () => waehleReiter(r.key),
|
onclick: () => waehleReiter(r.key),
|
||||||
}, r.label))
|
}, r.label))
|
||||||
const leiste = h('div', { class: 'sh-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Buchungskalenders' }, knoepfe)
|
const leiste = abo ? null : h('div', { class: 'sh-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Buchungskalenders' }, knoepfe)
|
||||||
leiste.addEventListener('keydown', (ev) => {
|
leiste?.addEventListener('keydown', (ev) => {
|
||||||
const i = REITER.findIndex((r) => r.key === aktiv)
|
const i = REITER.findIndex((r) => r.key === aktiv)
|
||||||
const ziel = { ArrowRight: (i + 1) % REITER.length, ArrowLeft: (i + REITER.length - 1) % REITER.length, Home: 0, End: REITER.length - 1 }[ev.key]
|
const ziel = { ArrowRight: (i + 1) % REITER.length, ArrowLeft: (i + REITER.length - 1) % REITER.length, Home: 0, End: REITER.length - 1 }[ev.key]
|
||||||
if (ziel === undefined) return
|
if (ziel === undefined) return
|
||||||
@@ -197,7 +208,10 @@ function zeigeReiter(s, { status, aktuell, zustand }) {
|
|||||||
k.setAttribute('aria-selected', String(an))
|
k.setAttribute('aria-selected', String(an))
|
||||||
k.tabIndex = an ? 0 : -1
|
k.tabIndex = an ? 0 : -1
|
||||||
}
|
}
|
||||||
panel.setAttribute('aria-labelledby', `bk-tab-${reiter.key}`)
|
if (abo) {
|
||||||
|
panel.setAttribute('aria-label', reiter.label)
|
||||||
|
meldeReiter(s.root, { eintraege: reiterEintraege('buchungen', REITER), aktiv: reiter.key, bei: (neu) => waehleReiter(neu) })
|
||||||
|
} else panel.setAttribute('aria-labelledby', `bk-tab-${reiter.key}`)
|
||||||
zeigeMeldung(s, '')
|
zeigeMeldung(s, '')
|
||||||
panel.replaceChildren()
|
panel.replaceChildren()
|
||||||
reiter.laden()
|
reiter.laden()
|
||||||
@@ -207,6 +221,6 @@ function zeigeReiter(s, { status, aktuell, zustand }) {
|
|||||||
const hinweis = zustand === 'setup' ? 'Wir bauen deinen Buchungskalender gerade ein. Du kannst schon Unterkünfte, Preise und Einstellungen vorbereiten.'
|
const hinweis = zustand === 'setup' ? 'Wir bauen deinen Buchungskalender gerade ein. Du kannst schon Unterkünfte, Preise und Einstellungen vorbereiten.'
|
||||||
: zustand === 'disabled' ? 'Dein Buchungskalender ist abgeschaltet. Du kannst bestehende Buchungen noch ansehen und bearbeiten.' : ''
|
: zustand === 'disabled' ? 'Dein Buchungskalender ist abgeschaltet. Du kannst bestehende Buchungen noch ansehen und bearbeiten.' : ''
|
||||||
const hinweisBox = h('div', { class: 'sh-hinweise' }, hinweis ? h('div', { class: `sh-box ${zustand === 'disabled' ? 'sh-box-warn' : 'sh-box-info'}`, role: 'note', 'data-hinweis': zustand }, hinweis) : null)
|
const hinweisBox = h('div', { class: 'sh-hinweise' }, hinweis ? h('div', { class: `sh-box ${zustand === 'disabled' ? 'sh-box-warn' : 'sh-box-info'}`, role: 'note', 'data-hinweis': zustand }, hinweis) : null)
|
||||||
s.buehne.replaceChildren(hinweisBox, leiste, panel)
|
s.buehne.replaceChildren(...[hinweisBox, leiste, panel].filter(Boolean))
|
||||||
waehleReiter(auswahl.reiter)
|
waehleReiter(auswahl.reiter)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/* Reiter der Funktionsmodule in der neuen Ansicht (Abo-Modell). Im Abo-Modell zeichnen Terminplaner, Webshop, Buchungskalender und
|
||||||
|
Analyse keine eigene Leiste, sondern melden ihre Reiter an die Etage 2 (public/neu/etage.js): CustomEvent 'kn:reiter' mit
|
||||||
|
bubbles und detail { eintraege: [{ key, label, kurz? }], aktiv, bei(key) }; eine leere Liste heisst „keine Reiter“.
|
||||||
|
Dieses Modul hat keine Importe und kennt weder DOM noch die Huelle. */
|
||||||
|
|
||||||
|
/** Kurzlabels fuer schmale Zeilen (C2): modul → { reiterKey: kurzes Wort }. Die Analyse braucht keine (Übersicht, Heatmap, Einstellungen). */
|
||||||
|
export const KURZLABELS = {
|
||||||
|
termine: { team: 'Team' },
|
||||||
|
shop: { uebersicht: 'Start', rechtliches: 'Recht' },
|
||||||
|
buchungen: { unterkuenfte: 'Preise' },
|
||||||
|
}
|
||||||
|
|
||||||
|
/** [{ key, label, … }] des Moduls → [{ key, label, kurz? }]; `kurz` nur mit Eintrag in KURZLABELS, alle anderen Felder fallen weg. */
|
||||||
|
export function reiterEintraege(modul, reiterListe) {
|
||||||
|
const kurz = Object.hasOwn(KURZLABELS, modul) ? KURZLABELS[modul] : {}
|
||||||
|
return reiterListe.map(({ key, label }) => (Object.hasOwn(kurz, key) ? { key, label, kurz: kurz[key] } : { key, label }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sendet 'kn:reiter' aus el. Ohne CustomEvent oder ohne el.dispatchEvent: false, sonst true. */
|
||||||
|
export function meldeReiter(el, { eintraege, aktiv = '', bei = () => {} } = {}) {
|
||||||
|
if (typeof CustomEvent !== 'function' || typeof el?.dispatchEvent !== 'function') return false
|
||||||
|
el.dispatchEvent(new CustomEvent('kn:reiter', { bubbles: true, detail: { eintraege, aktiv, bei } }))
|
||||||
|
return true
|
||||||
|
}
|
||||||
+18
-5
@@ -16,6 +16,7 @@
|
|||||||
import { fehlerKarte, h, infoKarte, request } from './shop-dom.js'
|
import { fehlerKarte, h, infoKarte, request } from './shop-dom.js'
|
||||||
import { anfrageKarte, anfrageZeilen, renderAnfrageFormular } from './shop-anfrage.js'
|
import { anfrageKarte, anfrageZeilen, renderAnfrageFormular } from './shop-anfrage.js'
|
||||||
import { renderBestellen, statusKarte } from './funktion-bestellen.js'
|
import { renderBestellen, statusKarte } from './funktion-bestellen.js'
|
||||||
|
import { meldeReiter, reiterEintraege } from './fn-reiter.js'
|
||||||
|
|
||||||
/** Die Reiter: jeder ein Modul mit render(panel, ctx), erst beim ersten Oeffnen geladen. */
|
/** Die Reiter: jeder ein Modul mit render(panel, ctx), erst beim ersten Oeffnen geladen. */
|
||||||
export const REITER = [
|
export const REITER = [
|
||||||
@@ -48,6 +49,10 @@ let S = null // aktuelle Sitzung der Ansicht; eine alte Antwort darf eine neuere
|
|||||||
|
|
||||||
const zustandVon = (projekt) => projekt?.features?.shop?.state || 'off'
|
const zustandVon = (projekt) => projekt?.features?.shop?.state || 'off'
|
||||||
const text = (wert) => (typeof wert === 'string' ? wert : '')
|
const text = (wert) => (typeof wert === 'string' ? wert : '')
|
||||||
|
// Abo-Modell (Antwort von GET /api/funktionen, strikt): die Reiter zeichnet die Etage, die Ansicht meldet sie (fn-reiter.js)
|
||||||
|
const istAbo = (s) => s.funktionen?.modell === 'abo'
|
||||||
|
/** Eine Ansicht ohne Reiter meldet die leere Liste, damit die Etage nicht die Reiter der vorigen stehen laesst. */
|
||||||
|
const keineReiter = (s) => { if (istAbo(s)) meldeReiter(s.root, { eintraege: [] }) }
|
||||||
|
|
||||||
/* --------------------------------------------------------- Einstieg --- */
|
/* --------------------------------------------------------- Einstieg --- */
|
||||||
/** Rueckkehr aus der Adresse lesen und diese bereinigen: #webshop?bestellt=1&projekt=<id> | #webshop?stripe=zurueck|neu&projekt=<id>. */
|
/** Rueckkehr aus der Adresse lesen und diese bereinigen: #webshop?bestellt=1&projekt=<id> | #webshop?stripe=zurueck|neu&projekt=<id>. */
|
||||||
@@ -81,10 +86,12 @@ async function starte(root, projectId) {
|
|||||||
}
|
}
|
||||||
if (S !== s) return
|
if (S !== s) return
|
||||||
if (!s.funktionen?.shop?.verfuegbar) {
|
if (!s.funktionen?.shop?.verfuegbar) {
|
||||||
|
keineReiter(s)
|
||||||
root.replaceChildren(infoKarte(NICHT_VERFUEGBAR, 'Sobald er für dich bereit ist, findest du ihn hier.'))
|
root.replaceChildren(infoKarte(NICHT_VERFUEGBAR, 'Sobald er für dich bereit ist, findest du ihn hier.'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!s.projects.length) {
|
if (!s.projects.length) {
|
||||||
|
keineReiter(s)
|
||||||
root.replaceChildren(h('div', { class: 'kb-panel kb-empty' }, 'Noch keine Website-Projekte zugewiesen.'))
|
root.replaceChildren(h('div', { class: 'kb-panel kb-empty' }, 'Noch keine Website-Projekte zugewiesen.'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -151,6 +158,7 @@ async function zeigeProjekt(s, optionen = {}) {
|
|||||||
const projekt = s.projekt
|
const projekt = s.projekt
|
||||||
const lauf = ++s.lauf
|
const lauf = ++s.lauf
|
||||||
const aktuell = () => S === s && s.lauf === lauf
|
const aktuell = () => S === s && s.lauf === lauf
|
||||||
|
keineReiter(s)
|
||||||
s.buehne.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
|
s.buehne.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
|
||||||
let status
|
let status
|
||||||
try {
|
try {
|
||||||
@@ -179,6 +187,7 @@ async function ladeNeu(s, optionen = {}) {
|
|||||||
/** Die Ansicht zum Zustand aus dem frisch geladenen Stand (status.state). */
|
/** Die Ansicht zum Zustand aus dem frisch geladenen Stand (status.state). */
|
||||||
async function zeichneAnsicht(s, { status, aktuell, bestellformular = false, meldung = null }) {
|
async function zeichneAnsicht(s, { status, aktuell, bestellformular = false, meldung = null }) {
|
||||||
s.status = status
|
s.status = status
|
||||||
|
keineReiter(s) // die Ansicht mit Reitern (zeigeReiter) meldet ihre Liste gleich danach
|
||||||
if (!status.verfuegbar) {
|
if (!status.verfuegbar) {
|
||||||
s.buehne.replaceChildren(infoKarte(NICHT_VERFUEGBAR, 'Sobald er für dich bereit ist, findest du ihn hier.'))
|
s.buehne.replaceChildren(infoKarte(NICHT_VERFUEGBAR, 'Sobald er für dich bereit ist, findest du ihn hier.'))
|
||||||
return
|
return
|
||||||
@@ -318,15 +327,16 @@ function zeigeReiter(s, { status, aktuell, zustand }) {
|
|||||||
neuLaden,
|
neuLaden,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const abo = istAbo(s) // Abo-Modell: keine eigene Leiste, die Etage zeichnet sie (kn:reiter)
|
||||||
const panel = h('div', { class: 'sh-panel', role: 'tabpanel', id: 'sh-panel', tabindex: '0' })
|
const panel = h('div', { class: 'sh-panel', role: 'tabpanel', id: 'sh-panel', tabindex: '0' })
|
||||||
let aktiv = null // Schluessel des gezeigten Reiters
|
let aktiv = null // Schluessel des gezeigten Reiters
|
||||||
let zaehler = 0 // nur der zuletzt gewaehlte Reiter zeichnet in das Panel
|
let zaehler = 0 // nur der zuletzt gewaehlte Reiter zeichnet in das Panel
|
||||||
const knoepfe = REITER.map((r) => h('button', {
|
const knoepfe = abo ? [] : REITER.map((r) => h('button', {
|
||||||
type: 'button', role: 'tab', class: 'sh-reiter', id: `sh-tab-${r.key}`, 'aria-controls': 'sh-panel', 'data-reiter': r.key,
|
type: 'button', role: 'tab', class: 'sh-reiter', id: `sh-tab-${r.key}`, 'aria-controls': 'sh-panel', 'data-reiter': r.key,
|
||||||
onclick: () => waehleReiter(r.key),
|
onclick: () => waehleReiter(r.key),
|
||||||
}, r.label))
|
}, r.label))
|
||||||
const leiste = h('div', { class: 'sh-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Webshops' }, knoepfe)
|
const leiste = abo ? null : h('div', { class: 'sh-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Webshops' }, knoepfe)
|
||||||
leiste.addEventListener('keydown', (ev) => {
|
leiste?.addEventListener('keydown', (ev) => {
|
||||||
const i = REITER.findIndex((r) => r.key === aktiv)
|
const i = REITER.findIndex((r) => r.key === aktiv)
|
||||||
const ziel = { ArrowRight: (i + 1) % REITER.length, ArrowLeft: (i + REITER.length - 1) % REITER.length, Home: 0, End: REITER.length - 1 }[ev.key]
|
const ziel = { ArrowRight: (i + 1) % REITER.length, ArrowLeft: (i + REITER.length - 1) % REITER.length, Home: 0, End: REITER.length - 1 }[ev.key]
|
||||||
if (ziel === undefined) return
|
if (ziel === undefined) return
|
||||||
@@ -345,7 +355,10 @@ function zeigeReiter(s, { status, aktuell, zustand }) {
|
|||||||
k.setAttribute('aria-selected', String(an))
|
k.setAttribute('aria-selected', String(an))
|
||||||
k.tabIndex = an ? 0 : -1
|
k.tabIndex = an ? 0 : -1
|
||||||
}
|
}
|
||||||
panel.setAttribute('aria-labelledby', `sh-tab-${reiter.key}`)
|
if (abo) {
|
||||||
|
panel.setAttribute('aria-label', reiter.label)
|
||||||
|
meldeReiter(s.root, { eintraege: reiterEintraege('shop', REITER), aktiv: reiter.key, bei: (neu) => waehleReiter(neu) })
|
||||||
|
} else panel.setAttribute('aria-labelledby', `sh-tab-${reiter.key}`)
|
||||||
zeigeMeldung(s, '')
|
zeigeMeldung(s, '')
|
||||||
panel.replaceChildren()
|
panel.replaceChildren()
|
||||||
reiter.laden()
|
reiter.laden()
|
||||||
@@ -353,7 +366,7 @@ function zeigeReiter(s, { status, aktuell, zustand }) {
|
|||||||
.catch((err) => { if (nr === zaehler && aktuell()) panel.replaceChildren(fehlerKarte(err.message, () => waehleReiter(reiter.key))) })
|
.catch((err) => { if (nr === zaehler && aktuell()) panel.replaceChildren(fehlerKarte(err.message, () => waehleReiter(reiter.key))) })
|
||||||
}
|
}
|
||||||
|
|
||||||
s.buehne.replaceChildren(hinweisBox, leiste, panel)
|
s.buehne.replaceChildren(...[hinweisBox, leiste, panel].filter(Boolean))
|
||||||
// vom Onboarding bei Stripe zurueck: die Uebersicht zeigt den Stand der Stripe-Karte
|
// vom Onboarding bei Stripe zurueck: die Uebersicht zeigt den Stand der Stripe-Karte
|
||||||
waehleReiter(s.stripe && zustand !== 'disabled' ? 'uebersicht' : auswahl.reiter, { merken: false })
|
waehleReiter(s.stripe && zustand !== 'disabled' ? 'uebersicht' : auswahl.reiter, { merken: false })
|
||||||
if (s.danke && zustand === 'setup') zeigeMeldung(s, DANKE_SETUP)
|
if (s.danke && zustand === 'setup') zeigeMeldung(s, DANKE_SETUP)
|
||||||
|
|||||||
+21
-6
@@ -23,6 +23,7 @@ import { render as renderEinstellungen } from './termine-tab-einstellungen.js'
|
|||||||
import { renderFragebogen } from './termine-fragebogen.js'
|
import { renderFragebogen } from './termine-fragebogen.js'
|
||||||
import { renderBestellen, statusKarte } from './funktion-bestellen.js'
|
import { renderBestellen, statusKarte } from './funktion-bestellen.js'
|
||||||
import { ZU_VERTRAEGE } from './vertraege-link.js'
|
import { ZU_VERTRAEGE } from './vertraege-link.js'
|
||||||
|
import { meldeReiter, reiterEintraege } from './fn-reiter.js'
|
||||||
|
|
||||||
// Auswahl bleibt beim Wechsel zwischen Funktionen und Terminplaner erhalten (der Tag nicht: er beginnt immer bei heute)
|
// Auswahl bleibt beim Wechsel zwischen Funktionen und Terminplaner erhalten (der Tag nicht: er beginnt immer bei heute)
|
||||||
const auswahl = { projectId: '', reiter: 'termine', ansicht: 'tag', filter: 'alle' }
|
const auswahl = { projectId: '', reiter: 'termine', ansicht: 'tag', filter: 'alle' }
|
||||||
@@ -41,6 +42,11 @@ function leseRueckkehr() {
|
|||||||
return { bestellt: query.get('bestellt') === '1', projekt: query.get('projekt') || '' }
|
return { bestellt: query.get('bestellt') === '1', projekt: query.get('projekt') || '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Abo-Modell (Antwort von GET /api/funktionen, strikt): die Reiter zeichnet die Etage, die Ansicht meldet sie (fn-reiter.js)
|
||||||
|
const istAbo = (s) => s.modell === 'abo'
|
||||||
|
/** Eine Ansicht ohne Reiter meldet die leere Liste, damit die Etage nicht die Reiter der vorigen stehen laesst. */
|
||||||
|
const keineReiter = (s) => { if (istAbo(s)) meldeReiter(s.root, { eintraege: [] }) }
|
||||||
|
|
||||||
const zustandVon = (projekt) => projekt?.features?.termine?.state || 'off'
|
const zustandVon = (projekt) => projekt?.features?.termine?.state || 'off'
|
||||||
const istBestellt = (zustand) => zustand === 'ordered' || zustand === 'setup'
|
const istBestellt = (zustand) => zustand === 'ordered' || zustand === 'setup'
|
||||||
// Entwurf: Funktionsdokument im Zustand Aus mit siteKey (wie die Kachel in funktionen.js)
|
// Entwurf: Funktionsdokument im Zustand Aus mit siteKey (wie die Kachel in funktionen.js)
|
||||||
@@ -52,14 +58,16 @@ export async function initTermine(root, { projectId } = {}) {
|
|||||||
const wunsch = rueckkehr.projekt || projectId || root.dataset.wunschProjekt || '' // die Kachel in Funktionen legt ihr Projekt hier ab
|
const wunsch = rueckkehr.projekt || projectId || root.dataset.wunschProjekt || '' // die Kachel in Funktionen legt ihr Projekt hier ab
|
||||||
delete root.dataset.wunschProjekt
|
delete root.dataset.wunschProjekt
|
||||||
if (wunsch) auswahl.projectId = wunsch
|
if (wunsch) auswahl.projectId = wunsch
|
||||||
const { projects = [], termine } = await request('/api/funktionen')
|
const { projects = [], termine, modell } = await request('/api/funktionen')
|
||||||
const s = { root, projects, termine: termine || null, projekt: null, daten: null, tag: heuteBerlin(), nurLesen: false, lauf: 0, buehne: null, meldungEl: null, danke: false }
|
const s = { root, projects, termine: termine || null, modell, projekt: null, daten: null, tag: heuteBerlin(), nurLesen: false, lauf: 0, buehne: null, meldungEl: null, danke: false }
|
||||||
S = s
|
S = s
|
||||||
if (!termine?.verfuegbar) {
|
if (!termine?.verfuegbar) {
|
||||||
|
keineReiter(s)
|
||||||
root.replaceChildren(infoKarte('Der Terminplaner ist noch nicht verfügbar', 'Sobald er für dich bereit ist, findest du ihn hier.'))
|
root.replaceChildren(infoKarte('Der Terminplaner ist noch nicht verfügbar', 'Sobald er für dich bereit ist, findest du ihn hier.'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!projects.length) {
|
if (!projects.length) {
|
||||||
|
keineReiter(s)
|
||||||
root.replaceChildren(h('div', { class: 'kb-panel kb-empty' }, 'Noch keine Website-Projekte zugewiesen.'))
|
root.replaceChildren(h('div', { class: 'kb-panel kb-empty' }, 'Noch keine Website-Projekte zugewiesen.'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -102,6 +110,7 @@ async function ladeFunktionen(s) {
|
|||||||
const projects = Array.isArray(antwort.projects) ? antwort.projects : []
|
const projects = Array.isArray(antwort.projects) ? antwort.projects : []
|
||||||
s.projects = projects
|
s.projects = projects
|
||||||
s.termine = antwort.termine || null
|
s.termine = antwort.termine || null
|
||||||
|
s.modell = antwort.modell
|
||||||
s.projekt = projects.find((p) => p.projectId === s.projekt?.projectId) || s.projekt
|
s.projekt = projects.find((p) => p.projectId === s.projekt?.projectId) || s.projekt
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +132,7 @@ async function zeigeProjekt(s, optionen = {}) {
|
|||||||
const projekt = s.projekt
|
const projekt = s.projekt
|
||||||
const lauf = ++s.lauf
|
const lauf = ++s.lauf
|
||||||
const aktuell = () => S === s && s.lauf === lauf
|
const aktuell = () => S === s && s.lauf === lauf
|
||||||
|
keineReiter(s)
|
||||||
s.buehne.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
|
s.buehne.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
|
||||||
let daten
|
let daten
|
||||||
try {
|
try {
|
||||||
@@ -155,6 +165,7 @@ async function zeichneAnsicht(s, { daten, aktuell, bestellformular = false, meld
|
|||||||
s.daten = daten
|
s.daten = daten
|
||||||
const zustand = daten.state || zustandVon(projekt)
|
const zustand = daten.state || zustandVon(projekt)
|
||||||
s.nurLesen = zustand !== 'active' && zustand !== 'off'
|
s.nurLesen = zustand !== 'active' && zustand !== 'off'
|
||||||
|
keineReiter(s) // die Ansichten mit Reitern (zeigeReiter) melden ihre Liste gleich danach
|
||||||
if (zustand === 'off') {
|
if (zustand === 'off') {
|
||||||
if (daten.abo?.ok === true) zeigeFragebogen(s, { projekt, aktuell })
|
if (daten.abo?.ok === true) zeigeFragebogen(s, { projekt, aktuell })
|
||||||
else await zeigeAboHinweis(s, { aktuell })
|
else await zeigeAboHinweis(s, { aktuell })
|
||||||
@@ -386,14 +397,15 @@ function zeigeReiter(s, { projekt, aktuell, zustand = s.nurLesen ? 'disabled' :
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const abo = istAbo(s) // Abo-Modell: keine eigene Leiste, die Etage zeichnet sie (kn:reiter)
|
||||||
const panel = h('div', { class: 'tm-panel', role: 'tabpanel', id: 'tm-panel', tabindex: '0' })
|
const panel = h('div', { class: 'tm-panel', role: 'tabpanel', id: 'tm-panel', tabindex: '0' })
|
||||||
let aktiv = null // Schluessel des gezeigten Reiters
|
let aktiv = null // Schluessel des gezeigten Reiters
|
||||||
const knoepfe = reiterListe.map((r) => h('button', {
|
const knoepfe = abo ? [] : reiterListe.map((r) => h('button', {
|
||||||
type: 'button', role: 'tab', class: 'tm-reiter', id: `tm-tab-${r.key}`, 'aria-controls': 'tm-panel', 'data-reiter': r.key,
|
type: 'button', role: 'tab', class: 'tm-reiter', id: `tm-tab-${r.key}`, 'aria-controls': 'tm-panel', 'data-reiter': r.key,
|
||||||
onclick: () => waehleReiter(r.key),
|
onclick: () => waehleReiter(r.key),
|
||||||
}, r.label))
|
}, r.label))
|
||||||
const leiste = h('div', { class: 'tm-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Terminplaners' }, knoepfe)
|
const leiste = abo ? null : h('div', { class: 'tm-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Terminplaners' }, knoepfe)
|
||||||
leiste.addEventListener('keydown', (ev) => {
|
leiste?.addEventListener('keydown', (ev) => {
|
||||||
const i = reiterListe.findIndex((r) => r.key === aktiv)
|
const i = reiterListe.findIndex((r) => r.key === aktiv)
|
||||||
const ziel = { ArrowRight: (i + 1) % reiterListe.length, ArrowLeft: (i + reiterListe.length - 1) % reiterListe.length, Home: 0, End: reiterListe.length - 1 }[ev.key]
|
const ziel = { ArrowRight: (i + 1) % reiterListe.length, ArrowLeft: (i + reiterListe.length - 1) % reiterListe.length, Home: 0, End: reiterListe.length - 1 }[ev.key]
|
||||||
if (ziel === undefined) return
|
if (ziel === undefined) return
|
||||||
@@ -412,7 +424,10 @@ function zeigeReiter(s, { projekt, aktuell, zustand = s.nurLesen ? 'disabled' :
|
|||||||
k.setAttribute('aria-selected', String(an))
|
k.setAttribute('aria-selected', String(an))
|
||||||
k.tabIndex = an ? 0 : -1
|
k.tabIndex = an ? 0 : -1
|
||||||
}
|
}
|
||||||
panel.setAttribute('aria-labelledby', `tm-tab-${reiter.key}`)
|
if (abo) {
|
||||||
|
panel.setAttribute('aria-label', reiter.label)
|
||||||
|
meldeReiter(s.root, { eintraege: reiterEintraege('termine', reiterListe), aktiv: reiter.key, bei: (neu) => waehleReiter(neu) })
|
||||||
|
} else panel.setAttribute('aria-labelledby', `tm-tab-${reiter.key}`)
|
||||||
zeigeMeldung(s, '')
|
zeigeMeldung(s, '')
|
||||||
panel.replaceChildren()
|
panel.replaceChildren()
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { test } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { KURZLABELS, meldeReiter, reiterEintraege } from '../public/fn-reiter.js'
|
||||||
|
import { h } from '../public/termine-dom.js'
|
||||||
|
import { initTermine } from '../public/termine.js'
|
||||||
|
import { initShop, REITER as SHOP_REITER, vergissAuswahl as vergissShop } from '../public/shop.js'
|
||||||
|
import { initBuchungen, REITER as BUCHUNGEN_REITER, vergissAuswahl as vergissBuchungen } from '../public/buchungen.js'
|
||||||
|
import { hoereAufKn, mitDom, routen } from './support/modulAbo.js'
|
||||||
|
|
||||||
|
// Reiter der Funktionsmodule im Abo-Modell (P4 Task 1, Spec 5.2/5.4, C2): keine eigene Leiste, kn:reiter an die Etage, Kurzlabels.
|
||||||
|
// Ohne modell === 'abo' bleibt die eigene Leiste mit den langen Labels und es gibt kein Ereignis.
|
||||||
|
|
||||||
|
test('KURZLABELS: genau diese Einträge (C2)', () => {
|
||||||
|
assert.deepEqual(KURZLABELS, { termine: { team: 'Team' }, shop: { uebersicht: 'Start', rechtliches: 'Recht' }, buchungen: { unterkuenfte: 'Preise' } })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('reiterEintraege: Kurzlabel nur mit Eintrag, fremde Felder fallen weg', () => {
|
||||||
|
const liste = [{ key: 'termine', label: 'Termine', render() {} }, { key: 'team', label: 'Team & Plätze', render() {} }]
|
||||||
|
assert.deepEqual(reiterEintraege('termine', liste), [{ key: 'termine', label: 'Termine' }, { key: 'team', label: 'Team & Plätze', kurz: 'Team' }])
|
||||||
|
assert.deepEqual(reiterEintraege('unbekannt', liste.slice(1)), [{ key: 'team', label: 'Team & Plätze' }])
|
||||||
|
assert.deepEqual(reiterEintraege('analyse', [{ key: 'heatmap', label: 'Heatmap' }]), [{ key: 'heatmap', label: 'Heatmap' }], 'Analyse: kein Kurzlabel nötig')
|
||||||
|
assert.deepEqual(reiterEintraege('constructor', [{ key: 'toString', label: 'X' }]), [{ key: 'toString', label: 'X' }], 'keine Treffer aus dem Prototyp')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('meldeReiter: das Ereignis steigt auf und trägt Einträge, aktiv und bei; ohne Element tut es nichts', mitDom(async (fx) => {
|
||||||
|
const k = hoereAufKn(fx)
|
||||||
|
const el = h('div')
|
||||||
|
document.body.append(el)
|
||||||
|
const bei = () => {}
|
||||||
|
assert.equal(meldeReiter(el, { eintraege: [{ key: 'a', label: 'A' }], aktiv: 'a', bei }), true)
|
||||||
|
assert.deepEqual(k.reiter, [{ eintraege: [{ key: 'a', label: 'A' }], aktiv: 'a', bei }])
|
||||||
|
assert.equal(meldeReiter(null, { eintraege: [] }), false)
|
||||||
|
assert.equal(meldeReiter({}, { eintraege: [] }), false, 'ohne dispatchEvent')
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------------------------------- die drei Module
|
||||||
|
const projekt = (features) => ({ projectId: 'p1', projectName: 'Mein Laden', features })
|
||||||
|
const funktionen = (modell, kopf, features) => ({ body: { ...(modell === undefined ? {} : { modell }), projects: [projekt(features)], ...kopf } })
|
||||||
|
|
||||||
|
const FAELLE = [
|
||||||
|
{
|
||||||
|
name: 'Terminplaner',
|
||||||
|
init: initTermine,
|
||||||
|
vorher: () => {},
|
||||||
|
tabelle: (modell) => ({
|
||||||
|
'GET /api/funktionen': funktionen(modell, { termine: { verfuegbar: true } }, { termine: { state: 'active', siteKey: 'abcdefghijklmnop' } }),
|
||||||
|
'GET /api/termine/einstellungen?projectId=p1': { body: { state: 'active', einstellung: {}, fehlt: [] } },
|
||||||
|
}),
|
||||||
|
alt: ['Termine', 'Leistungen', 'Team & Plätze', 'Einstellungen'],
|
||||||
|
kurz: ['Termine', 'Leistungen', 'Team', 'Einstellungen'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Webshop',
|
||||||
|
init: initShop,
|
||||||
|
vorher: vergissShop,
|
||||||
|
tabelle: (modell) => ({
|
||||||
|
'GET /api/funktionen': funktionen(modell, { shop: { verfuegbar: true }, kundenart: 'unternehmen' }, { shop: {} }),
|
||||||
|
'GET /api/shop/status?projectId=p1': { body: { verfuegbar: true, state: 'active', anfrage: null } },
|
||||||
|
}),
|
||||||
|
alt: ['Übersicht', 'Artikel', 'Bestellungen', 'Einstellungen', 'Rechtliches'],
|
||||||
|
kurz: ['Start', 'Artikel', 'Bestellungen', 'Einstellungen', 'Recht'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Buchungskalender',
|
||||||
|
init: initBuchungen,
|
||||||
|
vorher: vergissBuchungen,
|
||||||
|
tabelle: (modell) => ({
|
||||||
|
'GET /api/funktionen': funktionen(modell, { unterkunft: { verfuegbar: true } }, { unterkunft: { state: 'active' } }),
|
||||||
|
'GET /api/unterkunft/status?projectId=p1': { body: { state: 'active' } },
|
||||||
|
}),
|
||||||
|
alt: ['Anfragen', 'Kalender', 'Unterkünfte & Preise', 'Rechnungen', 'Einstellungen'],
|
||||||
|
kurz: ['Anfragen', 'Kalender', 'Preise', 'Rechnungen', 'Einstellungen'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async function oeffne(fx, f, modell) {
|
||||||
|
f.vorher?.()
|
||||||
|
routen(fx, f.tabelle(modell))
|
||||||
|
const root = h('div')
|
||||||
|
document.body.append(root)
|
||||||
|
await f.init(root, { projectId: 'p1' })
|
||||||
|
await fx.ruhe()
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const f of FAELLE) {
|
||||||
|
test(`${f.name}, Abo: keine eigene Leiste, kn:reiter mit Kurzlabels, bei() wechselt`, mitDom(async (fx) => {
|
||||||
|
const k = hoereAufKn(fx)
|
||||||
|
const root = await oeffne(fx, f, 'abo')
|
||||||
|
assert.equal(fx.finde(root, '[role="tablist"]'), null)
|
||||||
|
assert.equal(fx.finde(root, '[role="tab"]'), null)
|
||||||
|
const panel = fx.finde(root, '[role="tabpanel"]')
|
||||||
|
assert.ok(panel, 'das Panel bleibt')
|
||||||
|
assert.equal(panel.hasAttribute('aria-labelledby'), false)
|
||||||
|
assert.ok(panel.getAttribute('aria-label'), 'das Panel trägt aria-label')
|
||||||
|
const erst = k.reiter.at(-1)
|
||||||
|
assert.deepEqual(erst.eintraege.map((e) => e.label), f.alt)
|
||||||
|
assert.deepEqual(erst.eintraege.map((e) => e.kurz ?? e.label), f.kurz)
|
||||||
|
const andere = erst.eintraege.find((e) => e.key !== erst.aktiv).key
|
||||||
|
erst.bei(andere)
|
||||||
|
await fx.ruhe()
|
||||||
|
assert.equal(k.reiter.at(-1).aktiv, andere)
|
||||||
|
assert.equal(panel.getAttribute('aria-label'), erst.eintraege.find((e) => e.key === andere).label)
|
||||||
|
}))
|
||||||
|
|
||||||
|
test(`${f.name}, ohne modell (fehlt, „Abo“, „abo “): eigene Leiste mit langen Labels, kein Ereignis`, mitDom(async (fx) => {
|
||||||
|
for (const modell of [undefined, 'Abo', 'abo ']) {
|
||||||
|
const k = hoereAufKn(fx)
|
||||||
|
const root = await oeffne(fx, f, modell)
|
||||||
|
assert.deepEqual(fx.alle(root, '[role="tab"]').map((t) => fx.text(t)), f.alt, String(modell))
|
||||||
|
assert.ok(fx.finde(root, '[role="tablist"]'))
|
||||||
|
assert.equal(k.reiter.length, 0, String(modell))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
test('die langen Labels der Reiterlisten bleiben unverändert (Pins buchungenUi und shopView)', () => {
|
||||||
|
assert.deepEqual(SHOP_REITER.map((r) => r.label), FAELLE[1].alt)
|
||||||
|
assert.deepEqual(BUCHUNGEN_REITER.map((r) => r.label), FAELLE[2].alt)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Webshop im Abo-Modell, Zustand off: die Reiterliste ist leer', mitDom(async (fx) => {
|
||||||
|
const k = hoereAufKn(fx)
|
||||||
|
vergissShop()
|
||||||
|
routen(fx, {
|
||||||
|
'GET /api/funktionen': funktionen('abo', { shop: { verfuegbar: true }, kundenart: 'unternehmen' }, { shop: { state: 'off' } }),
|
||||||
|
'GET /api/shop/status?projectId=p1': { body: { verfuegbar: true, state: 'off', anfrage: null } },
|
||||||
|
})
|
||||||
|
const root = h('div')
|
||||||
|
document.body.append(root)
|
||||||
|
await initShop(root, { projectId: 'p1' })
|
||||||
|
await fx.ruhe()
|
||||||
|
assert.ok(k.reiter.length > 0, 'die Ansicht meldet sich')
|
||||||
|
assert.deepEqual(k.reiter.at(-1).eintraege, [])
|
||||||
|
}))
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
// Testhilfen fuer die Funktionsmodule im Abo-Modell (P4 „Funktionsmodule“): Fake-DOM mit CustomEvent, Antworten nach Adresse, Attrappe fuer den
|
||||||
|
// Einschalt-Ablauf (zeigeEinschalten aus public/neu/funktion-einschalten.js), Wurzeln fuer die innerHTML-Module (News, Analyse).
|
||||||
|
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { pathToFileURL } from 'node:url'
|
||||||
|
import { installFakeDom } from './fakeDom.js'
|
||||||
|
|
||||||
|
/** Wörter, die im Abo-Modell auf keiner Oberfläche der Funktionen stehen dürfen (Spec: keine Preise für Funktionen). */
|
||||||
|
export const KEIN_PREIS = /€|einmalig|Preis|Zahlungspflichtig|Bestellseite|Behalten/
|
||||||
|
|
||||||
|
/** CustomEvent fuer fakeDom: type, detail, bubbles, target, ergebnisse, preventDefault, stopPropagation. */
|
||||||
|
class FakeCustomEvent {
|
||||||
|
constructor(type, init = {}) {
|
||||||
|
this.type = type
|
||||||
|
this.detail = init.detail ?? null
|
||||||
|
this.bubbles = init.bubbles === true
|
||||||
|
this.target = null
|
||||||
|
this.currentTarget = null
|
||||||
|
this.defaultPrevented = false
|
||||||
|
this.stopped = false
|
||||||
|
this.ergebnisse = []
|
||||||
|
}
|
||||||
|
|
||||||
|
preventDefault() { this.defaultPrevented = true }
|
||||||
|
|
||||||
|
stopPropagation() { this.stopped = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Testrumpf: Fake-DOM an, CustomEvent der Fake-Klasse, nach dem Test alles zurück. fn(fx). */
|
||||||
|
export const mitDom = (fn) => async () => {
|
||||||
|
const fx = installFakeDom()
|
||||||
|
const davor = Object.getOwnPropertyDescriptor(globalThis, 'CustomEvent')
|
||||||
|
Object.defineProperty(globalThis, 'CustomEvent', { value: FakeCustomEvent, writable: true, configurable: true, enumerable: true })
|
||||||
|
try {
|
||||||
|
await fn(fx)
|
||||||
|
await fx.ruhe()
|
||||||
|
} finally {
|
||||||
|
fx.restore()
|
||||||
|
if (davor) Object.defineProperty(globalThis, 'CustomEvent', davor)
|
||||||
|
else delete globalThis.CustomEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lauscht am document.body auf kn:reiter. => { reiter: [detail, …] } (in der Reihenfolge des Eintreffens). */
|
||||||
|
export function hoereAufKn(fx) {
|
||||||
|
const k = { reiter: [] }
|
||||||
|
fx.document.body.addEventListener('kn:reiter', (ev) => { k.reiter.push(ev.detail) })
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ersetzt fetch: Antworten nach 'METHODE /pfad?query'. Wert: { status?, body } | Array (der Reihe nach, der letzte gilt weiter) |
|
||||||
|
* (aufruf) => Wert. Unbekannte Adressen: 404 und Eintrag in fx.unbekannt. Jeder Aufruf steht in fx.fetchLog.
|
||||||
|
*/
|
||||||
|
export function routen(fx, tabelle) {
|
||||||
|
const felder = new Map(Object.entries(tabelle).map(([schluessel, wert]) => [schluessel, Array.isArray(wert) ? [...wert] : wert]))
|
||||||
|
fx.unbekannt = []
|
||||||
|
globalThis.fetch = async (url, optionen = {}) => {
|
||||||
|
const aufruf = { method: optionen.method || 'GET', url: String(url), body: optionen.body ? JSON.parse(optionen.body) : undefined }
|
||||||
|
fx.fetchLog.push(aufruf)
|
||||||
|
const schluessel = `${aufruf.method} ${aufruf.url}`
|
||||||
|
let eintrag = felder.get(schluessel)
|
||||||
|
if (Array.isArray(eintrag)) eintrag = eintrag.length > 1 ? eintrag.shift() : eintrag[0]
|
||||||
|
if (typeof eintrag === 'function') eintrag = eintrag(aufruf)
|
||||||
|
if (eintrag === undefined) {
|
||||||
|
fx.unbekannt.push(schluessel)
|
||||||
|
eintrag = { status: 404, body: { error: `nicht vorgesehen: ${schluessel}` } }
|
||||||
|
}
|
||||||
|
const { status = 200, body = {} } = eintrag
|
||||||
|
return { ok: status >= 200 && status < 300, status, json: async () => structuredClone(body) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attrappe fuer zeigeEinschalten. fn(platz, { key, projectId, viewAs, onFertig }) zeichnet nichts und merkt den Aufruf.
|
||||||
|
* fertig(i, ergebnis) ruft onFertig des i-ten Aufrufs.
|
||||||
|
*/
|
||||||
|
export function einschaltenAttrappe() {
|
||||||
|
const aufrufe = []
|
||||||
|
const fn = (wurzel, o = {}) => {
|
||||||
|
aufrufe.push({ wurzel, key: o.key, projectId: o.projectId, viewAs: o.viewAs, onFertig: o.onFertig })
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
const fertig = (i = 0, ergebnis = { feature: { state: 'setup' }, ticketWoid: null }) => aufrufe[i].onFertig(ergebnis)
|
||||||
|
return { fn, aufrufe, fertig }
|
||||||
|
}
|
||||||
|
|
||||||
|
function attrappe() {
|
||||||
|
const handlers = {}
|
||||||
|
return {
|
||||||
|
innerHTML: '',
|
||||||
|
textContent: '',
|
||||||
|
disabled: false,
|
||||||
|
checked: false,
|
||||||
|
value: '',
|
||||||
|
dataset: {},
|
||||||
|
classList: { add() {}, remove() {}, toggle() {} },
|
||||||
|
handlers,
|
||||||
|
addEventListener(typ, fn) { handlers[typ] = fn },
|
||||||
|
querySelector: () => null,
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wurzel fuer die innerHTML-Module (News, Analyse): { root, stage, kn }. root.querySelector(stageSelektor) liefert stage, '.an' die Attrappe `an`.
|
||||||
|
* stage.els['#id'] entsteht beim ersten querySelector('#id'). root.dispatchEvent legt die Meldungen 'kn:reiter' (ev.detail) in kn.reiter ab.
|
||||||
|
*/
|
||||||
|
export function htmlWurzel(stageSelektor) {
|
||||||
|
const els = {}
|
||||||
|
const attribute = {}
|
||||||
|
const stage = {
|
||||||
|
els,
|
||||||
|
attribute,
|
||||||
|
innerHTML: '',
|
||||||
|
querySelector: (selektor) => (els[selektor] ||= attrappe()),
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
setAttribute: (name, wert) => { attribute[name] = String(wert) },
|
||||||
|
getAttribute: (name) => (Object.hasOwn(attribute, name) ? attribute[name] : null),
|
||||||
|
}
|
||||||
|
const an = attrappe()
|
||||||
|
const kn = { reiter: [] }
|
||||||
|
const root = {
|
||||||
|
innerHTML: '',
|
||||||
|
dataset: {},
|
||||||
|
querySelector: (selektor) => (selektor === stageSelektor ? stage : selektor === '.an' ? an : null),
|
||||||
|
dispatchEvent(ev) {
|
||||||
|
if (ev?.type === 'kn:reiter') kn.reiter.push(ev.detail)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return { root, stage, kn }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* public/funktionen.js laden, alle statischen Importe (absolut /x.js oder relativ ./x.js) auf die Dateien in public/ umbiegen
|
||||||
|
* (Muster funktionenZeitenTile.test.js). => das Modul.
|
||||||
|
*/
|
||||||
|
export async function ladeFunktionenModul() {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'fn-modul-abo-'))
|
||||||
|
const dateiUrl = (name) => pathToFileURL(new URL(`../../public/${name}`, import.meta.url).pathname).href
|
||||||
|
const quelle = readFileSync(new URL('../../public/funktionen.js', import.meta.url), 'utf8')
|
||||||
|
const src = quelle.replace(/(\bfrom\s+)'(?:\/|\.\/)([^']+)'/g, (_, von, name) => `${von}'${dateiUrl(name)}'`)
|
||||||
|
const datei = join(dir, 'funktionen.mjs')
|
||||||
|
writeFileSync(datei, src)
|
||||||
|
return import(pathToFileURL(datei).href)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user