diff --git a/pages/components/components.js b/pages/components/components.js index 4c5c8ee..9b116c2 100644 --- a/pages/components/components.js +++ b/pages/components/components.js @@ -20,6 +20,12 @@ class AboutElement extends InsertHTMLElement { } } +class ReportMaskElement extends InsertHTMLElement { + constructor() { + super("components/report-mask.html"); + } +} + class InChIToolsElement extends InsertHTMLElement { constructor() { super("components/inchi-tools.html"); @@ -541,6 +547,7 @@ class NGLViewerElement extends HTMLElement { customElements.define("inchi-about", AboutElement); customElements.define("inchi-inchi-tools", InChIToolsElement); +customElements.define("report-mask", ReportMaskElement); customElements.define("inchi-rinchi-tools", RInChIToolsElement); customElements.define("inchi-version-selection", InChIVersionSelectionElement); customElements.define("inchi-result-field", InChIResultFieldElement); diff --git a/pages/components/inchi-tools.html b/pages/components/inchi-tools.html index cbfbc13..54636f7 100644 --- a/pages/components/inchi-tools.html +++ b/pages/components/inchi-tools.html @@ -100,6 +100,7 @@ title="AuxInfo" > + diff --git a/pages/components/report-mask.html b/pages/components/report-mask.html new file mode 100644 index 0000000..6ce328c --- /dev/null +++ b/pages/components/report-mask.html @@ -0,0 +1,44 @@ + + + diff --git a/pages/css/report-mask.css b/pages/css/report-mask.css new file mode 100644 index 0000000..6e5f760 --- /dev/null +++ b/pages/css/report-mask.css @@ -0,0 +1,81 @@ +.mask-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + display: none; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.mask-overlay.open { + display: flex; +} + +.mask-dialog { + background: #fff; + padding: 20px; + width: 90%; + max-width: 420px; + border-radius: 8px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); +} + +.mask-dialog header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.mask-dialog h2 { + margin: 0; + font-size: 1.1rem; +} + +.mask-close { + background: transparent; + border: none; + font-size: 1.25rem; + cursor: pointer; +} + +.mask-dialog form { + display: flex; + flex-direction: column; +} + +.mask-dialog input[type="text"] { + padding: 8px 10px; + margin-bottom: 12px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.mask-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 10px; +} + +.btn-primary { + background: #00612c; + color: white; + border: none; + padding: 8px 14px; + border-radius: 4px; + cursor: pointer; +} + +.btn-secondary { + background: transparent; + border: 1px solid #ccc; + padding: 8px 14px; + border-radius: 4px; + cursor: pointer; +} + +.visually-hidden { + display: none !important; +} diff --git a/pages/index.html b/pages/index.html index 614c8b1..f9d90d7 100644 --- a/pages/index.html +++ b/pages/index.html @@ -165,5 +165,6 @@

InChI Web Demo

> + diff --git a/pages/index.js b/pages/index.js index 2c472ad..678071d 100644 --- a/pages/index.js +++ b/pages/index.js @@ -785,3 +785,227 @@ function throttleMap(inputs, mapper, maxConcurrent = 5) { next(); }); } + +async function openReportMask() { + let reportMaskHost = document.querySelector("report-mask"); + + initReportMask(reportMaskHost); + + // If host exposes open(), call it directly and pass structure via an event. + if (reportMaskHost.open && typeof reportMaskHost.open === "function") { + try { + reportMaskHost.open(); + return; + } catch (err) { + console.error("Error calling reportMaskHost.open():", err); + } + } + + // If we fall through, wait briefly for init to complete and try again. + const start = Date.now(); + const waitForOpen = () => { + if (reportMaskHost.open && typeof reportMaskHost.open === "function") { + try { + reportMaskHost.open(); + } catch (err) { + console.error("Error calling reportMaskHost.open():", err); + } + } else if (Date.now() - start < 2000) { + setTimeout(waitForOpen, 50); + } + }; + waitForOpen(); +} + +function initReportMask(providedHost) { + const host = providedHost || document.querySelector("report-mask"); + if (!host) return; + if (host._reportMaskInitialized) return; // idempotent + + // A small helper that tries to find the required internals and complete the init. + const attemptInit = () => { + const overlay = host.querySelector("#maskOverlay"); + const dialog = host.querySelector("#maskDialog"); + const closeBtn = host.querySelector("#closeMaskBtn"); + const cancelBtn = host.querySelector("#cancelBtn"); + const form = host.querySelector("#maskForm"); + const nameInput = host.querySelector("#nameInput"); + const descriptionInput = host.querySelector("#descriptionInput"); + + if (!overlay || !dialog || !closeBtn || !cancelBtn || !form || !nameInput || !descriptionInput) { + return false; // not ready yet + } + + let lastFocused = null; + let structure = null; + + function onKeyDown(e) { + if (e.key === "Escape") { + e.preventDefault(); + host._realClose && host._realClose(); + return; + } + + if (e.key === "Tab") { + const focusable = dialog.querySelectorAll( + "a[href], button:not([disabled]), textarea, input, select" + ); + if (focusable.length === 0) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + } + + function openDialog() { + lastFocused = document.activeElement; + overlay.classList.add("open"); + overlay.setAttribute("aria-hidden", "false"); + nameInput.focus(); + document.body.style.overflow = "hidden"; + document.addEventListener("keydown", onKeyDown); + } + + function closeDialog() { + overlay.classList.remove("open"); + overlay.setAttribute("aria-hidden", "true"); + document.body.style.overflow = ""; + document.removeEventListener("keydown", onKeyDown); + if (lastFocused instanceof HTMLElement) lastFocused.focus(); + } + + overlay.addEventListener("click", (e) => { + if (e.target === overlay) closeDialog(); + }); + closeBtn.addEventListener("click", closeDialog); + cancelBtn.addEventListener("click", closeDialog); + + form.addEventListener("submit", function (event) { + event.preventDefault(); + const name = nameInput.value.trim() || null; + const description = descriptionInput.value.trim() || null; + processReportMaskSubmission({ name, description }); + form.reset(); + closeDialog(); + }); + + // Compatibility: listen for the openReportMask event as before. + document.addEventListener("openReportMask", function (e) { + if (e && e.detail && e.detail.structure) structure = e.detail.structure; + openDialog(); + }); + + // Expose real methods. + host._realOpen = openDialog; + host._realClose = closeDialog; + host.open = function () { + // If init is done, call real open; otherwise mark pending and return. + if (host._reportMaskInitialized && host._realOpen) { + host._realOpen(); + } else { + host._pendingOpen = true; + } + }; + host.close = function () { + if (host._realClose) host._realClose(); + }; + + host._reportMaskInitialized = true; + + // If someone requested open before init completed, honor it now. + if (host._pendingOpen) { + host._pendingOpen = false; + host._realOpen && host._realOpen(); + } + + return true; + }; + + // Try immediate initialization; if not ready, observe until structure is present (or timeout). + if (!attemptInit()) { + const observer = new MutationObserver((_, obs) => { + if (attemptInit()) { + obs.disconnect(); + } + }); + observer.observe(host, { childList: true, subtree: true }); + // Ensure we stop observing after a timeout. + setTimeout(() => observer.disconnect(), 2000); + } +} +document.addEventListener('DOMContentLoaded', initReportMask); + +// Process a report-mask form submission (moved from inline fragment) +async function processReportMaskSubmission(formData = {}) { + try { + const textOrNull = (id) => { + const el = document.getElementById(id); + return el && el.textContent && el.textContent.trim() ? el.textContent.trim() : null; + }; + + // Get mol file from InChI tab + const ketcher = getKetcher('inchi-tab1-ketcher'); + let molfile = null; + try { + if (ketcher) molfile = await ketcher.getMolfile(); + } catch (err) { + molfile = null; + } + + const inchi = textOrNull('inchi-tab1-inchi'); + const inchikey = textOrNull('inchi-tab1-inchikey'); + const auxinfo = textOrNull('inchi-tab1-auxinfo'); + // Remove InChI options from the log + const log = textOrNull('inchi-tab1-logs'); + const cleanedLog = log && log.startsWith('InChI options: ') + ? log.replace(/^InChI options: [^\n]*\n?/, '') + : log; + const inchi_version = getVersion('inchi-tab1-pane'); + + // Collect InChI options as a string + let options = ""; + try { + options = getInchiOptions('inchi-tab1-pane') + .map((o) => "-" + o) + .join(" "); + } catch (err) { + options = ""; + } + + const payload = { + input_source: "WebDemo", + inchi_version: inchi_version, + user: formData.name || null, + description: formData.description, + molfile: molfile, + inchi: inchi, + inchikey: inchikey, + auxinfo: auxinfo, + options: options, + log: cleanedLog, + }; + + console.log('reportMask:json', payload); + document.dispatchEvent(new CustomEvent('reportMask:json', { detail: payload })); + + fetch('/api/ingest_issue', { //TODO change endpoint + method: 'POST', + headers: {"Authorization": "JchSKSAoUUjKXriWdcUlb2a3hIvIgdPs", "Content-Type": "application/json"}, + body: JSON.stringify(payload), + }) + .then(response => response.json()) + .then(json => { + console.log('reportMask:json', json); + document.dispatchEvent(new CustomEvent('reportMask:json', { detail: json })); + }) + + } catch (err) { + console.error('Error assembling report mask JSON', err); + } +}