/** * — iframe-based markwhen viewer. * * Two modes: * - `src` attribute: fetch a server-rendered `/render` endpoint (preferred) * - `text` property: render an ad-hoc .mw string via the server-side * render pipeline, delivered via srcdoc for isolation. * * The upstream @markwhen/{timeline,calendar} packages are pre-built Vue * apps, not importable libraries — so the iframe route is the sane embed. */ export type MarkwhenViewMode = 'timeline' | 'calendar'; export class RstackMarkwhenView extends HTMLElement { static get observedAttributes() { return ['src', 'view']; } #src = ''; #view: MarkwhenViewMode = 'timeline'; #shadow: ShadowRoot; #iframe: HTMLIFrameElement; constructor() { super(); this.#shadow = this.attachShadow({ mode: 'open' }); this.#shadow.innerHTML = ` `; this.#iframe = this.#shadow.querySelector('iframe')!; } set src(v: string) { this.#src = v; this.#render(); } get src() { return this.#src; } set view(v: MarkwhenViewMode) { this.#view = v; this.#render(); } get view() { return this.#view; } attributeChangedCallback(name: string, _old: string | null, next: string | null) { if (name === 'src') this.src = next ?? ''; if (name === 'view') this.view = (next === 'calendar' ? 'calendar' : 'timeline'); } connectedCallback() { this.#render(); } #render() { if (!this.#src) { this.#iframe.removeAttribute('src'); this.#iframe.srcdoc = `No timeline source yet.`; return; } // Append view param if caller used a bare endpoint URL. const url = new URL(this.#src, window.location.origin); if (!url.searchParams.has('view')) url.searchParams.set('view', this.#view); this.#iframe.removeAttribute('srcdoc'); this.#iframe.src = url.pathname + url.search; } } if (!customElements.get('rstack-markwhen-view')) { customElements.define('rstack-markwhen-view', RstackMarkwhenView); }