Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pages/components/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions pages/components/inchi-tools.html
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
title="AuxInfo"
></inchi-result-field>
<inchi-result-field id="inchi-tab1-logs" title="Log"></inchi-result-field>
<button class="btn btn-primary mt-2" onclick="openReportMask()" aria-haspopup="dialog">Report</button>
</div>

<!-- Convert Molfile to InChI -->
Expand Down
44 changes: 44 additions & 0 deletions pages/components/report-mask.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<link href="../css/report-mask.css" rel="stylesheet" />

<div
id="maskOverlay"
class="mask-overlay"
role="presentation"
aria-hidden="true"
>
<div
id="maskDialog"
class="mask-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="maskTitle"
>
<header>
<h2 id="maskTitle">Report structure</h2>
<button id="closeMaskBtn" class="mask-close" aria-label="Close dialog">
</button>
</header>

<form id="maskForm">
<label for="nameInput" class="visually-hidden">Name</label>
<input type="text" id="nameInput" placeholder="Name" />
<label for="descriptionInput" class="visually-hidden">Description</label>
<textarea
type="text"
rows="5"
cols="30"
id="descriptionInput"
placeholder="Please describe the problem that occurs with the structure."
required
></textarea>

<div class="mask-actions">
<button type="button" id="cancelBtn" class="btn-secondary">
Cancel
</button>
<button type="submit" class="btn-primary">Submit</button>
</div>
</form>
</div>
</div>
81 changes: 81 additions & 0 deletions pages/css/report-mask.css
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions pages/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -165,5 +165,6 @@ <h1 class="col-md-9 mt-auto">InChI Web Demo</h1>
></inchi-about>
</div>
</div>
<report-mask></report-mask>
</body>
</html>
224 changes: 224 additions & 0 deletions pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}