<!DOCTYPE html>
<html lang="en"><head>
<title>CodeMelted | UML Modeler</title>
<meta charset="UTF-8">
<meta name="description" content="UML provides a powerful ability to model software systems. I got tired of what was out there so I made my own.">
<meta name="keywords" content="CodeMeltedPWA, CodeMeltedDEV, SPA, UML, Software Engineering">
<meta name="author" content="Mark Shaffer">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="https://codemelted.com/favicon.png">
<link rel="stylesheet" href="https://codemelted.com/assets/css/scrollbars.css">
<style>
html, body {
margin: 0;
padding: 0;
}
header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 50px;
background-color: darkblue;
display: grid;
grid-template-columns: auto auto auto;
}
header div {
display: flex;
justify-content: center;
justify-items: center;
margin-top: 2.5px;
font-size: xx-large;
text-align: center;
}
header div a {
margin-right: 10px;
cursor: pointer;
text-decoration: none;
height: 50px;
width: 50px;
}
header div a:hover {
background-color: darkred;
}
main {
position: fixed;
display: grid;
grid-template-columns: auto auto;
padding: 0;
margin: 0;
top: 50px;
bottom: 42px;
width: 100%;
overflow: auto;
background-color: darkblue;
}
main textarea {
padding: 0;
margin: 0;
font-family: monospace;
color: white;
font-size: x-large;
margin-top: 2px;
border: none;
outline: none;
resize: none;
width: 99.5%;
background-color: black;
height: 99.5%;
}
main pre {
padding: 0;
margin: 0;
background-color: wheat;
height: 100%;
width: 100%;
display: flex;
align-content: center;
justify-content: start;
align-items: center;
}
footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: darkblue;
color: goldenrod;
display: grid;
grid-template-columns: auto auto;
text-align: center;
font-size: larger;
font-weight: bold;
}
footer div {
padding: 10px;
}
</style>
</head><body>
<header>
<div><a class="codemelted-nav-control" id="btnOpenModel" title="Open Model">📂</a></div>
<div><a class="codemelted-nav-control" id="btnSaveModel" title="Save Model">💾</a></div>
<div><a class="codemelted-nav-control" id="btnHelp" title="Mermaid Syntax">🆘</a></div>
</header>
<main>
<textarea id="txtEditor" wrap="off"></textarea>
<pre id="preModel" class="mermaid"></pre>
</main>
<footer>
<div>Editor</div>
<div>Model</div>
</footer>
<dialog id="dlgError">
<label id="lblError"></label>
<p>
<center><button id="btnClose">Close</button></center>
</p>
<script>
const dlgError = document.getElementById("dlgError");
const btnClose = document.getElementById("btnClose");
btnClose.onclick = (evt) => { dlgError.close(); };
</script>
</dialog>
<dialog id="dlgSaveFile">
<label>Save As Filename:</label><br />
<input id="txtFilename" type="text"></input>
<p>
<center>
<button id="btnOK">OK</button>
<button id="btnCancel">CANCEL</button>
</center>
</p>
<script>
const dlgSaveFile = document.getElementById("dlgSaveFile");
const btnOK = document.getElementById("btnOK");
const btnCancel = document.getElementById("btnCancel");
const txtFilename = document.getElementById("txtFilename");
btnOK.onclick = (e) => { dlgSaveFile.close(txtFilename.value); };
btnCancel.onclick = (e) => {dlgSaveFile.close(null); };
</script>
</dialog>
<script type="module">
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
mermaid.initialize({ startOnLoad: false });
let _modelCode = undefined;
let _svgModel = undefined;
const btnOpenModel = document.getElementById("btnOpenModel");
const btnSaveModel = document.getElementById("btnSaveModel")
const btnHelp = document.getElementById("btnHelp");
const txtEditor = document.getElementById("txtEditor");
const preModel = document.getElementById("preModel");
let _timerId = undefined;
function renderModel() {
clearTimeout(_timerId);
_timerId = setTimeout(async () => {
try {
_modelCode = txtEditor.value.trim();
const { svg } = await mermaid.render("mermaid", _modelCode);
_svgModel = svg;
preModel.innerHTML = _svgModel;
} catch (err) {
console.warn("renderModel", err);
}
}, 500);
}
txtEditor.onkeydown = (evt) => {
if (evt.key == "Tab") {
evt.preventDefault();
const textArea = evt.currentTarget;
textArea.setRangeText(
" ",
textArea.selectionStart,
textArea.selectionEnd,
"end"
);
}
};
txtEditor.onpaste = (evt) => { renderModel(); };
txtEditor.onkeypress = (evt) => { renderModel(); };
function showErrorDialog(msg) {
const dlgError = document.getElementById("dlgError");
const lblError = document.getElementById("lblError");
lblError.innerHTML = msg;
dlgError.showModal();
}
async function openFile(isTextFile, accept="") {
return new Promise((resolve, reject) => {
try {
if (typeof isTextFile !== "boolean" || typeof accept !== "string") {
throw new SyntaxError("parameters are not of an expected type");
}
const input = globalThis.document.createElement("input");
input.type = "file";
input.accept = accept;
input.onchange = async () => {
let file = undefined;
if (input.files != null) {
file = input.files[0];
if (isTextFile) {
const buffer = await file.arrayBuffer();
const decoder = new TextDecoder();
resolve(decoder.decode(buffer));
} else {
const bytes = await file.bytes();
resolve(bytes);
}
} else {
resolve(null);
}
};
input.click();
} catch (err) {
reject(err);
}
});
}
async function saveFile(filename, data) {
return new Promise((resolve, reject) => {
try {
if (typeof filename !== "string" ||
filename.length === 0 ) {
throw new SyntaxError("filename was not specified");
}
let blobURL = undefined;
if (typeof data === "string") {
const buffer = new TextEncoder().encode(data);
const blob = new Blob([buffer]);
blobURL = URL.createObjectURL(blob);
} else if (data instanceof Uint8Array) {
const blob = new Blob([data]);
blobURL = URL.createObjectURL(blob);
} else {
throw new SyntaxError("data is not of an expected type");
}
const a = globalThis.document.createElement('a');
a.href = blobURL;
a.download = filename;
a.style.display = 'none';
globalThis.document.body.append(a);
a.click();
setTimeout(() => {
URL.revokeObjectURL(blobURL);
a.remove();
resolve();
}, 500);
} catch (err) {
reject(err);
}
});
}
btnOpenModel.onclick = async (evt) => {
try {
const modelCode = await openFile(true, ".mmd");
if (modelCode) {
txtEditor.textContent = modelCode;
renderModel();
}
} catch (err) {
showErrorDialog(`Failed to open file. Reason:<br />${err}`);
}
};
btnSaveModel.onclick = (evt) => {
try {
const dlgSaveFile = document.getElementById("dlgSaveFile");
dlgSaveFile.onclose = async (e) => {
const filename = dlgSaveFile.returnValue;
if (filename) {
if (_modelCode && _modelCode !== "") {
await saveFile(`${filename}.mmd`, _modelCode);
await saveFile(`${filename}.svg`, _svgModel);
}
}
};
const txtFilename = document.getElementById("txtFilename");
txtFilename.value = "";
dlgSaveFile.showModal();
} catch (err) {
showErrorDialog(`Failed to save file. Reason:<br />${err}`);
}
};
btnHelp.onclick = (evt) => {
onOpenLinkClicked("mermaid.js.org/intro/syntax-reference.html");
};
function onOpenLinkClicked(url) {
const w = 900;
const h = 600;
const top = (window.screen.height - h) / 2;
const left = (window.screen.width - w) / 2;
const settings = `toolbar=no, location=no, ` +
`directories=no, status=no, menubar=no, ` +
`scrollbars=no, resizable=yes, copyhistory=no, ` +
`width=${w}, height=${h}, top=${top}, left=${left}`;
window.open(
`https://${url}`,
"_blank",
settings,
);
}
</script>
</body></html>