split out format bar, add preview

This commit is contained in:
Nicholas Jitkoff 2022-06-29 08:34:40 -07:00
parent 0cc414e307
commit cff86a16e1
4 changed files with 199 additions and 186 deletions

View file

@ -108,10 +108,8 @@ class DataURL {
return this;
}
compress = async (format = LZMA_MARKER) => {
console.log("data", this.data.length, {data:this.data})
let rawData = await base64ToByteArray(this.data);
compress = async (format = GZIP_MARKER) => {
let rawData = this.encoding ? await base64ToByteArray(this.data) : stringToByteArray(this.data);
let compressedData = await compressData(rawData, format);
var base64String = dataToBase64(compressedData);
this.data = base64String;
@ -227,36 +225,16 @@ function infoForDataURL(url) {
var BASE64_MARKER = 'base64';
var LZMA64_MARKER = 'bxze64';
var GZIP64_MARKER = 'gzip64';
var BROT64_MARKER = 'brot64';
var BASE_MARKER = 'bs';
var LZMA_MARKER = 'xz';
var GZIP_MARKER = 'gz';
var BROT_MARKER = 'br';
function compressDataURL(dataURL, callback) {
var base64Index = dataURL.indexOf(BASE64_MARKER);
var base64 = dataURL.substring(base64Index + BASE64_MARKER.length + 1);
compressString(base64ToByteArray(base64), LZMA64_MARKER, function(result) {
callback(dataURL.substring(0, base64Index) + LZMA64_MARKER + "," + result)
})
}
function base64ToByteArray(base64) {
return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
}
// function base64ToByteArray(base64) {
// var raw = window.atob(base64);
// var rawLength = raw.length;
// var array = new Uint8Array(new ArrayBuffer(rawLength));
// for(let i = 0; i < rawLength; i++) {
// array[i] = raw.charCodeAt(i);
// }
// return array;
// }
function loadScript(src, callback) {
let script = el("script", { src });
script.addEventListener('load', function(e) {
@ -273,42 +251,30 @@ function escapeStringForIMessage(str) {
return str;
}
function decryptBase64(cipher, base64, callback) {
if (!cipher) return callback(base64);
// function decompressDataURL(dataURL, preamble, callback) {
// let info = infoForDataURL(dataURL);
loadScript("/js/crypto-js.min.js", () => {
let pass = prompt("This page is encrypted. What's the passcode?");
if (!pass) return callback(base64);
let decrypted = CryptoJS[cipher.toUpperCase()].decrypt(base64, pass);
return callback(CryptoJS.enc.Base64.stringify(decrypted));
})
}
// let encoding = info.encoding;
// let encodingIndex = dataURL.indexOf(encoding);
function decompressDataURL(dataURL, preamble, callback) {
let info = infoForDataURL(dataURL);
// if (encoding && encoding != "base64") {
// var base64 = dataURL.substring(encodingIndex + LZMA64_MARKER.length + 1);
// base64 = base64.replace("=",""); // TODO: apply this elsewhere;
let encoding = info.encoding;
let encodingIndex = dataURL.indexOf(encoding);
if (encoding && encoding != "base64") {
var base64 = dataURL.substring(encodingIndex + LZMA64_MARKER.length + 1);
base64 = base64.replace("=",""); // TODO: apply this elsewhere;
decryptBase64(info.params?.cipher, base64, (base64) => {
let bytes = base64ToByteArray(base64);
decompressString(bytes, encoding, function(string) {
stringToData(string, function(data) {
if (!data) return callback();
callback(dataURL.substring(0, encodingIndex) + BASE64_MARKER + "," + (preamble || '') + data.split(',')[1], string)
})
})
})
// decryptBase64(info.params?.cipher, base64, (base64) => {
// let bytes = base64ToByteArray(base64);
// decompressString(bytes, encoding, function(string) {
// stringToData(string, function(data) {
// if (!data) return callback();
// callback(dataURL.substring(0, encodingIndex) + BASE64_MARKER + "," + (preamble || '') + data.split(',')[1], string)
// })
// })
// })
} else {
callback(dataURL)
}
}
// } else {
// callback(dataURL)
// }
// }
async function hashString(string, base = 36) {
const arrayBuffer = await(new TextEncoder().encode(string))
@ -328,48 +294,48 @@ async function hashString(string, base = 36) {
return hashAsBase64;
}
function compressString(string, encoding = LZMA64_MARKER, callback) {
if (encoding == LZMA64_MARKER) {
loadScript("/js/lzma/lzma_worker-min.js", () => {
LZMA.compress(string, 9, function(result, error) {
if (error) console.error(error);
var base64String = window.btoa(String.fromCharCode.apply(null, new Uint8Array(result)));
return callback(base64String);
});
})
} else if (encoding == BROT64_MARKER) {
// import("/js/brotli/decode.js").then((module) => {
// console.log("module", module)
// return callback(module.BrotliDecode(data));
// });
} else if (encoding == GZIP64_MARKER) {
import("/js/gzip/pako.js").then((module) => {
let result = pako.deflate(string, {level:"9"});
var base64String = window.btoa(String.fromCharCode.apply(null, new Uint8Array(result)));
return callback(base64String);
});
}
}
// function compressString(string, encoding = LZMA64_MARKER, callback) {
// if (encoding == LZMA64_MARKER) {
// loadScript("/js/lzma/lzma_worker-min.js", () => {
// LZMA.compress(string, 9, function(result, error) {
// if (error) console.error(error);
// var base64String = window.btoa(String.fromCharCode.apply(null, new Uint8Array(result)));
// return callback(base64String);
// });
// })
// } else if (encoding == BROT64_MARKER) {
// // import("/js/brotli/decode.js").then((module) => {
// // console.log("module", module)
// // return callback(module.BrotliDecode(data));
// // });
// } else if (encoding == GZIP64_MARKER) {
// import("/js/gzip/pako.js").then((module) => {
// let result = pako.deflate(string, {level:"9"});
// var base64String = window.btoa(String.fromCharCode.apply(null, new Uint8Array(result)));
// return callback(base64String);
// });
// }
// }
function decompressString(data, encoding, callback) {
if (encoding == LZMA64_MARKER) {
LZMA.decompress(data, function(result, error) {
if (!(typeof result === 'string')) result = new Uint8Array(result)
if (error) console.error(error);
callback(result);
});
} else if (encoding == BROT64_MARKER) {
import("/js/brotli/decode.js").then((module) => {
console.log("module", module)
return callback(module.BrotliDecode(data));
});
} else if (encoding == GZIP64_MARKER) {
import("/js/gzip/pako.js").then((module) => {
let byteArray = pako.inflate(data);
return callback(byteArray);
});
}
}
// function decompressString(data, encoding, callback) {
// if (encoding == LZMA64_MARKER) {
// LZMA.decompress(data, function(result, error) {
// if (!(typeof result === 'string')) result = new Uint8Array(result)
// if (error) console.error(error);
// callback(result);
// });
// } else if (encoding == BROT64_MARKER) {
// import("/js/brotli/decode.js").then((module) => {
// console.log("module", module)
// return callback(module.BrotliDecode(data));
// });
// } else if (encoding == GZIP64_MARKER) {
// import("/js/gzip/pako.js").then((module) => {
// let byteArray = pako.inflate(data);
// return callback(byteArray);
// });
// }
// }
// echo -n 'hello world' | brotli | base64
@ -488,10 +454,10 @@ export {
infoForDataURL,
stringToData,
dataToString,
compressString,
decompressString,
compressDataURL,
decompressDataURL,
// compressString,
// decompressString,
// compressDataURL,
// decompressDataURL,
hashString,
encodePrettyComponent,
decodePrettyComponent,
@ -500,8 +466,6 @@ export {
parseBittyURL,
BASE64_MARKER,
LZMA64_MARKER,
GZIP64_MARKER,
BROT64_MARKER,
BASE_MARKER,
LZMA_MARKER,
GZIP_MARKER,

View file

@ -25,16 +25,37 @@ html {
}
body {
margin: 0 auto;
padding: 10rem 6vmin 4vmin 6vmin;
max-width: 45em;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
line-height: 1.5em;
min-height:100vh;
display:
flex;
flex-direction: row-reverse;
margin: 0;
}
.editor-container {
margin: 0 auto;
padding: 5rem 6vmin 4vmin 6vmin;
max-width: 45em;
/* background-color:#0070e0; */
box-sizing:
border-box;
}
body:not(.preview) #preview-frame {
display:none;
}
#preview-frame {
background-color:#fafafa;
border:0;
/* position:absolute; */
right:0;
width:50%;
bottom:0;
height:100vh;
background: repeating-conic-gradient(rgba(128,128,128,.10) 0% 25%, transparent 0% 50%) 50% / 4px 4px;
}
h1 {
font-weight: 400;
}
@ -88,7 +109,7 @@ body.drag #content {
outline: none;
/* min-width: 180px; */
top: 0;
position: absolute;
/* position: absolute; */
padding: 0 1em;
font-weight: normal;
opacity:0.33;
@ -98,8 +119,9 @@ body.drag #content {
margin-left: -1em;
line-height: 2.5em;
display: inline-block;
/* margin-top: -31em; */
/* margin-top: -2.5em; */
z-index: 100;
left: 0;
}
#doc-title.open {
@ -193,7 +215,7 @@ input:focus {
position: fixed;
top: 0;
left: 0;
width: 100vw;
width: 50vw;
height: 100vh;
background-position: center 60px;
box-sizing: border-box;
@ -290,7 +312,7 @@ input:focus {
padding:0;
border:
none;
background-color:var(--background-color);
/* background-color:var(--background-color); */
display:
flex;
align-items: left;
@ -299,6 +321,7 @@ input:focus {
flex-wrap: wrap-reverse;
/* height: 10rem; */
padding-left: 4vmin;
z-index:1000;
}
#menubar > span {
display: flex;
@ -529,8 +552,8 @@ label:first-child {
cursor:default;
}
#menubar.ql-snow.ql-toolbar button,
#menubar.ql-snow.ql-toolbar .ql-picker {
#formatbar.ql-snow.ql-toolbar button,
#formatbar.ql-snow.ql-toolbar .ql-picker {
height: 40px;
min-width: 40px;
float: none;
@ -538,8 +561,8 @@ label:first-child {
text-align: revert;
width: 40px;
}
#menubar.ql-snow.ql-toolbar button,
#menubar.ql-snow.ql-toolbar .ql-picker .ql-picker-label {
#formatbar.ql-snow.ql-toolbar button,
#formatbar.ql-snow.ql-toolbar .ql-picker .ql-picker-label {
/* background-size: 24px; */
padding: 10px;
}
@ -554,7 +577,7 @@ div#editor.ql-container.ql-snow {
height: 1.5rem;
}
#menubar.ql-snow .ql-color-picker .ql-picker-options {
#formatbar.ql-snow .ql-color-picker .ql-picker-options {
width: 11.2rem;
padding: 0;
}
@ -569,10 +592,10 @@ div#editor.ql-container.ql-snow {
}
#menubar .ql-toolbar,
#menubar .ql-stroke,
#menubar .ql-picker,
#menubar.ql-snow a {
#formatbar .ql-toolbar,
#formatbar .ql-stroke,
#formatbar .ql-picker,
#formatbar.ql-snow a {
color: var(--text-color) ;
stroke: var(--text-color) ;
}
@ -604,9 +627,9 @@ label:not(:first-child) {
font-size: 16px;
}
.ql-formats {
opacity:0.0;
}
#formatbar .ql-formats {
opacity:1.0;
margin-right:0;display: contents;}
body:not(.dragging) #drop-zone {
display:none;
@ -625,4 +648,29 @@ body:not(.dragging) #drop-zone {
font-family: sans-serif;
color: white;
line-height: 100vh;
opacity:0.2;
opacity:0.2;
}
#formatbar {
margin-top:-8em;
border:
none;
/* border-bottom:1px solid
var(--divider-color); */
position: sticky;
top: 0;
z-index: 100;
background-color: var(--background-color);
padding: 0;
margin: 0 -12px;
display: flex;
flex-wrap: wrap;
opacity:0.25;
}
#formatbar:hover {
opacity:1.0;
}
body.filecontent #formatbar {
display:none;
}

View file

@ -2,7 +2,7 @@
<html>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src data: 'self'; script-src 'self'; connect-src https://codepen.io; font-src https://fonts.gstatic.com; style-src https://fonts.googleapis.com 'unsafe-inline' 'self'">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src data: 'self'; script-src cdnjs.cloudflare.com cdn.quilljs.com 'self'; connect-src https://codepen.io; font-src https://fonts.gstatic.com; style-src https://fonts.googleapis.com cdnjs.cloudflare.com cdn.quilljs.com 'unsafe-inline' 'self'">
<meta id="themeColor" name="theme-color" content="#f1f1f1" media="(prefers-color-scheme: light)">
<meta id="themeColorDark" name="theme-color" content="#121212" media="(prefers-color-scheme: dark)">
<link rel="canonical" id="canonical" href="/edit">
@ -22,62 +22,70 @@
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined">
<body>
<iframe id="preview-frame"></iframe>
<div class="editor-container">
<div id="doc-file"></div>
<input id="file-input" type="file" name="name" style="display: none;" />
<div id="drop-zone">DROP HERE</div>
<div id="placeholder">
&nbsp;itty bitty things<br>
&nbsp;can be conveyed in a link.<br>
&nbsp;type here &ndash; and then share!<p>&nbsp;<a id="ib-info" href="http://about.bitty.site" target="_blank">about itty bitty</a><br>
</div>
<div id="drop-zone"></div>
<div id="copy-message">Link copied to clipboard.</div>
<div id="placeholder">&nbsp;itty bitty things<br>&nbsp;can be conveyed in a link.<br>&nbsp;type here &ndash; and then share!<p>&nbsp;<a id="ib-info" href="http://about.bitty.site" target="_blank">about itty bitty</a></div>
<div id="doc-title" class="toolbarbutton" placeholder="untitled">
<div class="title" id="doc-title-text">untitled</div>
<form id="md-contents" class="menu">
<div class="inputrow"><label for="md-title">title</label><input type="text" name="title" id="md-title" placeholder="untitled"></div>
<div class="inputrow"><label for="md-include">metadata</label><input type="checkbox" name="includeMetadata" id="md-include" placeholder="untitled"><label for="md-include">include social metadata</label></div>
<div class="inputrow"><label for="md-description">description</label><input type="text" name="description" id="md-description" placeholder="text"></div>
<div class="inputrow"><label for="md-favicon">favicon</label><input type="text" name="favicon" id="md-favicon" placeholder="emoji or url"></div>
<div class="inputrow"><label for="md-image">image</label><input type="text" name="image" id="md-image" placeholder="image url"></div>
<div class="inputrow"><label for="md-password">password</label><input type="text" name="password" id="md-password" placeholder="encryption password"></div>
</form>
</div>
<div id="editor"></div>
<div class="toolbar" id="menubar">
<!-- <div class="spacer"></div> -->
<div class="toolbar hide-before-edit" id="formatbar">
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<button class="ql-underline"></button>
<select class="ql-header" id="ql-header">HX</select>
</span>
<span class="ql-formats">
<select class="ql-color"><option value="#F6D4D2"/><option value="#FAE4D3"/><option value="#FEF3D5"/><option value="#CFE8DC"/><option value="#CFE0E8"/><option value="#E1D7E8"/><option value="white"/><option value="#E99590"/><option value="#F3BB92"/><option value="#FDE095"/><option value="#86C6A8"/><option value="#86B1C6"/><option value="#B39BC6"/><option value="#A4A4A4"/><option value="#D42A20"/><option value="#E77625"/><option value="#FAC22B"/><option value="#0E8E51"/><option value="#0E638E"/><option value="#67378D"/><option value="#727272"/><option value="#BF0000"/><option value="#DB3F00"/><option value="#F8A600"/><option value="#005E12"/><option value="#00275E"/><option value="#2C005C"/><option value="#3D3D3D"/><option value="#860000"/><option value="#943E00"/><option value="#AE7400"/><option value="#00420D"/><option value="#001B42"/><option value="#1F0040"/><option value="black"/></select>
<select class="ql-background"><option value="#F6D4D2"/><option value="#FAE4D3"/><option value="#FEF3D5"/><option value="#CFE8DC"/><option value="#CFE0E8"/><option value="#E1D7E8"/><option value="white"/><option value="#E99590"/><option value="#F3BB92"/><option value="#FDE095"/><option value="#86C6A8"/><option value="#86B1C6"/><option value="#B39BC6"/><option value="#A4A4A4"/><option value="#D42A20"/><option value="#E77625"/><option value="#FAC22B"/><option value="#0E8E51"/><option value="#0E638E"/><option value="#67378D"/><option value="#727272"/><option value="#BF0000"/><option value="#DB3F00"/><option value="#F8A600"/><option value="#005E12"/><option value="#00275E"/><option value="#2C005C"/><option value="#3D3D3D"/><option value="#860000"/><option value="#943E00"/><option value="#AE7400"/><option value="#00420D"/><option value="#001B42"/><option value="#1F0040"/><option value="black"/></select>
<button class="ql-clean"></button>
</span>
<span class="ql-formats">
<select class="ql-align"></select>
<button class="ql-blockquote"></button>
<button class="ql-code-block"></button>
</span>
<span class="ql-formats">
<button class="ql-clean"></button>
<button class="ql-link"></button>
</span>
<span class="ql-formats">
<button type="button" class="ql-list" value="bullet"></button>
<button type="button" class="ql-list" value="ordered"></button>
</span>
<span class="ql-formats">
<button class="ql-indent" value="-1"></button>
<button class="ql-indent" value="+1"></button>
</span>
</div>
<div id="placeholder">&nbsp;itty bitty things<br>&nbsp;can be conveyed in a link.<br>&nbsp;type here &ndash; and then share!<p>&nbsp;<a id="ib-info" href="http://about.bitty.site" target="_blank">about itty bitty</a></div>
<div id="doc-title" class="toolbarbutton hide-before-edit" placeholder="untitled">
<div class="title" id="doc-title-text">untitled</div>
<form id="md-contents" class="menu">
<div class="inputrow"><label for="md-title">title</label><input type="text" name="title" id="md-title" placeholder="untitled"></div>
<div class="inputrow"><label for="md-include">metadata</label><input type="checkbox" name="includeMetadata" id="md-include" placeholder="untitled"><label for="md-include">include social metadata</label></div>
<div class="inputrow"><label for="md-description">description</label><input type="text" name="description" id="md-description" placeholder="text"></div>
<div class="inputrow"><label for="md-favicon">favicon</label><input type="text" name="favicon" id="md-favicon" placeholder="emoji or url"></div>
<div class="inputrow"><label for="md-image">image</label><input type="text" name="image" id="md-image" placeholder="image url"></div>
<div class="inputrow"><label for="md-password">password</label><input type="text" name="password" id="md-password" placeholder="encryption password"></div>
</form>
</div>
<div id="editor"></div>
<div class="toolbar" id="menubar">
<!-- <div class="spacer"></div> -->
<div class="toolbar-right">
<div id="length" class="hide-before-edit"></div>
<div id="share" class="hide-before-edit menu-item">share</div>
<div id="mainmenu">
<div class="menu-item">menu</div>
<div id="menu-contents" class="menu">
<a id="preview">show preview</a>
<a id="copy">copy link</a>
<hr>
<span id="menus-share-hint" class="disabled">share via</span><br>
@ -92,12 +100,7 @@
</div>
</div>
</div>
</div>
<div class="toolbar" id="formatbar">
</div>
</body>
</html>

View file

@ -22,7 +22,7 @@ var quill = new Quill('#editor', {
modules: {
syntax: true,
keyboard: { bindings },
toolbar: "#menubar"
toolbar: "#formatbar"
}
});
@ -69,14 +69,14 @@ window.onload = function() {
}
});
content.addEventListener("keydown", handleKey);
content.addEventListener("keyup", handleInput);
// content.addEventListener("keydown", handleKey);
// content.addEventListener("keyup", handleInput);
QS("#doc-title").addEventListener("keyup", handleInput);
Array.from(document.getElementsByTagName("input")).forEach(i => i.addEventListener("keydown", handleInput))
document.getElementById("drop-zone").addEventListener("drop", handleDrop);
document.getElementById("drop-zone").addEventListener("dragover", e => e.preventDefault());
content.addEventListener("paste", handlePaste);
editor.addEventListener("paste", handlePaste);
// content.contentEditable = "true";
editor.focus();
// document.execCommand("selectAll", false, null);
@ -86,9 +86,13 @@ window.onload = function() {
if (!navigator.share) QS("#share").style.display = "none"
QS("#twitter").onclick = tweetLink;
QS("#copy").onclick = copyLink;
QS("#preview").onclick = togglePreview;
QS("#mainmenu").onclick = () => { toggleMenu(QS("#mainmenu"))};
QS("#doc-title").onclick = toggleMetadata;
QS("#doc-title").onclick = toggleMetadata;
var hash = window.location.hash.substring(1);
if (hash.length) {
@ -110,7 +114,7 @@ window.onload = function() {
};
function setContent(html) {
content.innerHTML = html;
editor.innerHTML = html;
updateBodyClass();
}
@ -128,6 +132,10 @@ function updateBodyClass(hasContent) {
} else {
document.body.classList.remove("edited");
}
console.log("importedFileData",importedFileData != undefined)
document.body.classList.toggle("filecontent", importedFileData != undefined);
document.body.classList.add("loaded");
}
@ -148,23 +156,9 @@ async function handleDrop(e) {
if (ratio <= 0.95) url = durl.href;
importedFileData = url;
updateLink(url, file.name, true);
updateLink(url, {title:file.name}, true);
setFileName(file.name);
// url = url.replace(DATA_PREFIX, DATA_PREFIX_8);
// bitty.compressDataURL(url, function(url2) {
// var ratio = url2.length / url.length;
// console.log("Compressed to", Math.round(ratio * 100) + "%", url2, url);
// if (e.ctrlKey)
// bitty.decompressDataURL(url2, undefined, function(url3) {
// console.log("Verified", url == url3);
// });
// if (e.altKey) url2 = url2.replace(DATA_PREFIX_BXZE, "!");
// importedFileData = durl.href;
// updateLink(url2, file.name, true);
// setFileName(file.name);
// });
},
false
);
@ -264,42 +258,40 @@ function getMetadata() {
return object;
}
function handleContentChange() {
async function handleContentChange() {
var text = editor.innerText;
let hasContent = text.trim().length > 0;
updateBodyClass(hasContent);
if (!hasContent) return;
var metadata = getMetadata();
var rawHTML = text.indexOf("</") > 0;
if (rawHTML) {
text = text.replace(/[ |\t]+/g, " ").replace(/> +</g, "> <");
} else {
text = content.innerHTML;
text = editor.innerHTML;
}
if (text.trim().length) {
const t0 = performance.now();
bitty.compressString(text, bitty.GZIP64_MARKER, function(zip) {
const t1 = performance.now();
let durl = new bitty.DataURL(`data:text/html;charset=utf-8,${text}`)
durl = await durl.compress(bitty.GZIP_MARKER);
console.log("data", durl)
// bitty.compressString(text, bitty.GZIP64_MARKER, function(zip) {
// bitty.compressString(text, bitty.LZMA64_MARKER, function(zip2) {
// const t2 = performance.now();
// console.debug("gz", Math.round(zip.length/text.length*100) + "%", Math.round((t1-t0)),"ms");
// console.debug("lz", Math.round(zip2.length/text.length*100) + "%", Math.round((t2-t1)), "ms");
// });
zip.replace("=","")
// zip.replace("=","")
if (rawHTML) {
updateLink(DATA_PREFIX_GZIP + zip, metadata);
updateLink(durl.href, metadata);
} else {
updateLink("?" + zip, metadata);
updateLink("?" + durl.href, metadata);
}
});
// });
setFileName("");
} else if (importedFileData) {
updateLink(importedFileData, {title});
@ -336,9 +328,9 @@ function updateLink(url, metadata, push) {
bittyLink = new URL(url, document.location).href;
document.getElementById("canonical").href = bittyLink;
debugger
console.log({bittyLink});
if(previewContent) QS("#preview-frame").src = bittyLink;
var hash = location.hash;
// if (push || !hash || !hash.length) {
@ -392,6 +384,12 @@ function toggleMetadata(e) {
QS("#doc-title").classList.toggle("open");
QS("#md-title").focus();
}
let previewContent = false;
function togglePreview() {
previewContent = !previewContent;
document.body.classList.toggle("preview", previewContent);
}
function copyThenLink() {
copyLink();
return confirm("Copied your link to the clipboard. Paste it to share.");