Skip to content

Instantly share code, notes, and snippets.

@secondarykey
Last active March 4, 2025 22:39
Show Gist options
  • Select an option

  • Save secondarykey/f7daf8da25c4b2384420bd190ac18245 to your computer and use it in GitHub Desktop.

Select an option

Save secondarykey/f7daf8da25c4b2384420bd190ac18245 to your computer and use it in GitHub Desktop.
Itoma is markdown viewer

Itoma

Itoma is markdown viewer.

Edit Text

Write markdown. You can also drag and drop text files.(Limited to 10kB)

Download File

There is no way to save the file itself, so please download and overwrite the edited text.

Download View File

Press the button at the bottom right of the viewer to switch to HTML mode. Please download the HTML and use it.

Other

History Save

There is an auto-save feature so that you can use it even if you suddenly close your browser (once every 5 minutes). If the contents are the same, they will not be saved. The work state is saved as history (up to 16 times).

To open it, select History from the menu.

History is stored in localStorage.

Editor font

You can change the font using the icon in the bottom right of the editor.

The font name cannot be obtained, so please look it up and enter it yourself. e.g.) "Source Code Pro" "Fira Code" "Proggy Fonts" etc... If it's installed on your device, it should be applied.

Once you specify a color, it cannot be changed in Light or Dark mode.

To revert, press "Erase" to apply

Style Setting

By default, the following is set.

.htmlViewer > h1 {
  border-bottom: 2px solid var(--viewer-header-color);
}

.htmlViewer > h2 {
  border-bottom: 1px solid var(--viewer-header-color);
}

".htmlViewer" is where the HTML will be output. If you want to switch between light and dark modes.Please use "body.light-mode" and "body.dark-mode". Please refer to this file with development tools to check the currently set custom properties.

For example, the following setting will change the color of the "ZenUML" comments.

.zenuml .comments p {
  color: blue;
}

Development

The basic premise of Itoma is that it works with a single HTML file. I would like to split it into JS files and CSS files, but I am holding back and implementing it.

<html>
<head>
<title>Itoma v0.3.3</title>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!--
<script src="https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/highlight.min.js"></script>
-->
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
import zenuml from 'https://cdn.jsdelivr.net/npm/@mermaid-js/[email protected]/dist/mermaid-zenuml.esm.min.mjs';
mermaid.initialize({ startOnLoad: false });
await mermaid.registerExternalDiagrams([zenuml]);
globalThis.mermaid = mermaid;
</script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
<!--
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/styles/github.min.css">
-->
<style>
body.light-mode {
--bg-color: #fdfdfd;
--color: #111111;
--viewer-header-color: rgba(20,20,20,0.2);
--btn-color: #333333;
--btn-hover-color: #e1e1e1;
--btn-disable-color: #aaaaaa;
--btn_embed-border-color: #bbb;
--btn_float-color: rgba(0, 0, 0, 0.3);
--btn_check-bg-color: #0d6efd;
--input-bg-color: #f5f5f5;
--input-border-color: #cccccc;
--drawer-bg-color: rgba(0, 0, 0, 0.3);
--drawer_content-bg-color: #fafafa;
--drawer_remove-color: rgba(255, 0, 0, 0.7);
--menu-hover-color: #eeeeee;
--menu-border-color: #eeeeee;
--nav-bg-color: #eeeeee;
--nav-border-color: #cccccc;
--splitter-color: #eeeeee;
--splitter-hover-color: #0d6efd;
--editor-bg-color: #fdfdfd;
--editor-color: #111111;
--viewer-bg-color: #fdfdfd;
--html-bg-color: #ffffff;
--modal-bg-color: #f5f5f5;
--modal-border-color: #cccccc;
--btn_appy-color: #0d6efd;
--mode-change: background-color 0.5s, color 0.5s;
--scroll-color: #aaaaaa;
--scroll-hover-color: #999999;
--scroll-bg-color: #cccccc;
}
body.dark-mode {
--bg-color: #111111;
--color: #e1e1e1;
--viewer-header-color: rgba(240,240,240,0.2);
--btn-color: #cccccc;
--btn-hover-color: #444444;
--btn-disable-color: #aaaaaa;
--btn_embed-border-color: #666666;
--btn_float-color: rgba(255, 255, 255, 0.3);
--btn_check-bg-color: #000055;
--input-bg-color: #444444;
--input-border-color: #555555;
--drawer-bg-color: rgba(0, 0, 0, 0.5);
--drawer_content-bg-color: #222222;
--drawer_remove-color: rgba(255, 0, 0, 0.5);
--menu-hover-color: #292929;
--menu-border-color: #333333;
--nav-bg-color: #333333;
--nav-border-color: #444444;
--splitter-color: #333333;
--splitter-hover-color: #000055;
--editor-bg-color: #222222;
--editor-color: #f1f1f1;
--viewer-bg-color: #222222;
--html-bg-color: #222222;
--modal-bg-color: #222222;
--modal-border-color: #333333;
--btn_appy-color: #5daefd;
--mode-change: background-color 0.2s, color 0.2s;
--scroll-color: #393939;
--scroll-hover-color: #444444;
--scroll-bg-color: #292929;
}
body {
background-color: var(--bg-color);
color: var(--color);
overflow: hidden;
transition: var(--mode-change);
}
.itomaInput {
background-color: var(--input-bg-color) !important;
color: var(--color) !important;
border: 1px solid var(--input-border-color) !important;
transition: var(--mode-change);
}
#fileGroup>input {
background-color: var(--input-bg-color) !important;
color: var(--color) !important;
border-top: 1px solid var(--input-border-color);
border-bottom: 1px solid var(--input-border-color);
border-right: 1px solid var(--input-border-color);
border-left: 1px solid var(--input-border-color);
transition: var(--mode-change);
}
#fileGroup>button {
border-top: 1px solid var(--input-border-color);
border-bottom: 1px solid var(--input-border-color);
border-right: 1px solid var(--input-border-color);
transition: var(--mode-change);
}
.btn {
color: var(--btn-color);
transition: var(--mode-change);
}
.btn:hover {
background-color: var(--btn-hover-color);
transition: var(--mode-change);
}
.btn:disabled {
border: 0;
color: var(--btn-disable-color);
transition: var(--mode-change);
}
::-webkit-scrollbar {
cursor: auto;
width: 14px;
}
::-webkit-scrollbar-thumb {
cursor: auto;
background: var(--scroll-color);
}
::-webkit-scrollbar-thumb:hover {
background: var(--scroll-hover-color);
}
::-webkit-scrollbar-track {
background: var(--scroll-bg-color);
}
#nav {
background-color: var(--nav-bg-color) !important;
border-bottom: 1px solid var(--nav-border-color);
transition: var(--mode-change);
}
#logo>a {
margin-right: 0px;
}
#logo>span {
font-size: 20px;
margin-right: 20px;
}
#name {
width: 300px;
}
#autoBtn {
margin-top: 8px;
background-color: var(--input-bg-color);
border-color: var(--input-border-color);
transition: var(--mode-change);
}
#autoBtn:checked {
margin-top: 8px;
background-color: var(--btn_check-bg-color);
transition: var(--mode-change);
}
#drawer {
background-color: var(--drawer-bg-color);
width: 100vw;
height: 100vh;
min-width: 100vw;
min-height: 100vh;
margin: 0;
padding: 0;
transition: 0.2s;
position: absolute;
opacity: 0;
visibility: hidden;
z-index: 1000;
}
#drawer.show {
transition: 0.5s;
opacity: 1;
visibility: visible;
}
#drawer>.row {
height: 100vh;
min-height: 100vh;
margin: 0;
padding: 0;
}
#drawerContent {
margin: 0;
padding: 0;
}
#drawerContent>ul {
height: 100%;
background-color: var(--drawer_content-bg-color);
border-right: 1px solid var(--menu-border-color);
}
#drawerContent>ul>li {
background-color: var(--drawer_content-bg-color);
color: var(--color);
border-radius: 0;
border-top: 0;
border-bottom: 1px solid var(--menu-border-color);
border-right: 0;
border-left: 0;
}
#drawerContent>ul>li:last-child {
border-top: 1px solid var(--menu-border-color);
border-bottom: 0;
}
.menuBtn {
display: flex;
flex-flow: row;
cursor: pointer;
}
.menuBtn:hover {
background-color: var(--menu-hover-color) !important;
}
.menuBtn>button {
margin-left: auto;
}
.menuBtn>button>i {
color: var(--drawer_remove-color);
}
#infoBtn {
margin-top: auto;
}
#content {
display: flex;
flex-flow: row;
width: 100vw;
height: calc(100vh - 50px);
}
.editorStyle {
resize: none;
border: none;
outline: none;
padding: 10px;
background-color: var(--editor-bg-color);
color: var(--editor-color);
}
#editor {
height: calc(100vh - 50px);
width: calc(100% / 2 - 5px);
transition: var(--mode-change);
}
#splitter {
cursor: ew-resize;
width: 10px;
height: calc(100vh - 50px);
border: none;
background-color: var(--splitter-color);
}
#splitter:hover {
transition: 0.5s;
background-color: var(--splitter-hover-color);
}
#viewer {
outline: none;
width: calc(100% / 2 - 5px);
height: 100%;
transition: var(--mode-change);
}
#messageContainer {
z-index: 4000;
}
#message>div {
display: flex;
margin: 0px;
}
#message>div>i {
font-size: 36px;
}
#message>div>span {
margin-left: 20px;
}
#message>div>button {
margin-top: 8px;
margin-left: auto;
}
.modal>div>div.modal-content {
background-color: var(--modal-bg-color);
}
.modal>div>div.modal-content>div.modal-header {
border-bottom: 1px solid var(--modal-border-color);
}
.modal>div>div.modal-content>div.modal-body {
min-height: 80px;
}
.modal>div>div.modal-content>div.modal-footer {
border-top: 1px solid var(--modal-border-color);
}
.modal>div>div.modal-content>div.modal-footer>button {
font-size: 24px;
margin-left: 24px;
}
.modal>div>div.modal-content>div.modal-footer>button.apply {
font-weight: bold;
color: var(--btn_appy-color);
}
#editorFontText {
width: 100%;
}
#viewerStyleText {
width: 100%;
min-height: 400px;
}
.htmlViewer {
margin: 0;
padding: 10px;
background-color: var(--html-bg-color);
overflow: auto;
}
#html {
width: 100vw;
height: 100vh;
min-width: 100vw;
min-height: 100vh;
max-width: 100vw;
max-height: 100vh;
transition: 0.2s;
position: absolute;
opacity: 0;
visibility: hidden;
z-index: 2000;
}
#editorFont {
position: absolute;
left: calc(100vw / 2 - 10px - 36px - 48px);
bottom: 15px;
font-size: 36px;
color: var(--btn_float-color);
transition: var(--mode-change);
}
#hidden {
position: absolute;
right: 28px;
bottom: 15px;
font-size: 36px;
color: var(--btn_float-color);
transition: var(--mode-change);
}
#html.show {
transition: 0.5s;
opacity: 1;
visibility: visible;
}
#htmlController {
position: absolute;
right: 20px;
bottom: 10px;
border-radius: 10%;
z-index: 3000;
transition: 0.2s;
opacity: 0;
visibility: hidden;
padding: 5px;
}
#htmlController>button {
color: var(--btn_float-color);
font-size: 36px;
margin-left: 30px;
}
#htmlController.show {
transition: 0.5s;
opacity: 1;
visibility: visible;
}
/** Added because "ZenUML" breaks the display **/
#zenuml-intersection-detector-container {
overflow: hidden;
}
</style>
</head>
<body class="dark-mode">
<div id="html" class="htmlViewer"></div>
<!-- ファイルメニュー表示 -->
<div id="drawer" class="container">
<div class="row">
<div class="col-4" id="drawerContent">
<ul class="list-group" id="history">
<li id="infoBtn" class="menuBtn list-group-item">
<i class="bi bi-info-square"></i>
</li>
</ul>
</div>
<div class="col-8" id="drawerBG"> </div>
</div>
</div>
<button id="hidden" type="button" class="btn">
<i class="bi bi-filetype-html"></i>
</button>
<div id="htmlController">
<button type="button" class="btn btnLightDark">
<i class="bi bi-moon iconLightDark"></i>
</button>
<button id="htmlDownloadBtn" type="button" class="btn">
<i class="bi bi-download"></i>
</button>
<button id="htmlCloseBtn" type="button" class="btn">
<i class="bi bi-box-arrow-right"></i>
</button>
</div>
<!-- ナビゲーション表示 -->
<nav id="nav" class="navbar bg-body-tertiary">
<div class="container-fluid">
<div class="row g-3">
<div id="logo" class="col-auto">
<a class="navbar-brand" href="#">
<button type="button" class="btn" id="drawerBtn">
<i class="bi bi-list"></i>
</button>
</a>
<span>Itoma</span>
</div>
<div class="col-auto">
<div id="fileGroup" class="input-group">
<input type="text" id="name" class="form-control form-control-sm" placeholder="History name"
value="Itoma.md">
<button type="button" class="btn" id="saveBtn">
<i class="bi bi-floppy"></i>
</button>
</div>
</div>
<div class="col-auto">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="autoBtn" data-bs-toggle="tooltip"
data-bs-placement="bottom" data-bs-title="Auto Save" checked>
</div>
</div>
<div class="col-auto">
<button type="button" class="btn" id="downloadBtn">
<i class="bi bi-download"></i>
</button>
</div>
</div>
<div class="row g-3">
<div class="col-auto">
<button type="button" class="btn" id="styleBtn">
<i class="bi bi-filetype-css"></i>
</button>
</div>
<div class="col-auto">
<button type="button" class="btn btnLightDark">
<i class="bi bi-moon iconLightDark"></i>
</button>
</div>
</div>
</div>
</nav>
<!-- コンテンツ部分 -->
<div id="content">
<!-- テキスト部分 -->
<textarea id="editor" class="editorStyle"></textarea>
<button id="editorFont" type="button" class="btn">
<i class="bi bi-fonts"></i>
</button>
<div id="splitter" draggable="true">
</div>
<div id="viewer" class="htmlViewer"></div>
</div>
<!-- confirm dialog -->
<div id="confirm" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">Confirmation</h5>
</div>
<div class="modal-body">
<p id="confirmMsg">The current text is deleted and the history is loaded.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn" data-bs-dismiss="modal">
<i class="bi bi-x-circle"></i>
</button>
<button type="button" class="btn apply" data-bs-dismiss="modal">
<i class="bi bi-check-circle"></i>
</button>
</div>
</div>
</div>
</div>
<!-- font setting -->
<div id="editorFontStyle" class="modal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Editor Font Setting</h5>
</div>
<div class="modal-body">
<form class="row g-3">
<div class="col-auto">
<input type="text" class="form-control itomaInput" placeholder="FontName" id="fontName">
</div>
<div class="col-auto">
<input type="text" class="form-control itomaInput" placeholder="Size" style="width:100px" id="fontSize">
</div>
<div class="col-auto">
<input type="color" class="form-control form-control-color itomaInput" title="Choose font color"
id="fontColor">
</div>
<div class="col-auto">
<input type="color" class="form-control form-control-color itomaInput" title="Choose background color"
id="fontBGColor">
</div>
</form>
<textarea class="editorStyle" rows="4" id="editorFontText"># Itoma
Itoma is markdown viewer.
This Area is Editor sample.</textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn" data-bs-dismiss="modal">
<i class="bi bi-x-circle"></i>
</button>
<button type="button" class="btn" id="editorFontErase">
<i class="bi bi-eraser"></i>
</button>
<button type="button" class="btn apply" id="editorFontApply">
<i class="bi bi-check-circle"></i>
</button>
</div>
</div>
</div>
</div>
<!-- style setting -->
<div id="viewerStyle" class="modal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">Viewer Style Setting</h5>
</div>
<div class="modal-body">
<textarea class="editorStyle" id="viewerStyleText"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn" data-bs-dismiss="modal">
<i class="bi bi-x-circle"></i>
</button>
<button type="button" class="btn apply" id="styleApply">
<i class="bi bi-check-circle"></i>
</button>
</div>
</div>
</div>
</div>
<!-- message toast -->
<div id="messageContainer" class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="message" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="alert" role="alert">
<i class="bi"></i>
<span></span>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<script>
//file limit 10kB
const UploadMax = 1024 * 10;
// viewer update sec
const updateSec = 0.3;
var changeTextTimeout = -1;
//localStorage Header(key)
const StorageHeader = "Itoma.";
const historyLimit = 16;
const saveMinite = 5;
var saveTextInterval = -1;
// currently displayed text(deciding whether to render)
var nowText = "";
var nowId = "";
var nowWidth = 0;
const innerStyle = document.createElement('style');
innerStyle.type = 'text/css';
innerStyle.textContent = `
.htmlViewer > h1 {
border-bottom: 2px solid var(--viewer-header-color);
}
.htmlViewer > h2 {
border-bottom: 1px solid var(--viewer-header-color);
}
`;
document.head.appendChild(innerStyle);
document.addEventListener("DOMContentLoaded", function (e) {
//Added mermaid rendering
const renderer = new marked.Renderer();
renderer.code = (code) => {
if (code.lang == 'mermaid') {
var text = code.text;
return `<pre class="mermaid">${text}</pre>`;
}
return `<pre>${code.text}</pre>`
//return '<pre><code>\n' + hljs.highlightAuto(code).value + '\n</code></pre>';
}
marked.use({ renderer });
/**
* Editor
*/
const editor = document.querySelector("#editor");
const editorFontBtn = document.querySelector("#editorFont");
const viewer = document.querySelector("#viewer");
// editor text rendering
const viewEditorText = () => {
viewText(editor.value);
}
// text rendering
const viewText = (val) => {
if (nowText != val) {
nowText = val;
renderHTML(viewer,val);
}
changeTextTimeout = -1;
}
const renderHTML = (elm,val) => {
var rtn = marked.marked(val);
elm.innerHTML = rtn;
mermaid.run();
}
// Search for indented or interpolated strings in text
const getIndentChar = (before) => {
var rtn = {}
rtn.indent = "";
rtn.char = "";
const last = before.lastIndexOf('\n')
if (last === -1) {
return rtn;
}
const line = before.substring(last + 1);
for (let idx = 0; idx < line.length; ++idx) {
var c = line[idx]
console.log(c)
if (c !== " ") {
if (c === "-") {
rtn.char = "- ";
} else if (c === ">") {
rtn.char = "> ";
} else if (c === "1") {
var c2 = line[idx + 1];
if (c2 === ".") {
rtn.char = "1. ";
}
}
break;
}
rtn.indent += " ";
}
return rtn
}
// Change the text in the editor(Enter)
const changeEditorValue = () => {
const val = editor.value;
const start = editor.selectionStart;
const end = editor.selectionEnd;
const before = val.substring(0, start)
const after = val.substring(end)
var obj = getIndentChar(before);
var add = "\n" + obj.indent + obj.char;
editor.value = before + add + after;
editor.selectionStart = start + add.length;
editor.selectionEnd = start + add.length;
}
//Editor Input Event
editor.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
changeEditorValue();
}
// Wait for a certain period of time to display
if (changeTextTimeout === -1) {
viewEditorText();
changeTextTimeout = setTimeout(function () {
changeTextTimeout = -1;
}, updateSec * 1000);
} else {
clearTimeout(changeTextTimeout);
changeTextTimeout = setTimeout(viewEditorText, updateSec * 1000);
}
});
editor.addEventListener("dragover", e => {
e.preventDefault()
//TODO Cursor?
});
// dropping a file into the editor
editor.addEventListener("drop", function (e) {
const file = e.dataTransfer.files[0];
if (file === undefined) {
return;
}
e.preventDefault();
e.stopPropagation();
if (file.size > UploadMax) {
showMessage("warning", `File size(${file.size}B) exceeds Limit(${UploadMax}B)`)
return;
}
nameTxt.value = file.name;
const reader = new FileReader();
reader.onload = function (e) {
editor.value = e.target.result;
viewText(editor.value);
};
reader.readAsText(file);
});
const createEditorStyle = (name, size, color, bgcolor) => {
var editorStyle = {};
editorStyle.color = color;
editorStyle.backgroundColor = bgcolor;
editorStyle.fontFamily = name;
editorStyle.fontSize = size;
return editorStyle;
};
const saveEditorStyle = (name, size, color, bgcolor) => {
var editorStyle = createEditorStyle(name,size,color,bgcolor);
localStorage.setItem(EditorStyleKey, JSON.stringify(editorStyle));
writeStyle(editor, editorStyle)
}
const fontModal = document.querySelector("#editorFontStyle");
const fontApply = fontModal.querySelector("#editorFontApply");
const fontErase = fontModal.querySelector("#editorFontErase");
const fontDialog = new bootstrap.Modal(fontModal);
const fontName = fontModal.querySelector("#fontName");
const fontSize = fontModal.querySelector("#fontSize");
const fontColor = fontModal.querySelector("#fontColor");
const fontBGColor = fontModal.querySelector("#fontBGColor");
const fontEditor = fontModal.querySelector(".editorStyle");
function setDefaultColor(elm,val) {
if ( val === null || val === "" ) {
elm.value = "#000000";
elm.checked = true;
} else {
elm.value = val;
elm.checked = false;
}
}
function getDefaultColor(elm) {
if ( !elm.checked ) {
return elm.value;
}
return ""
}
function changeSampleEditorColor(e) {
e.target.checked = false;
var editorStyle = createEditorStyle(fontName.value, fontSize.value,
getDefaultColor(fontColor), getDefaultColor(fontBGColor));
writeStyle(fontEditor, editorStyle);
}
fontName.addEventListener("change",changeSampleEditorColor);
fontSize.addEventListener("change",changeSampleEditorColor);
fontColor.addEventListener("change",changeSampleEditorColor);
fontBGColor.addEventListener("change",changeSampleEditorColor);
editorFontBtn.addEventListener("click", function () {
var style = localStorage.getItem(EditorStyleKey);
if (style !== null) {
var s = JSON.parse(style);
fontName.value = s.fontFamily;
fontSize.value = s.fontSize;
setDefaultColor(fontColor,s.color);
setDefaultColor(fontBGColor,s.backgroundColor);
writeStyle(fontEditor, s);
} else {
fontName.value = "";
fontSize.value = "";
setDefaultColor(fontColor,null);
setDefaultColor(fontBGColor,null);
}
// open dialog
fontDialog.show();
});
fontErase.addEventListener("click", function () {
fontName.value = "";
fontSize.value = "";
setDefaultColor(fontColor,null);
setDefaultColor(fontBGColor,null);
writeStyle(fontEditor, createEditorStyle(null,null,null,null));
});
fontApply.addEventListener("click", function () {
saveEditorStyle(fontName.value, fontSize.value,
getDefaultColor(fontColor), getDefaultColor(fontBGColor));
fontDialog.hide();
});
/*
* screen operation
*/
const drawerBtn = document.querySelector("#drawerBtn");
const drawerBG = document.querySelector("#drawerBG");
const drawer = document.querySelector("#drawer");
const splitter = document.querySelector("#splitter");
// switch drawer
const toggleDrawer = () => {
drawer.classList.toggle("show");
}
drawerBtn.addEventListener("click", toggleDrawer);
drawerBG.addEventListener("click", toggleDrawer);
// drag splitter
const dragSplitter = (e) => {
var w = e.clientX;
if (w >= 100) {
nowWidth = w;
redrawEditor(w);
}
}
splitter.addEventListener("dragover", e => e.preventDefault());
splitter.addEventListener("drag", dragSplitter);
/**
* Drawer Operation
*/
const HistoryKey = StorageHeader + "history";
const getHistoryText = (id) => {
var data = localStorage.getItem(StorageHeader + id);
if (data === null) {
data = "";
}
return data;
}
const redrawEditor = (w) => {
if (w >= 100) {
editor.style.width = w + "px";
viewer.style.width = (window.innerWidth - (w + 10)) + "px";
editorFontBtn.style.left = (w - 48 - 36) + "px";
}
}
const openHistory = (obj) => {
nameTxt.value = obj.name;
var txt = getHistoryText(obj.id);
editor.value = txt;
}
const removeHistory = (id) => {
var history = getHistoryList();
var newHis = [];
history.forEach((obj) => {
if (id !== obj.id) {
newHis.push(obj);
}
})
setHistoryList(newHis);
localStorage.removeItem(StorageHeader + id);
}
const setHistoryList = (history) => {
localStorage.setItem(HistoryKey, JSON.stringify(history));
}
const getHistoryList = () => {
var str = localStorage.getItem(HistoryKey);
var history = [];
if (str !== null && str !== "") {
history = JSON.parse(str);
}
return history;
}
const drawHistoryMenu = () => {
var historyUL = document.querySelector("#history");
var history = getHistoryList();
var lis = historyUL.querySelectorAll("li.history")
lis.forEach((li) => {
li.parentElement.removeChild(li);
});
history.forEach((obj) => {
var li = document.createElement("li");
li.classList.add("list-group-item");
li.classList.add("menuBtn");
li.classList.add("history");
li.setAttribute("data-id", obj.id);
var d = new Date(Date.parse(obj.date));
var div = document.createElement("div");
div.textContent = d.toLocaleString() + ":" + obj.name;
li.appendChild(div)
var btn = document.createElement("button");
btn.setAttribute("type", "button")
btn.classList.add("btn")
var icon = document.createElement("i");
icon.classList.add("bi");
icon.classList.add("bi-trash");
btn.appendChild(icon);
li.appendChild(btn)
li.addEventListener("click", function (e) {
showConfirm("The current text is deleted and the history is loaded.").then(function () {
openHistory(obj);
viewEditorText();
toggleDrawer();
}).catch((err) => {
if (err) {
console.error(err);
showErrorMessage(err);
}
});
});
btn.addEventListener("click", function (e) {
e.stopPropagation();
showConfirm("Delete history").then(function () {
removeHistory(obj.id);
drawHistoryMenu();
});
});
historyUL.prepend(li);
});
var header = document.createElement("li")
header.classList.add("list-group-item");
header.classList.add("history");
header.textContent = "History";
historyUL.prepend(header);
}
const saveHistory = (auto) => {
var name = getName();
var txt = editor.value;
var str = getHistoryText(nowId);
if (str === txt) {
showMessage("info", "No changes, so no saving.");
return;
}
var str = localStorage.getItem(HistoryKey);
var history = [];
if (str !== null && str !== "") {
history = JSON.parse(str);
}
var obj = {};
obj.name = name;
obj.date = (new Date()).toISOString();
obj.id = self.crypto.randomUUID();
nowId = obj.id;
history.push(obj);
var newhis = [];
var leng = history.length;
for (var idx = 0; idx < leng; ++idx) {
var obj = history[idx];
var remove = false;
if ((leng - historyLimit) > idx) {
remove = true;
}
if (remove) {
localStorage.removeItem(StorageHeader + obj.id);
} else {
newhis.push(obj);
}
}
setHistoryList(newhis);
localStorage.setItem(StorageHeader + obj.id, txt);
showMessage("success", "Save " + name);
drawHistoryMenu();
}
const info = document.querySelector("#infoBtn");
info.addEventListener("click", function (e) {
viewText(versionText);
toggleDrawer();
});
/**
* Nav Operation
*/
const nameTxt = document.querySelector("#name");
const getName = () => {
var n = nameTxt.value;
if (n === "") {
n = "empty.md";
}
return n;
}
const getNameExt = (ext) => {
var n = getName();
var idx = n.lastIndexOf("\.");
if (idx !== -1) {
n = n.substring(0, idx);
}
return n + "." + ext;
}
const saveBtn = document.querySelector("#saveBtn");
saveBtn.addEventListener("click", function () {
saveHistory(false);
});
const setAutoSave = () => {
saveTextInterval = setInterval(function (e) {
saveHistory(true);
}, 1000 * 60 * saveMinite);
}
const autoBtn = document.querySelector("#autoBtn");
autoBtn.addEventListener("change", function () {
var autoSave = autoBtn.checked;
showMessage("info", "Auto Save " + (autoSave ? "start" : "stop"));
if (autoSave) {
if (saveTextInterval !== -1) {
clearInterval(saveTextInterval);
} else {
setAutoSave();
}
} else {
saveTextInterval = -1;
}
});
const downloadBtn = document.querySelector("#downloadBtn");
downloadBtn.addEventListener("click", function () {
var n = getNameExt("md");
var text = editor.value;
const blob = new Blob([text], { type: "text/plain" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = n;
a.click();
});
// More on Light/Dark Mode later.
const errModal = document.querySelector("#confirm");
const msgTxt = errModal.querySelector("#confirmMsg");
const dialog = new bootstrap.Modal(errModal);
const showConfirmDialog = (msg, callback) => {
msgTxt.textContent = msg;
dialog.show();
var hidden = function () {
//fade not work
var elm = document.activeElement;
if (elm.classList.contains("apply")) {
callback(true)
} else {
callback(false)
}
errModal.removeEventListener("hidden.bs.modal", hidden);
}
errModal.addEventListener("hidden.bs.modal", hidden);
}
const toastElm = document.querySelector("#message");
var toast = new bootstrap.Toast(toastElm);
const showToast = (auto, icon, alert, msg, stack) => {
toast._config.delay = 3000;
toast._config.autohide = auto;
var frame = toastElm.querySelector("div");
frame.classList.remove(...frame.classList);
frame.classList.add("alert", alert);
var i = frame.querySelector("i");
i.classList.remove(...i.classList);
i.classList.add("bi", icon);
if (stack != undefined) {
i.addEventListener("dblclick", function () {
});
}
var span = toastElm.querySelector("span");
span.textContent = msg;
toast.show();
}
const showErrorMessage = (err) => {
showMessage("error", msg, stack);
}
const showMessage = (t, msg, stack) => {
var auto = true;
var icon = "bi-info-circle";
var alert = "alert-" + t;
if (t === "success") {
icon = "bi-check-circle";
} else if (t === "warning") {
icon = "bi-exclamation-triangle";
auto = false;
} else if (t === "error") {
icon = "bi-exclamation-circle";
alert = "alert-danger";
auto = false;
}
showToast(auto, icon, alert, msg, stack);
}
showConfirm = async (msg) => {
var p = new Promise((res, rej) => {
try {
showConfirmDialog(msg, function (ok) {
if (ok) {
res();
} else {
rej();
}
})
} catch (err) {
console.warn(err);
rej(err);
}
});
return p;
}
const html = document.querySelector("#html");
const hidden = document.querySelector("#hidden");
const cntl = document.querySelector("#htmlController");
const dwn = document.querySelector("#htmlDownloadBtn");
const back = document.querySelector("#htmlCloseBtn");
dwn.addEventListener("click", function (e) {
cntl.classList.remove("show");
const blob = new Blob([document.documentElement.outerHTML], { type: 'text/html' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = getNameExt("html");
link.click();
html.classList.remove("show");
});
back.addEventListener("click", function (e) {
cntl.classList.remove("show");
html.classList.remove("show");
})
hidden.addEventListener("click", function (e) {
renderHTML(html,editor.value);
//html.innerHTML = viewer.innerHTML;
cntl.classList.add("show");
html.classList.add("show");
});
const styleModal = document.querySelector("#viewerStyle");
const styleApply = styleModal.querySelector("#styleApply");
const styleText = styleModal.querySelector("#viewerStyleText");
const styleDialog = new bootstrap.Modal(styleModal);
const styleBtn = document.querySelector("#styleBtn");
styleBtn.addEventListener("click",function() {
styleText.value = innerStyle.textContent;
styleDialog.show();
});
styleApply.addEventListener("click",function() {
var val = styleText.value;
var viewerStyleId = localStorage.getItem(ViewerStyleKey);
localStorage.setItem(ViewerStyleKey + "." + viewerStyleId,val);
innerStyle.textContent = val;
styleDialog.hide();
})
const ldBtns = document.querySelectorAll(".btnLightDark");
const icons = document.querySelectorAll(".iconLightDark");
const changeLightDarkIcon = (icon, mode) => {
if (mode === "light") {
icon.classList.remove("bi-sun");
icon.classList.add("bi-moon");
} else {
icon.classList.remove("bi-moon");
icon.classList.add("bi-sun");
}
}
const changeLightDarkAllIcon = (mode) => {
icons.forEach((icon) => {
changeLightDarkIcon(icon, mode);
})
}
const changeLightDark = () => {
var list = document.body.classList;
list.toggle("light-mode");
list.toggle("dark-mode");
var mode = "light";
if (list.contains("dark-mode")) {
mode = "dark";
}
localStorage.setItem(StorageHeader + "mode", mode);
changeLightDarkAllIcon(mode);
};
ldBtns.forEach((btn) => {
btn.addEventListener("click", changeLightDark);
})
const initializeLightDark = () => {
//設定があるかを確認
var mode = localStorage.getItem(StorageHeader + "mode");
if (mode === null) {
mode = "light";
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
mode = "dark";
}
}
document.body.classList.add(mode + "-mode");
if (mode === "light") {
//初期値を消す
document.body.classList.remove("dark-mode");
}
changeLightDarkAllIcon(mode);
}
window.addEventListener("resize", function () {
redrawEditor(nowWidth);
var vendor = document.querySelector("#zenuml-intersection-detector-container");
if (vendor !== null) {
vendor.style.width = "100vw";
}
if ( html.classList.contains("show") ) {
renderHTML(html,editor.value);
}
});
//Overwrite style object
const writeStyle = (elm, obj) => {
//Overwrite only existing keys
Object.keys(obj).forEach((key) => {
elm.style[key] = obj[key];
})
}
const ViewerStyleKey = StorageHeader + "viewerStyle";
const ViewerStyleListKey = StorageHeader + "viewerStyleList";
const initializeViewerStyle = () => {
var style = innerStyle.textContent;
var viewerStyleId = localStorage.getItem(ViewerStyleKey);
if ( viewerStyleId !== null ) {
style = localStorage.getItem(ViewerStyleKey + "." + viewerStyleId);
} else {
//default setting
var id = self.crypto.randomUUID();
var list = [];
var obj = {};
obj.id = id;
obj.name = "default";
list.push(obj);
localStorage.setItem(ViewerStyleKey,id);
localStorage.setItem(ViewerStyleListKey,list)
localStorage.setItem(ViewerStyleKey + "." + id,style);
}
innerStyle.textContent = style;
}
const EditorStyleKey = StorageHeader + "editorStyle";
const initializeEditorStyle = () => {
//キー値の取得
var fontStyle = localStorage.getItem(EditorStyleKey);
if (fontStyle !== null) {
writeStyle(editor, JSON.parse(fontStyle));
}
}
const initializeStyle = () => {
initializeEditorStyle();
initializeViewerStyle();
}
const initialize = () => {
initializeLightDark();
initializeStyle();
editor.value = infoText;
viewEditorText();
drawHistoryMenu();
setAutoSave();
}
initialize();
});
var infoText = `# Itoma
Itoma is markdown viewer.
## Edit Text
Write markdown.
You can also drag and drop text files.(Limited to 10kB)
## Download File
There is no way to save the file itself, so please download and overwrite the edited text.
## Download View File
Press the button at the bottom right of the viewer to switch to HTML mode.
Please download the HTML and use it.
## Other
#### History Save
There is an auto-save feature so that you can use it even if you suddenly close your browser (once every 5 minutes).
If the contents are the same, they will not be saved.
The work state is saved as history (up to 16 times).
To open it, select History from the menu.
History is stored in localStorage.
### Editor font
You can change the font using the icon in the bottom right of the editor.
The font name cannot be obtained, so please look it up and enter it yourself.
e.g.) "Source Code Pro" "Fira Code" "Proggy Fonts" etc...
If it's installed on your device, it should be applied.
Once you specify a color, it cannot be changed in Light or Dark mode.
To revert, press "Erase" to apply
### Style Setting
By default, the following is set.
<pre class="css">
.htmlViewer > h1 {
border-bottom: 2px solid var(--viewer-header-color);
}
.htmlViewer > h2 {
border-bottom: 1px solid var(--viewer-header-color);
}
</pre>
".htmlViewer" is where the HTML will be output.
If you want to switch between light and dark modes.Please use "body.light-mode" and "body.dark-mode".
Please refer to this file with development tools to check the currently set custom properties.
For example, the following setting will change the color of the "ZenUML" comments.
<pre class="css">
.zenuml .comments p {
color: blue;
}
</pre>
## Development
The basic premise of Itoma is that it works with a single HTML file.
I would like to split it into JS files and CSS files, but I am holding back and implementing it.
`
var versionText = `
# Itoma 0.3.3
## Mermaid version
<pre class="mermaid">
info
</pre>
## Fixes(0.3.2 -> 0.3.3)
- Switch between version information and initial display.
- Added redrawing when resizing in HTML mode.
## Fixes(0.3.1 -> 0.3.2)
- Changed drawing in HTML mode from copy to render
## Source
https://gist.github.com/secondarykey/f7daf8da25c4b2384420bd190ac18245
`
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment