lightview
Version:
Small, simple, powerful web UI and micro front end creation ... Great ideas from Svelte, React, Vue and Riot combined.
527 lines (491 loc) • 22.2 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lightview:REPL</title>
<style>
.hljs[slot] { padding: 5px; }
</style>
</head>
<body>
<link rel="stylesheet" href="../css/highlightjs.min.css" export>
<script src="../javascript/lightning-fs.js"></script>
<script src="../javascript/isomorphic-git.js"></script>
<script src="../javascript/highlightjs.min.js"></script>
<script src="../javascript/turndown.js"></script>
<div id="content" style="flex:auto;min-width:500px;border:1px solid;padding:10px;margin-top:1em;margin-bottom:1em; auto;overflow:auto;font-size:smaller">
<div style="display:flex;flex-direction:column;">
<div l-if="${!hidetabs}" id="tabs" style="flex-grow:0;width:100%;border:1px;padding-bottom:5px;text-align:center" hidden>
<span style="padding-right:10px" for="headhtml"><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" for="bodyhtml"><label for="bodyhtml" l-on:click="${onTabClick}">HTML Body</label><input for="bodyhtml" value="${bodyhtmlPinned}" type="checkbox" l-on:click="${onPinClick}"></span>
<!--span style="padding-right:10px" for="markdown"><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" for="css"><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" for="script"><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" for="preview"><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="margin:10px" class="language-html"><slot name="headhtml"></slot></div>
<div id="bodyhtml" style="margin:10px" class="language-html"><slot name="bodyhtml"></slot></div>
<!--textarea id="markdown" style="margin-right:2px;">${markdown}</textarea-->
<div id="css" style="margin:10px" class="language-css"><slot name="css"></slot></div>
<div id="script" style="margin:10px" class="language-javascript"><slot name="script"></slot></div>
<iframe id="preview" style="height:${previewheight||'150px'};max-width:99%;width:99%;" hidden></iframe>
</div>
<div l-if="${editable}" style="width:100%;text-align:center">
<div style="padding:5px">${source}</div>
<button l-on:click="${doSave}">Save</button>
<button l-if="${canReset}" l-on:click="${doReset}">Reset</button>
</div>
<style>
label:hover {
text-decoration: underline
}
slot[name=headhtml]:not([hidden])::before { content: "<head>" }
slot[name=headhtml]:not([hidden])::after { content: "</head>" }
slot[name=bodyhtml]:not([hidden])::before { content: "<body${bodyattributes ? ' ' + bodyattributes : ''}>" }
slot[name=bodyhtml]:not([hidden])::after { content: "</body>" }
slot[name=css]:not([hidden])::before { content: "<style>" }
slot[name=css]:not([hidden])::after { content: "< /style>" }
slot[name=script]:not([hidden])::before { content: "<script ${scriptattributes}>" }
slot[name=script]:not([hidden])::after { content: "</script>" }
</style>
<script id="lightview">
(document.currentComponent||(document.currentComponent=document.body)).mount = async function() {
let CJ
try {
const {CodeJar} = await import(new URL("../javascript/codejar.min.js", this.componentBaseURI).href);
CJ = CodeJar;
} catch {
}
let turndownService;
try {
turndownService = new TurndownService({
headingStyle: "atx",
codeBlockStyle: "fenced",
emDelimiter: "*"
});
turndownService.keep(() => true);
} catch(e) {
}
const {html, css, script} = await import(new URL("../javascript/types.js", this.componentBaseURI).href);
self.variables({
onTabClick: "function",
onPinClick: "function",
doSave: "function",
doReset: "function"
});
self.variables({
wysiwygPinned: "boolean",
headhtmlPinned: "boolean",
headhtml: html,
bodyhtmlPinned: "boolean",
bodyhtml: html,
bodyattributes:"string",
markdownPinned: "boolean",
markdown: "string",
cssPinned: "boolean",
cssText: css,
scriptPinned: "boolean",
scriptText: script,
scriptattributes:"string",
source: "string",
canReset: "boolean",
editable: "boolean"
}, {reactive});
self.variables({
previewPinned: "boolean"
},{imported,reactive});
self.variables({
src: "string",
path: "string",
hidetabs: "boolean",
maintainbody: "boolean",
contentbackground:"string",
readonly: "boolean",
previewheight: "string"
}, {imported});
bodyhtmlPinned = true;
canReset = false;
bodyattributes = "";
scriptattributes = "";
editable = false;
const getPath = () => {
if(path==="$location" || path==null) return source = window.location.pathname;
return source = path;
}
const trimContent = (text) => {
let result = text;
if(result[0]==="\n") result = result.substring(1);
while(result[result.length-1]==="\n") result = result.substring(0,result.length-1);
return result;
}
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 loadFromTemplate = () => {
const templateEl = this.querySelector('[slot="src"]');
if(templateEl) {
const template = document.createElement("div");
template.innerHTML = templateEl.innerHTML;
return template;
}
}
const initFromTemplate = (template) => {
head_el = document.createElement("head");
body_el = document.createElement("body");
if(template) {
const head = template.querySelector("l-head");
if(head) {
headhtml = trimContent(head.innerHTML || "");
[...head.attributes].forEach((attr) => head_el.setAttribute(attr.name,attr.value));
} else {
headhtml = "";
}
style_el = template.querySelector("style");
if(style_el) {
cssText = trimContent(style_el.innerText);
style_el.remove();
} else {
style_el = document.createElement("style");
cssText = "";
}
script_el = template.querySelector('script:not([src*="lightview.js"])');
if(script_el && script_el.parentElement===template) {
scriptText = trimContent(script_el.innerText);
script_el.remove();
} else {
script_el = document.createElement("script");
scriptText = "";
}
const body = template.querySelector("l-body");
if(body) {
bodyhtml = trimContent(body.innerHTML);
[...body.attributes].forEach((attr) => body_el.setAttribute(attr.name,attr.value))
} else {
bodyhtml = "";
}
} else {
script_el = document.createElement("script");
style_el = document.createElement("style");
bodyhtml = "";
headhtml = "";
scriptText = "";
cssText = "";
}
body_el.appendChild(style_el);
body_el.appendChild(script_el);
bodyattributes = "";
[...body_el.attributes].forEach((attr) => bodyattributes = bodyattributes + " " + attr.name + '=\\"' + attr.value + '\\"');
scriptattributes = "";
[...script_el.attributes].forEach((attr) => scriptattributes = scriptattributes + " " + attr.name + '=\\"' + attr.value + '\\"');
}
let head_el,
body_el,
style_el,
script_el;
const parseFullHTML = (fullHTML) => {
const parser = new DOMParser(),
fragment = parser.parseFromString(fullHTML, "text/html");
head_el = fragment?.head,
body_el = fragment?.body,
style_el = fragment?.body.querySelector("style"),
script_el = fragment?.body.querySelector("script:not([src])");
if(!maintainbody) {
if (style_el) {
cssText = trimContent(style_el?.innerHTML);
style_el.remove();
} else {
cssText = "";
}
if (script_el) {
scriptText = trimContent(script_el?.innerHTML);
script_el.remove();
} else {
scriptText = "";
}
}
headhtml = trimContent(head_el?.innerHTML || "");
bodyhtml = trimContent(body_el?.innerHTML);
if(style_el && body_el) body_el.appendChild(style_el);
if(script_el && body_el) body_el.appendChild(script_el);
};
let fs,
url;
if (src) {
url = new URL(src,document.baseURI); // , document.baseURI
if(typeof(LightningFS)!=="undefined") {
fs = new LightningFS(url.hostname + "_repl").promises;
}
try {
parseFullHTML(await loadFromFile());
canReset = true;
} catch (e) {
try {
parseFullHTML(await loadFromServer());
} catch (e) {
fullHTML = e.message;
}
}
} else {
url = new URL(getPath(),document.baseURI);
if(typeof(LightningFS)!=="undefined") {
fs = new LightningFS(url.hostname + "_repl").promises;
}
try {
parseFullHTML(await loadFromFile());
canReset = true;
} catch (e) {
initFromTemplate(loadFromTemplate());
}
};
// initialize variables
markdown = "";
const tabs = [...self.querySelectorAll("span[for]")]
.map((span) => {
const id = span.getAttribute("for");
if(id==="preview") {
const el = self.getElementById(id);
if(previewPinned) el.removeAttribute("hidden");
return [id, el];
}
const slot = self.querySelector(`[slot="${id}"]`);
if(slot) {
if(slot.hasAttribute("hidden")) self.querySelector(`slot[name="${id}"]`)?.setAttribute("hidden","");
else {
self.varsProxy[`${id}Pinned`] = true;
self.querySelector(`slot[name="${id}"]`)?.setAttribute("checked","");
}
} else {
self.querySelector(`slot[name="${id}"]`)?.setAttribute("hidden","");
span.style.display = "none";
}
return [id, slot];
}).filter(([id, slot]) => slot!=null);
const showTab = (targetid) => {
tabs.forEach(([id, el, label]) => {
if (id === targetid || self.varsProxy[`${id}Pinned`]) {
el.removeAttribute("hidden");
self.querySelector(`slot[name="${id}"]`)?.removeAttribute("hidden");
} else if (!self.varsProxy[`${id}Pinned`]) {
el.setAttribute("hidden","");
self.querySelector(`slot[name="${id}"]`)?.setAttribute("hidden","");
}
});
};
const hideTab = (targetid) => {
tabs.forEach(([id, el, label]) => {
if (id === targetid) {
el.setAttribute("hidden","");
self.querySelector(`slot[name="${id}"]`)?.setAttribute("hidden","");
}
});
};
const replaceEntities = (el) => {
[...el.childNodes].forEach((node) => {
if(node.nodeType===Node.TEXT_NODE) {
if(["<",">","&"].some((entity) => node.data.includes(entity))) {
node.data = node.data.replaceAll(/</g,"<")
.replaceAll(/>/g,">")
.replaceAll(/&/g,"&");
}
} else if(node.nodeType===Node.ELEMENT_NODE) {
replaceEntities(node);
}
})
};
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") continue;
throw e;
}
}
await fs.writeFile(url.pathname, doPreview(), {encoding: "utf8"}, () => {
});
source = `IndexedDB://${url.hostname + "_repl"}${url.pathname}`;
canReset = true;
};
doReset = async () => {
originalHTML = null;
try {
await fs.unlink(url.pathname);
} catch (e) {
}
if (src) {
try {
parseFullHTML(await loadFromServer());
source = url.href;
createEditor();
} catch (e) {
previewEl.innerHTML = fullHTML = e.message;
}
} else {
try {
initFromTemplate(loadFromTemplate());
source = getPath();
createEditor();
} catch (e) {
previewEl.innerHTML = fullHTML = e.message;
}
}
canReset = false;
};
const content = self.getElementById("content");
let originalHTML;
const doPreview = () => {
head_el.innerHTML = `<base href=\"${document.baseURI}\"></base>` + trimContent(headhtml); //
body_el.innerHTML = trimContent(bodyhtml);
if(!maintainbody) {
style_el.innerText = trimContent(cssText);
script_el.innerHTML = "currentComponent ||= document.body;" + trimContent(scriptText);
body_el.appendChild(style_el);
body_el.appendChild(script_el);
style_el.innerText = style_el.innerText.replaceAll(/<br>/g,"\n");
}
const str = "<html>" + head_el.outerHTML + body_el.outerHTML + "</html>",
blob = new Blob([str], {type : 'text/html'}),
newurl = window.URL.createObjectURL(blob);
if(originalHTML) canReset = originalHTML!=str;
else originalHTML = str;
previewEl.src = newurl;
content.style.minWidth = self.style.minWidth;
content.style.maxWidth = self.style.maxWidth;
//content.style.width = tabs.reduce((min,tab) => Math.max(min,tab[1].clientWidth),0)+"px";
return str;
};
const tabsEl = self.getElementById("tabs"),
headhtmlEl = self.querySelector('[slot="headhtml"]'),
bodyhtmlEl = self.querySelector('[slot="bodyhtml"]'),
markdownEl = self.getElementById("markdown"),
cssEl = self.querySelector('[slot="css"]'),
scriptEl = self.querySelector('[slot="script"]'),
previewEl = self.getElementById("preview");
const highlight = (el,...args) => {
hljs.highlightElement(el,...args);
}
const setCode = (el,code,language) => {
const pre = document.createElement("pre"),
div = document.createElement("div");
pre.style.margin = "5px";
div.className = `language-${language}`;
div.innerText = trimContent(code).replaceAll(/\n/g,"|newline|");
hljs.highlightElement(div);
pre.innerHTML = div.innerHTML.replaceAll(/\|newline\|/g,"\n");
while(el.lastChild) el.lastChild.remove();
replaceEntities(el);
el.append(pre);
}
if(headhtmlEl) {
headhtmlEl.className = "language-html";
if(headhtmlEl.hasAttribute("readonly") || readonly) {
setCode(headhtmlEl,headhtml,"html");
} else {
editable = true;
const bodyJar = CJ(headhtmlEl, highlight);
bodyJar.updateCode(headhtml);
replaceEntities(headhtmlEl);
bodyJar.onUpdate(code => {
replaceEntities(headhtmlEl);
headhtml = code;
doPreview();
})
}
}
const createEditor = () => {
if (bodyhtmlEl) {
bodyhtmlEl.className = "language-html";
if (bodyhtmlEl.hasAttribute("readonly") || readonly) {
setCode(bodyhtmlEl, bodyhtml, "html");
} else {
editable = true;
const bodyJar = CJ(bodyhtmlEl, highlight);
bodyJar.updateCode(bodyhtml);
replaceEntities(bodyhtmlEl);
bodyJar.onUpdate(code => {
replaceEntities(bodyhtmlEl);
bodyhtml = code;
doPreview();
});
}
}
if (cssEl) {
cssEl.className = "language-css";
if (cssEl.hasAttribute("readonly") || readonly) {
setCode(cssEl, cssText, "css");
//cssEl.innerText = cssText;
} else {
editable = true;
const bodyJar = CJ(cssEl, highlight);
bodyJar.updateCode(cssText);
bodyJar.onUpdate(code => {
cssText = code;
doPreview();
});
}
}
if (scriptEl) {
scriptEl.className = "language-javascript";
if (scriptEl.hasAttribute("readonly") || readonly) {
setCode(scriptEl, scriptText, "javascript");
//scriptEl.innerText = scriptText;
} else {
editable = true;
const bodyJar = CJ(scriptEl, highlight);
bodyJar.updateCode(scriptText);
replaceEntities(scriptEl);
bodyJar.onUpdate(code => {
replaceEntities(scriptEl);
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;
}
});*/
self.addEventListener("mounted", () => {
tabsEl.removeAttribute("hidden");
if(contentbackground) content.style.background = contentbackground;
createEditor();
});
}
</script>
</body>
</html>