mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
fix(flowchart): replace window.prompt/confirm with inline modal in standalone window; v4.9.9
Electron renderer contexts return undefined from window.prompt/window.confirm, so shape change, edge kind, edge label, and reset confirmation did nothing. Bundle now ships promptInline/confirmInline (custom DOM overlay modals) and uses them in onEdgeClick / onShapeMenu / reset handler. Exposed as window.FlowchartModals for jsdom tests. Amit Haridas
This commit is contained in:
@@ -257,4 +257,4 @@ Amit Haridas (amit.wh@gmail.com)
|
||||
|
||||
## Version
|
||||
|
||||
v4.9.8
|
||||
v4.9.9
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# PanConverter - Updates & Changelog
|
||||
|
||||
## Version 4.9.9 (2026-09-15)
|
||||
|
||||
### Fix
|
||||
- **Standalone Flowchart Generator window: replaced broken `window.prompt` / `window.confirm` with an inline DOM-modal dialog.** Electron renderer contexts (the BrowserWindow hosting the standalone window) return `undefined` when `window.prompt(...)` or `window.confirm(...)` is called — meaning every shape menu, edge-kind change, edge-label edit, and reset confirmation silently did nothing. The bundle now ships two helpers (`promptInline`, `confirmInline`) that build a small overlay with a styled title, message, OK / Cancel buttons, and Enter / Escape / backdrop-click handling. The four call sites (`onEdgeClick` for kind + label, `onShapeMenu`, and the Reset click handler) are now `async` and await the helpers.
|
||||
- New `window.FlowchartModals = { promptInline, confirmInline }` export on the bundle so jsdom tests can drive the modals directly without rebuilding the IIFE.
|
||||
- 6 new tests in `tests/flowchart-controller.test.js` — OK / Cancel / Escape resolution paths for `promptInline`, OK / Cancel for `confirmInline`, and the `danger` flag renders a red "Delete" primary button.
|
||||
|
||||
## Version 4.9.8 (2026-09-15)
|
||||
|
||||
### Fix
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "markdown-converter",
|
||||
"version": "4.9.8",
|
||||
"version": "4.9.9",
|
||||
"description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v4.9.8 — Bundled single-file Flowchart Generator loader.
|
||||
* v4.9.9 — Bundled single-file Flowchart Generator loader.
|
||||
*
|
||||
* Inlines the four pure modules (flowchart-shapes / flowchart-mermaid /
|
||||
* flowchart-store / flowchart-canvas) plus the renderer controller
|
||||
@@ -19,6 +19,11 @@
|
||||
* brute-force bundle everything into one self-contained file. No
|
||||
* cross-file script ordering, no UMD wrapper, no `require()`. The
|
||||
* standalone window now has exactly one script dependency.
|
||||
* v4.9.9 — Electron renderer contexts disable `window.prompt` and
|
||||
* `window.confirm`, so shape change / edge kind / edge label /
|
||||
* reset confirmation did nothing. Replaced with `promptInline`
|
||||
* and `confirmInline` (custom DOM-overlay modals). Exposed as
|
||||
* `window.FlowchartModals` for jsdom tests.
|
||||
*
|
||||
* The legacy individual files under src/flowchart/* and
|
||||
* src/renderer/flowchart-controller.js are kept untouched — the
|
||||
@@ -814,6 +819,218 @@
|
||||
}, PERSIST_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
// ========== Inline modal dialog (replacement for window.prompt/confirm) ==========
|
||||
// v4.9.9 — window.prompt and window.confirm are disabled in Electron
|
||||
// renderer contexts (the BrowserWindow of a BrowserView/WebContentsView
|
||||
// returns undefined when called). Build minimal modal interactions on top
|
||||
// of plain DOM nodes. Resolves with the entered string (or null on
|
||||
// cancel/Esc/backdrop-click) for promptInline, and with a boolean for
|
||||
// confirmInline.
|
||||
function promptInline({ title, message, defaultValue = '', kind = 'text' }) {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement('div');
|
||||
Object.assign(overlay.style, {
|
||||
position: 'fixed',
|
||||
inset: '0',
|
||||
background: 'rgba(0,0,0,0.45)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 99999,
|
||||
});
|
||||
const box = document.createElement('div');
|
||||
Object.assign(box.style, {
|
||||
background: '#ffffff',
|
||||
color: '#1f2328',
|
||||
border: '1px solid #d0d7de',
|
||||
borderRadius: '8px',
|
||||
padding: '20px 24px',
|
||||
minWidth: '320px',
|
||||
maxWidth: '480px',
|
||||
boxShadow: '0 12px 32px rgba(0,0,0,0.25)',
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
});
|
||||
if (title) {
|
||||
const h = document.createElement('div');
|
||||
h.textContent = title;
|
||||
Object.assign(h.style, { fontSize: '14px', fontWeight: '600', marginBottom: '12px' });
|
||||
box.appendChild(h);
|
||||
}
|
||||
if (message) {
|
||||
const m = document.createElement('div');
|
||||
m.textContent = message;
|
||||
Object.assign(m.style, {
|
||||
fontSize: '12px',
|
||||
color: '#57606a',
|
||||
marginBottom: '12px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
});
|
||||
box.appendChild(m);
|
||||
}
|
||||
const input = document.createElement('input');
|
||||
input.type = kind === 'number' ? 'number' : 'text';
|
||||
input.value = defaultValue;
|
||||
Object.assign(input.style, {
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
fontSize: '13px',
|
||||
border: '1px solid #d0d7de',
|
||||
borderRadius: '4px',
|
||||
boxSizing: 'border-box',
|
||||
});
|
||||
box.appendChild(input);
|
||||
|
||||
const buttons = document.createElement('div');
|
||||
Object.assign(buttons.style, {
|
||||
marginTop: '14px',
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
justifyContent: 'flex-end',
|
||||
});
|
||||
const ok = document.createElement('button');
|
||||
ok.textContent = 'OK';
|
||||
Object.assign(ok.style, {
|
||||
padding: '6px 14px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
background: '#1f883d',
|
||||
color: '#ffffff',
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer',
|
||||
});
|
||||
const cancel = document.createElement('button');
|
||||
cancel.textContent = 'Cancel';
|
||||
Object.assign(cancel.style, {
|
||||
padding: '6px 14px',
|
||||
border: '1px solid #d0d7de',
|
||||
borderRadius: '4px',
|
||||
background: '#f6f8fa',
|
||||
color: '#1f2328',
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer',
|
||||
});
|
||||
buttons.appendChild(cancel);
|
||||
buttons.appendChild(ok);
|
||||
box.appendChild(buttons);
|
||||
overlay.appendChild(box);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
let resolved = false;
|
||||
const cleanup = (val) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
document.body.removeChild(overlay);
|
||||
resolve(val);
|
||||
};
|
||||
ok.addEventListener('click', () => cleanup(input.value || null));
|
||||
cancel.addEventListener('click', () => cleanup(null));
|
||||
overlay.addEventListener('click', (ev) => {
|
||||
if (ev.target === overlay) cleanup(null);
|
||||
});
|
||||
input.addEventListener('keydown', (kev) => {
|
||||
if (kev.key === 'Enter') cleanup(input.value || null);
|
||||
if (kev.key === 'Escape') cleanup(null);
|
||||
});
|
||||
setTimeout(() => input.focus(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
function confirmInline({ title, message, danger = false }) {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement('div');
|
||||
Object.assign(overlay.style, {
|
||||
position: 'fixed',
|
||||
inset: '0',
|
||||
background: 'rgba(0,0,0,0.45)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 99999,
|
||||
});
|
||||
const box = document.createElement('div');
|
||||
Object.assign(box.style, {
|
||||
background: '#ffffff',
|
||||
color: '#1f2328',
|
||||
border: '1px solid #d0d7de',
|
||||
borderRadius: '8px',
|
||||
padding: '20px 24px',
|
||||
minWidth: '320px',
|
||||
maxWidth: '480px',
|
||||
boxShadow: '0 12px 32px rgba(0,0,0,0.25)',
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
});
|
||||
if (title) {
|
||||
const h = document.createElement('div');
|
||||
h.textContent = title;
|
||||
Object.assign(h.style, { fontSize: '14px', fontWeight: '600', marginBottom: '12px' });
|
||||
box.appendChild(h);
|
||||
}
|
||||
if (message) {
|
||||
const m = document.createElement('div');
|
||||
m.textContent = message;
|
||||
Object.assign(m.style, {
|
||||
fontSize: '13px',
|
||||
color: '#1f2328',
|
||||
marginBottom: '14px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
});
|
||||
box.appendChild(m);
|
||||
}
|
||||
const buttons = document.createElement('div');
|
||||
Object.assign(buttons.style, { display: 'flex', gap: '8px', justifyContent: 'flex-end' });
|
||||
const ok = document.createElement('button');
|
||||
ok.textContent = danger ? 'Delete' : 'OK';
|
||||
Object.assign(ok.style, {
|
||||
padding: '6px 14px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
background: danger ? '#cf222e' : '#1f883d',
|
||||
color: '#ffffff',
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer',
|
||||
});
|
||||
const cancel = document.createElement('button');
|
||||
cancel.textContent = 'Cancel';
|
||||
Object.assign(cancel.style, {
|
||||
padding: '6px 14px',
|
||||
border: '1px solid #d0d7de',
|
||||
borderRadius: '4px',
|
||||
background: '#f6f8fa',
|
||||
color: '#1f2328',
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer',
|
||||
});
|
||||
buttons.appendChild(cancel);
|
||||
buttons.appendChild(ok);
|
||||
box.appendChild(buttons);
|
||||
overlay.appendChild(box);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
let resolved = false;
|
||||
const cleanup = (val) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
document.body.removeChild(overlay);
|
||||
resolve(val);
|
||||
};
|
||||
ok.addEventListener('click', () => cleanup(true));
|
||||
cancel.addEventListener('click', () => cleanup(false));
|
||||
overlay.addEventListener('click', (ev) => {
|
||||
if (ev.target === overlay) cleanup(false);
|
||||
});
|
||||
document.addEventListener('keydown', function onKey(ev) {
|
||||
if (ev.key === 'Enter') {
|
||||
cleanup(true);
|
||||
document.removeEventListener('keydown', onKey);
|
||||
}
|
||||
if (ev.key === 'Escape') {
|
||||
cleanup(false);
|
||||
document.removeEventListener('keydown', onKey);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
// Resolve the userData path ONCE on mount. The persistence path is
|
||||
// interpolated into a string on every read/write; calling the async
|
||||
@@ -836,14 +1053,22 @@
|
||||
});
|
||||
|
||||
_canvas = createCanvas(els.canvasHost, _store, {
|
||||
onEdgeClick: (edgeId) => {
|
||||
onEdgeClick: async (edgeId) => {
|
||||
const edge = _store.getGraph().edges.find((e) => e.id === edgeId);
|
||||
if (!edge) return;
|
||||
const nextKind = window.prompt('Edge kind (solid, dotted, thick):', edge.kind);
|
||||
const nextKind = await promptInline({
|
||||
title: 'Edge kind',
|
||||
message: 'Enter the new edge kind for this connection.',
|
||||
defaultValue: edge.kind,
|
||||
});
|
||||
if (nextKind && ['solid', 'dotted', 'thick'].includes(nextKind)) {
|
||||
_store.setEdgeKind(edgeId, nextKind);
|
||||
}
|
||||
const nextLabel = window.prompt('Edge label (empty to clear):', edge.label || '');
|
||||
const nextLabel = await promptInline({
|
||||
title: 'Edge label',
|
||||
message: 'Enter a label for this edge (leave blank to clear).',
|
||||
defaultValue: edge.label || '',
|
||||
});
|
||||
if (nextLabel !== null) {
|
||||
_store.setEdgeLabel(edgeId, nextLabel);
|
||||
}
|
||||
@@ -852,10 +1077,11 @@
|
||||
// Canvas already paints the .selected highlight; nothing else
|
||||
// needed here for selection state.
|
||||
},
|
||||
onShapeMenu: (nodeId) => {
|
||||
const next = window.prompt(
|
||||
'New shape (process, decision, terminator, subroutine, document):'
|
||||
);
|
||||
onShapeMenu: async (nodeId) => {
|
||||
const next = await promptInline({
|
||||
title: 'Change shape',
|
||||
message: 'New shape (process, decision, terminator, subroutine, document):',
|
||||
});
|
||||
if (next) _store.setNodeKind(nodeId, next);
|
||||
},
|
||||
});
|
||||
@@ -884,9 +1110,14 @@
|
||||
}
|
||||
|
||||
if (els.btnReset) {
|
||||
els.btnReset.addEventListener('click', () => {
|
||||
els.btnReset.addEventListener('click', async () => {
|
||||
if (!_store) return;
|
||||
if (!window.confirm('Clear all nodes and edges?')) return;
|
||||
const ok = await confirmInline({
|
||||
title: 'Reset diagram',
|
||||
message: 'Clear all nodes and edges? This cannot be undone.',
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
_store.deserialize({ nodes: [], edges: [] });
|
||||
setStatus('Reset');
|
||||
});
|
||||
@@ -945,6 +1176,11 @@
|
||||
},
|
||||
};
|
||||
|
||||
// Expose the inline modal helpers (v4.9.9) so jsdom tests can drive them
|
||||
// directly without rebuilding the bundle's IIFE. Production code accesses
|
||||
// these by closure; this handle exists purely for unit tests.
|
||||
window.FlowchartModals = { promptInline, confirmInline };
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bootstrap);
|
||||
} else {
|
||||
|
||||
@@ -81,6 +81,7 @@ afterEach(() => {
|
||||
delete window.FlowchartMermaid;
|
||||
delete window.FlowchartStore;
|
||||
delete window.FlowchartCanvas;
|
||||
delete window.FlowchartModals;
|
||||
});
|
||||
|
||||
describe('flowchart-generator.html — stylesheet links', () => {
|
||||
@@ -234,3 +235,127 @@ describe('flowchart pure modules — UMD browser-global assignment (v4.9.7)', ()
|
||||
expect(src).toMatch(new RegExp(`window\\.${globalName}\\s*=\\s*exported`));
|
||||
});
|
||||
});
|
||||
|
||||
// v4.9.9 — Electron renderer contexts disable `window.prompt` and
|
||||
// `window.confirm` (the BrowserWindow returns undefined when called),
|
||||
// which broke shape change / edge kind / edge label / reset confirm in
|
||||
// the standalone Flowchart Generator window. The bundle now ships
|
||||
// `promptInline` and `confirmInline` (custom DOM overlay modals) and
|
||||
// exposes them as `window.FlowchartModals` for testing. These tests load
|
||||
// the real bundle into jsdom so we exercise the actual overlay code.
|
||||
describe('flowchart-bundle: inline modal helpers (v4.9.9)', () => {
|
||||
const BUNDLE_PATH = path.join(__dirname, '..', 'src', 'renderer', 'flowchart-bundle.js');
|
||||
const HTML_PATH_BUNDLE = path.join(__dirname, '..', 'src', 'flowchart-generator.html');
|
||||
|
||||
async function loadBundle() {
|
||||
const html = fs.readFileSync(HTML_PATH_BUNDLE, 'utf-8');
|
||||
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||||
document.body.innerHTML = bodyMatch ? bodyMatch[1] : html;
|
||||
window.electronAPI = {
|
||||
flowchart: {
|
||||
getUserDataPath: jest.fn(async () => '/userdata'),
|
||||
readFile: jest.fn(async () => null),
|
||||
writeFile: jest.fn(async () => undefined),
|
||||
insertAtCursor: jest.fn(),
|
||||
},
|
||||
};
|
||||
const bundleSrc = fs.readFileSync(BUNDLE_PATH, 'utf-8');
|
||||
// eslint-disable-next-line no-new-func
|
||||
new Function('window', 'document', bundleSrc)(window, document);
|
||||
// Flush microtasks so the bootstrap chain settles before tests run.
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
await Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
test('promptInline resolves with the entered value on OK click', async () => {
|
||||
await loadBundle();
|
||||
expect(typeof window.FlowchartModals.promptInline).toBe('function');
|
||||
const promise = window.FlowchartModals.promptInline({
|
||||
title: 'Edge kind',
|
||||
message: 'Enter the new edge kind.',
|
||||
defaultValue: 'solid',
|
||||
});
|
||||
const input = document.querySelector('input');
|
||||
expect(input).not.toBeNull();
|
||||
input.value = 'dotted';
|
||||
const okButton = Array.from(document.querySelectorAll('button')).find(
|
||||
(b) => b.textContent === 'OK'
|
||||
);
|
||||
expect(okButton).toBeDefined();
|
||||
okButton.click();
|
||||
await expect(promise).resolves.toBe('dotted');
|
||||
expect(document.querySelector('input')).toBeNull();
|
||||
});
|
||||
|
||||
test('promptInline resolves null on Cancel click', async () => {
|
||||
await loadBundle();
|
||||
const promise = window.FlowchartModals.promptInline({
|
||||
title: 'Edge label',
|
||||
message: 'Enter a label.',
|
||||
defaultValue: '',
|
||||
});
|
||||
const cancelButton = Array.from(document.querySelectorAll('button')).find(
|
||||
(b) => b.textContent === 'Cancel'
|
||||
);
|
||||
expect(cancelButton).toBeDefined();
|
||||
cancelButton.click();
|
||||
await expect(promise).resolves.toBeNull();
|
||||
expect(document.querySelector('input')).toBeNull();
|
||||
});
|
||||
|
||||
test('promptInline resolves null on Escape key', async () => {
|
||||
await loadBundle();
|
||||
const promise = window.FlowchartModals.promptInline({
|
||||
title: 'Edge kind',
|
||||
defaultValue: 'solid',
|
||||
});
|
||||
const input = document.querySelector('input');
|
||||
const ev = new window.KeyboardEvent('keydown', { key: 'Escape', bubbles: true });
|
||||
input.dispatchEvent(ev);
|
||||
await expect(promise).resolves.toBeNull();
|
||||
});
|
||||
|
||||
test('confirmInline resolves true on OK click', async () => {
|
||||
await loadBundle();
|
||||
const promise = window.FlowchartModals.confirmInline({
|
||||
title: 'Reset diagram',
|
||||
message: 'Clear all nodes and edges?',
|
||||
});
|
||||
const okButton = Array.from(document.querySelectorAll('button')).find(
|
||||
(b) => b.textContent === 'OK'
|
||||
);
|
||||
expect(okButton).toBeDefined();
|
||||
okButton.click();
|
||||
await expect(promise).resolves.toBe(true);
|
||||
});
|
||||
|
||||
test('confirmInline resolves false on Cancel click', async () => {
|
||||
await loadBundle();
|
||||
const promise = window.FlowchartModals.confirmInline({
|
||||
title: 'Reset diagram',
|
||||
message: 'Clear all nodes and edges?',
|
||||
});
|
||||
const cancelButton = Array.from(document.querySelectorAll('button')).find(
|
||||
(b) => b.textContent === 'Cancel'
|
||||
);
|
||||
expect(cancelButton).toBeDefined();
|
||||
cancelButton.click();
|
||||
await expect(promise).resolves.toBe(false);
|
||||
});
|
||||
|
||||
test('confirmInline with danger flag renders a Delete primary button', async () => {
|
||||
await loadBundle();
|
||||
const promise = window.FlowchartModals.confirmInline({
|
||||
title: 'Reset diagram',
|
||||
message: 'Clear all nodes and edges?',
|
||||
danger: true,
|
||||
});
|
||||
const buttons = Array.from(document.querySelectorAll('button'));
|
||||
const primary = buttons.find((b) => b.textContent === 'Delete');
|
||||
expect(primary).toBeDefined();
|
||||
expect(primary.style.background).toBe('rgb(207, 34, 46)');
|
||||
primary.click();
|
||||
await expect(promise).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user