import { ExportFormat, exportScenario, type GeneratedState, generatedStateToString, getResultFrom, RolledValues, RollSelections, type RollTable, RollTableDatabase, type RollTableResult } from '../common/rolltable'; import { buildGeneratedElement, copyBBID, copyEmojiTextID, copyMDID, copyTextID, htmlTableIdentifier, rerollAllId, rerollId, selectAllId, selectedIdPrefix, selectNoneId } from '../common/template'; import { DOMLoaded } from './onload'; import { scrapeGeneratedScenario } from './scraper'; import { showPopup } from './popup'; import { pulseElement } from './pulse'; export class Generator { readonly generator: HTMLElement; readonly scenario: HTMLUListElement; readonly copyButtons: HTMLElement; readonly rollButtons: HTMLElement; readonly db: RollTableDatabase | undefined; private readonly rolled = new RolledValues(); private readonly selected = new RollSelections(); get state(): GeneratedState { return { final: false, rolled: this.rolled, selected: this.selected, } } getTableWithHtmlId(id: string, prefix?: string): RollTable | undefined { return Array.from(this.rolled.keys()).find(t => id === ((prefix ?? '') + htmlTableIdentifier(t))); } selectAll(): this { this.selected.clear(); for (const check of this.scenario.querySelectorAll('input[type=checkbox]') as Iterable) { check.checked = true; pulseElement(check); const table = this.getTableWithHtmlId(check.id, selectedIdPrefix); if (table) { this.selected.add(table); } } return this } selectNone(): this { this.selected.clear(); for (const check of this.scenario.querySelectorAll('input[type=checkbox]') as Iterable) { check.checked = false; pulseElement(check); } return this } loadValuesFromDOM(): this { this.rolled.clear() this.selected.clear() const scenario = scrapeGeneratedScenario(this.scenario) if (!scenario) { throw Error("Failed to load generated values from DOM") } for (const [scrapedTable, scrapedResult] of scenario.rolled) { const table = this.db?.getTableMatching(scrapedTable) ?? scrapedTable const result = getResultFrom(table, scrapedResult) if (scenario.selected.has(scrapedTable)) { this.selected.add(table) } this.rolled.add(result) } return this } attachHandlers(): this { this.generator.addEventListener('click', (e) => this.clickHandler(e)); this.generator.addEventListener('change', (e) => this.changeHandler(e)); return this; } async copy(format: ExportFormat): Promise { const exported = exportScenario(Array.from(this.rolled.values()), format) return navigator.clipboard.writeText(exported) } private clickHandler(e: Event): void { if (e.target instanceof HTMLButtonElement || e.target instanceof HTMLAnchorElement) { switch (e.target.id) { case selectNoneId: this.selectNone() break case selectAllId: this.selectAll() break case copyMDID: this.copy(ExportFormat.Markdown) .then(() => showPopup(this.copyButtons, `Copied Markdown to clipboard!`, 'success')) .catch((e) => { console.error("Failed while copying Markdown:", e) showPopup(this.copyButtons, `Failed to copy Markdown to clipboard`, 'error') }) break case copyBBID: this.copy(ExportFormat.BBCode) .then(() => showPopup(this.copyButtons, `Copied BBCode to clipboard!`, 'success')) .catch((e) => { console.error("Failed while copying BBCode:", e) showPopup(this.copyButtons, `Failed to copy BBCode to clipboard`, 'error') }) break case copyEmojiTextID: this.copy(ExportFormat.TextEmoji) .then(() => showPopup(this.copyButtons, `Copied text (with emojis) to clipboard!`, 'success')) .catch((e) => { console.error("Failed while copying text (with emojis):", e) showPopup(this.copyButtons, `Failed to copy text (with emojis) to clipboard`, 'error') }) break case copyTextID: this.copy(ExportFormat.TextOnly) .then(() => showPopup(this.copyButtons, `Copied text to clipboard!`, 'success')) .catch((e) => { console.error("Failed while copying text:", e) showPopup(this.copyButtons, `Failed to copy text to clipboard`, 'error') }) break case rerollId: for (const row of this.scenario.querySelectorAll(".generatedElement")) { if (row.querySelector("input[type=checkbox]:checked")) { const text = row.querySelector(".resultText") if (text) { pulseElement(text) } } } showPopup(this.rollButtons, `only pretending to reroll`, 'warning') break case rerollAllId: for (const row of this.scenario.querySelectorAll(".generatedElement")) { const check = row.querySelector("input[type=checkbox]:checked") if (check) { check.checked = false pulseElement(check) } const text = row.querySelector(".resultText") if (text) { pulseElement(text) } } showPopup(this.rollButtons, `only pretending to reroll all`, 'warning') break default: if (e.target.classList.contains("resultText")) { for (let target: HTMLElement|null = e.target; target && target !== this.generator; target = target.parentElement) { if (target.classList.contains("generatedElement")) { const check = target.querySelector(".generatedSelect") if (check) { check.click() } } } } else { return } } e.preventDefault() } } private changeHandler(e: Event): void { if (e.target instanceof HTMLInputElement && e.target.type === 'checkbox' && e.target.id.startsWith(selectedIdPrefix)) { const check = e.target const table = this.getTableWithHtmlId(check.id, selectedIdPrefix); if (table) { if (check.checked) { this.selected.add(table); } else { this.selected.delete(table); } pulseElement(check) } } } private animationendHandler(e: AnimationEvent): void { if (e.animationName === "pulse" && e.target instanceof HTMLElement && e.target.classList.contains("pulse")) { e.target.classList.remove("pulse") } } constructor(generator: HTMLElement, generatorForm: HTMLUListElement, copyButtons: HTMLElement, rollButtons: HTMLElement, db?: RollTableDatabase) { this.generator = generator; this.scenario = generatorForm; this.copyButtons = copyButtons; this.rollButtons = rollButtons; this.db = db; } } function initGenerator(db?: RollTableDatabase): Generator { const generatorFound = document.getElementById('generator'); if (!generatorFound) { throw Error('generator was not found'); } const generatedScenarioFound = document.getElementById('generatedScenario'); if (!generatedScenarioFound || !(generatedScenarioFound instanceof HTMLUListElement)) { throw Error('generated scenario was not found'); } const copyButtons = document.getElementById("copyButtons") if (!copyButtons) { throw Error('copy buttons were not found') } const rollButtons = document.getElementById("rollButtons") if (!rollButtons) { throw Error('copy buttons were not found') } return new Generator(generatorFound, generatedScenarioFound, copyButtons, rollButtons, db).loadValuesFromDOM().attachHandlers(); } let pendingGenerator: Promise|undefined = undefined export async function prepareGenerator(db?: Promise): Promise { if (pendingGenerator) { throw Error(`prepareGenerator should only be called once`) } pendingGenerator = DOMLoaded.then(() => db) .then((promisedDb) => initGenerator(promisedDb)) return pendingGenerator } DOMLoaded.then(() => pendingGenerator ?? prepareGenerator()) .then(g => console.info(`loaded generator: ${generatedStateToString(g.state)}`)) .catch(e => console.error('failed to load generator', e))