| 1 |
import { h, t, getById, debounce, writeError, resetLog } from "/common.module.js"; |
| 2 |
import lzString from "/lz-string.module.js"; |
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
const htmlPrefix = "h:"; |
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
const allowedTags = new Set(["B", "STRONG", "I", "EM", "U", "UL", "OL", "LI", "DIV", "P", "BR"]); |
| 12 |
const droppedTags = new Set(["SCRIPT", "STYLE", "TITLE", "TEXTAREA"]); |
| 13 |
|
| 14 |
const input = getById("input"); |
| 15 |
const toolbar = getById("toolbar"); |
| 16 |
const buttons = [...toolbar.querySelectorAll("button[data-command]")]; |
| 17 |
|
| 18 |
|
| 19 |
|
| 20 |
let written = null; |
| 21 |
|
| 22 |
document.execCommand("defaultParagraphSeparator", false, "div"); |
| 23 |
document.execCommand("styleWithCSS", false, false); |
| 24 |
|
| 25 |
|
| 26 |
toolbar.addEventListener("mousedown", event => { |
| 27 |
if (event.target.closest("button")) event.preventDefault(); |
| 28 |
}); |
| 29 |
toolbar.addEventListener("click", event => { |
| 30 |
const button = event.target.closest("button[data-command]"); |
| 31 |
if (!button) return; |
| 32 |
input.focus(); |
| 33 |
document.execCommand(button.dataset.command); |
| 34 |
updateToolbar(); |
| 35 |
}); |
| 36 |
|
| 37 |
|
| 38 |
|
| 39 |
input.addEventListener("keydown", event => { |
| 40 |
if (event.key !== "Tab" || event.ctrlKey || event.altKey || event.metaKey) return; |
| 41 |
const node = document.getSelection()?.anchorNode; |
| 42 |
const element = node instanceof Element ? node : node?.parentElement; |
| 43 |
if (!element?.closest("li")) return; |
| 44 |
event.preventDefault(); |
| 45 |
document.execCommand(event.shiftKey ? "outdent" : "indent"); |
| 46 |
}); |
| 47 |
|
| 48 |
|
| 49 |
|
| 50 |
input.addEventListener("paste", event => { |
| 51 |
const html = event.clipboardData?.getData("text/html"); |
| 52 |
if (!html) return; |
| 53 |
event.preventDefault(); |
| 54 |
document.execCommand("insertHTML", false, h("div", clean(parse(html))).innerHTML); |
| 55 |
}); |
| 56 |
|
| 57 |
input.addEventListener("input", debounce(() => { |
| 58 |
if (input.textContent === '') { |
| 59 |
written = ''; |
| 60 |
} |
| 61 |
else { |
| 62 |
const html = h("div", clean(input.childNodes)).innerHTML; |
| 63 |
written = '#' + htmlPrefix + lzString.compressToEncodedURIComponent(html); |
| 64 |
} |
| 65 |
window.location.hash = written; |
| 66 |
resetLog(); |
| 67 |
}, 10)); |
| 68 |
|
| 69 |
document.addEventListener("selectionchange", updateToolbar); |
| 70 |
|
| 71 |
window.addEventListener('hashchange', loadState); |
| 72 |
loadState(); |
| 73 |
|
| 74 |
function loadState() { |
| 75 |
if (window.location.hash === written) return; |
| 76 |
written = window.location.hash; |
| 77 |
|
| 78 |
const hash = window.location.hash.substring(1); |
| 79 |
if (hash === '') { |
| 80 |
input.replaceChildren(); |
| 81 |
return; |
| 82 |
} |
| 83 |
|
| 84 |
const isHtml = hash.startsWith(htmlPrefix); |
| 85 |
const note = lzString.decompressFromEncodedURIComponent(isHtml ? hash.substring(htmlPrefix.length) : hash); |
| 86 |
if (!note) { |
| 87 |
|
| 88 |
writeError("Failed to load note from url."); |
| 89 |
return; |
| 90 |
} |
| 91 |
input.replaceChildren(...(isHtml ? clean(parse(note)) : lines(note))); |
| 92 |
} |
| 93 |
|
| 94 |
function updateToolbar() { |
| 95 |
for (const button of buttons) { |
| 96 |
button.setAttribute("aria-pressed", String(document.queryCommandState(button.dataset.command))); |
| 97 |
} |
| 98 |
} |
| 99 |
|
| 100 |
|
| 101 |
|
| 102 |
|
| 103 |
|
| 104 |
|
| 105 |
|
| 106 |
function parse(html) { |
| 107 |
return new DOMParser().parseFromString(html, "text/html").body.childNodes; |
| 108 |
} |
| 109 |
|
| 110 |
|
| 111 |
|
| 112 |
|
| 113 |
|
| 114 |
|
| 115 |
function clean(nodes) { |
| 116 |
const result = []; |
| 117 |
for (const node of nodes) { |
| 118 |
if (node.nodeType === Node.TEXT_NODE) { |
| 119 |
result.push(t(node.data)); |
| 120 |
} else if (node.nodeType !== Node.ELEMENT_NODE || droppedTags.has(node.tagName)) { |
| 121 |
|
| 122 |
} else if (allowedTags.has(node.tagName)) { |
| 123 |
result.push(h(node.tagName.toLowerCase(), clean(node.childNodes))); |
| 124 |
} else { |
| 125 |
result.push(...clean(node.childNodes)); |
| 126 |
} |
| 127 |
} |
| 128 |
return result; |
| 129 |
} |
| 130 |
|
| 131 |
|
| 132 |
|
| 133 |
|
| 134 |
|
| 135 |
|
| 136 |
|
| 137 |
function lines(text) { |
| 138 |
return text.split("\n").map(line => |
| 139 |
line === '' ? h("div", [h("br")]) : h("div", line.replace(/ (?= )/g, " "))); |
| 140 |
} |