lightview
Version:
Small, simple, powerful web UI and micro front end creation ... Great ideas from Svelte, React, Vue and Riot combined.
320 lines (292 loc) • 13 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lightview:REPL</title>
</head>
<body>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/styles/default.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<div id="content" style="flex:auto;min-height:100%;width:750px;max-width:750px;height:100%;border:1px solid;padding:10px;margin:0 auto">
<div style="display:flex;flex-direction:column;">
<div id="tabs" style="flex-grow:0;width:100%;border:1px;padding-bottom:5px;text-align:center">
<span style="padding-right:10px"><label for="headhtml" l-on:click="${onTabClick}">HTML Head</label><input for="headhtml" value="${headhtmlPinned}" type="checkbox" l-on:click="${onPinClick}"></span>
<span style="padding-right:10px"><label for="bodyhtml" l-on:click="${onTabClick}">HTML Body</label><input for="bodyhtml" value="${bodyhtmlPinned}" type="checkbox" l-on:click="${onPinClick}" checked></span>
<!--span style="padding-right:10px"><label for="markdown" l-on:click="${onTabClick}">Markdown (Body)</label><input for="markdown" value="${markdownPinned}" type="checkbox" l-on:click="${onPinClick}"></span-->
<span style="padding-right:10px"><label for="css" l-on:click="${onTabClick}">Style</label><input for="css" value="${cssPinned}" type="checkbox" l-on:click="${onPinClick}"></span>
<span style="padding-right:10px"><label for="script" l-on:click="${onTabClick}">Script</label><input for="script" value="${scriptPinned}" type="checkbox" l-on:click="${onPinClick}"></span>
<span style="padding-right:10px"><label for="preview" l-on:click="${onTabClick}">Preview</label><input for="preview" value="${previewPinned}" type="checkbox" l-on:click="${onPinClick}"></span>
</div>
</div>
<div id="headhtml" style="display:none;margin:10px" class="language-html"><head><slot name="head"></slot></head></div>
<div id="bodyhtml" style="margin:10px" class="language-html"><body><slot name="body"></slot></body></div>
<!--textarea id="markdown" style="margin-right:2px;display:none">${markdown}</textarea-->
<div id="css" style="display:none;margin:10px" class="language-css"><style><slot name="css"></slot></style></div>
<div id="script" style="display:none;margin:10px" class="language-javascript"><script><slot name="script"></slot></script></div>
<iframe id="preview" style="max-width:99%;width:99%;margin:10px;display:none" src="./blank.html"></iframe>
</div>
<div style="width:100%;text-align:center">
<div style="padding:5px">${source}</div>
<button l-on:click="${doSave}">Save</button>
<button l-if="${!source.trim().startsWith('http')}" l-on:click="${doReset}">Reset</button>
</div>
<style id="style">
label:hover {
text-decoration: underline
}
</style>
<script id="lightview">
(document.currentComponent||(document.currentComponent=document.body)).mount = async function() {
const {CodeJar} = await import("https://cdn.jsdelivr.net/npm/codejar@3.6.0/codejar.min.js");
const turndownService = new TurndownService({
headingStyle: "atx",
codeBlockStyle: "fenced",
emDelimiter: "*"
});
turndownService.keep(() => true);
const {html, css, script} = await import("../types.js");
self.variables({
onTabClick: "function",
onPinClick: "function",
doSave: "function",
doReset: "function"
});
self.variables({
wysiwygPinned: "boolean",
bodyhtmlPinned: "boolean",
bodyhtml: html,
markdownPinned: "boolean",
markdown: "string",
cssPinned: "boolean",
cssText: css,
scriptPinned: "boolean",
scriptText: script,
headhtmlPinned: "boolean",
headhtml: html,
previewPinned: "boolean",
source: "string",
}, {reactive});
self.variables({
src: "string",
}, {imported});
bodyhtmlPinned = true;
const loadFromFile = async () => {
const html = await fs.readFile(url.pathname, {encoding: "utf8"});
source = `IndexedDB://${url.hostname + "_repl"}${url.pathname}`;
return html;
};
const loadFromServer = async () => {
const response = await fetch(url.href);
const html = await response.text();
source = url.href;
return html;
};
const parseFullHTML = (fullHTML) => {
const parser = new DOMParser(),
fragment = parser.parseFromString(fullHTML, "text/html"),
body_el = fragment?.querySelector("body"),
style_el = fragment?.querySelector("style"),
script_el = body_el?.querySelector("script");
if (style_el) {
cssText = style_el?.innerHTML.trim();
style_el.remove();
} else {
cssText = "";
}
if (script_el) {
scriptText = script_el?.innerHTML.trim(),
script_el.remove();
} else {
scriptText = "";
}
headhtml = (fragment?.head.innerHTML || "").trim();
bodyhtml = body_el?.innerHTML.trim();
};
let fs,
url;
if (src) {
url = new URL(src, window.location.href);
fs = new LightningFS(url.hostname + "_repl").promises;
try {
parseFullHTML(await loadFromFile());
} catch (e) {
try {
parseFullHTML(await loadFromServer());
} catch (e) {
fullHTML = e.message;
}
}
} else {
url = new URL(window.location.href);
fs = new LightningFS(url.hostname + "_repl").promises;
bodyhtml = "";
headhtml = "";
scriptText = "";
cssText = "";
}
;
// initialize variables
markdown = "";
const tabs = [...self.querySelectorAll("label[for]")]
.map((label) => {
const id = label.getAttribute("for");
return [id, self.getElementById(id), label];
});
const showTab = (targetid) => {
tabs.forEach(([id, el, label]) => {
if (id === targetid || self.varsProxy[`${id}Pinned`]) {
el.style.display = "block";
} else if (!self.varsProxy[`${id}Pinned`]) {
el.style.display = "none";
}
});
};
const hideTab = (targetid) => {
tabs.forEach(([id, el, label]) => {
if (id === targetid) el.style.display = "none";
});
};
onTabClick = (event) => {
showTab(event.target.getAttribute("for"));
};
onPinClick = (event) => {
const id = event.target.getAttribute("for"),
checked = self.varsProxy[`${id}Pinned`] = event.target.checked;
if (checked) onTabClick(event);
else hideTab(id);
};
doSave = async () => {
const parts = url.pathname.split("/");
let dir = "";
parts.shift();
parts.pop();
for (const part of parts) {
dir = dir + "/" + part;
try {
await fs.mkdir(dir);
} catch (e) {
if (e.message === "EEXIST") break;
throw e;
}
}
fs.writeFile(url.pathname, doPreview(), {encoding: "utf8"}, () => {
});
source = `IndexedDB://${url.hostname + "_repl"}${url.pathname}`;
};
doReset = async () => {
try {
await fs.unlink(url.pathname);
} catch (e) {
}
if (src) {
try {
parseFullHTML(await loadFromServer());
doPreview();
} catch (e) {
previewEl.innerHTML = fullHTML = e.message;
}
} else {
bodyhtml = "";
headhtml = "";
scriptText = "";
cssText = "";
previewEl.innerHTML = "";
}
};
const doPreview = () => {
// not quite write, createComponent needs to be reworked to handle a head and import its links
/*const template = document.createElement("template");
template.innerHTML = bodyhtml +
"<style>" + cssText + "</style>" +
'<script id="lightview">' + scriptText + "<" + "/script>";
const component = window.customElements.get("x-preview");
if (component) {
component.setTemplateNode(template);
} else {
Lightview.createComponent("x-preview", template);
}
previewEl.innerHTML = "<x-preview></x-preview>";
return "<html><head>" + headhtml + "</head><body>" + template.innerHTML + "<body></html>"*/
const str =
"<html>" +
"<head>" + headhtml.replace("./lightview.js",new URL("./lightview.js",window.location.href).href) +" </head>" +
"<body>" + bodyhtml +
"<style>" + cssText + "</style>" +
'<script id="lightview">debugger;' + scriptText + "<" + "/script>" +
"</body>" +
"</html>",
blob = new Blob([str], {type : 'text/html'}),
newurl = window.URL.createObjectURL(blob);
previewEl.src = newurl;
};
const tabsEl = self.getElementById("tabs"),
headhtmlEl = document.body.querySelector('[slot="head"]'),
bodyhtmlEl = document.body.querySelector('[slot="body"]'),
markdownEl = self.getElementById("markdown"),
cssEl = document.body.querySelector('[slot="css"]'),
scriptEl = document.body.querySelector('[slot="script"]'),
previewEl = self.getElementById("preview");
const highlight = (el,...args) => {
hljs.highlightElement(el,...args);
}
if(headhtmlEl) {
headhtmlEl.className = "language-html";
const bodyJar = CodeJar(headhtmlEl, highlight);
bodyJar.updateCode(headhtml);
bodyJar.onUpdate(code => {
headhtml = code;
doPreview();
})
}
if(bodyhtmlEl) {
bodyhtmlEl.className = "language-html";
const bodyJar = CodeJar(bodyhtmlEl, highlight);
bodyJar.updateCode(bodyhtml);
bodyJar.onUpdate(code => {
bodyhtml = code;
doPreview();
})
}
if(cssEl) {
cssEl.className = "language-css";
const bodyJar = CodeJar(cssEl, highlight);
bodyJar.updateCode(cssText);
bodyJar.onUpdate(code => {
cssText = code;
doPreview();
})
}
if(scriptEl) {
scriptEl.className = "language-javascript";
const bodyJar = CodeJar(scriptEl, highlight);
bodyJar.updateCode(scriptText);
bodyJar.onUpdate(code => {
scriptText = code;
doPreview();
})
}
doPreview();
/*let prevmarkdown; // prevents indirect recursion
observe(() => {
const text = turndownService.turndown(bodyhtml).trim();
if (text && text !== prevtext) {
markdown = markdownEl.innerHTML = prevmarkdown = text;
}
});
let prevbodyhtml; // prevents indirect recursion
observe(() => {
const html = marked.parse(markdown).trim();
if (html && html !== prevbodyhtml) {
bodyhtml = bodyhtmlEl.innerText = prevbodyhtml = html;
}
});*/
const initEditor = () => {
};
self.addEventListener("mounted", () => {
initEditor();
doPreview();
});
}
</script>
</body>
</html>