tag-browser: use separate <code-block> module for style preview
This commit is contained in:
400
html_resources/code-block/code-block.js
Normal file
400
html_resources/code-block/code-block.js
Normal file
@@ -0,0 +1,400 @@
|
||||
'use strict';
|
||||
|
||||
class CodeBlock extends HTMLElement{
|
||||
constructor(){
|
||||
super();
|
||||
let template = document.getElementById("code-block-template");
|
||||
let templateContent = template ? template.content : CodeBlock.Fragment();
|
||||
let cloned = templateContent.cloneNode(true);
|
||||
const shadowRoot = this.attachShadow({mode: 'open'})
|
||||
.appendChild(cloned);
|
||||
this.highlighter = {
|
||||
ready: false,
|
||||
waiting: false,
|
||||
fn: null,
|
||||
failed: false,
|
||||
type: null,
|
||||
empty: true,
|
||||
linkGenerator: null,
|
||||
linkMatcher: null
|
||||
};
|
||||
}
|
||||
|
||||
determineAndLoadContent(){
|
||||
CodeBlock.getSource(this.src)
|
||||
.then(
|
||||
(data) => this.consumeData(data,CodeBlock.InsertMode.Replace),
|
||||
(e) => this.consumeData({content:this.textContent},CodeBlock.InsertMode.Replace)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
get name(){
|
||||
return this.dataset.name;
|
||||
}
|
||||
set name(some){
|
||||
this.dataset.name = some;
|
||||
this.shadowRoot.querySelector("caption").textContent = some;
|
||||
}
|
||||
|
||||
connectedCallback(){
|
||||
if(!this.isConnected || this.initialized){
|
||||
return
|
||||
}
|
||||
if(this.dataset.matchlinks){
|
||||
let parts = this.dataset.matchlinks.split(" -> ");
|
||||
// this is kinda sketchy
|
||||
if(parts.length === 2){
|
||||
try{
|
||||
this.highlighter.linkMatcher = new RegExp(parts[0],"g");
|
||||
this.highlighter.linkGenerator = (a) => (parts[1].replace("%s",a));
|
||||
}catch(e){
|
||||
console.warn(e);
|
||||
this.highlighter.linkMatcher = null;
|
||||
this.highlighter.linkGenerator = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(this.dataset.name){
|
||||
this.name = this.dataset.name
|
||||
}
|
||||
this.initialized = true;
|
||||
|
||||
if(this.copyable){
|
||||
CodeBlock.addClipboardListenerTo(this);
|
||||
}
|
||||
|
||||
if(this.highlighter.empty && this.dataset.highlight){
|
||||
CodeBlock.addHighlighterTo(this);
|
||||
return
|
||||
}
|
||||
|
||||
this.determineAndLoadContent();
|
||||
}
|
||||
|
||||
get copyable(){
|
||||
return this.classList.contains("copy-able")
|
||||
}
|
||||
|
||||
static addHighlighterTo(elem){
|
||||
if(elem instanceof CodeBlock){
|
||||
elem.highlighter.empty = false;
|
||||
switch(elem.dataset.highlight){
|
||||
case "css":
|
||||
case "simple":
|
||||
elem.highlighter.type = elem.dataset.highlight;
|
||||
elem.highlighter.waiting = true;
|
||||
break;
|
||||
default:
|
||||
console.warn("invalid highlighter");
|
||||
elem.determineAndLoadContent();
|
||||
return
|
||||
}
|
||||
import("./code-block-highlighter.js")
|
||||
.then(it => {
|
||||
switch(elem.highlighter.type){
|
||||
case "css":
|
||||
elem.highlighter.fn = new it.CSSHighlighter();
|
||||
break;
|
||||
case "simple":
|
||||
elem.highlighter.fn = new it.SimpleHighlighter();
|
||||
}
|
||||
elem.highlighter.ready = true;
|
||||
elem.highlighter.waiting = false;
|
||||
elem.determineAndLoadContent()
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
elem.highlighter.failed = true;
|
||||
ele.highlighter.waiting = false;
|
||||
elem.determineAndLoadContent()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
clearContent(){
|
||||
let innerbox = this.codeBox;
|
||||
while(innerbox.children.length){
|
||||
innerbox.children[0].remove();
|
||||
}
|
||||
}
|
||||
|
||||
static addClipboardListenerTo(aBlock){
|
||||
let copyButton = aBlock.copyButton;
|
||||
if(copyButton){
|
||||
return
|
||||
}
|
||||
copyButton = aBlock.shadowRoot.querySelector(".copy-button");
|
||||
aBlock.copyButton = copyButton;
|
||||
|
||||
copyButton.addEventListener("click",(e) => {
|
||||
e.preventDefault();
|
||||
try{
|
||||
let writing = navigator.clipboard.writeText(aBlock.value);
|
||||
writing.then(()=>{
|
||||
copyButton.classList.add("copy-success");
|
||||
setTimeout(()=>copyButton.classList.remove("copy-success"),2000);
|
||||
});
|
||||
|
||||
}catch(e){
|
||||
console.error("couldn't copy content to clipboard");
|
||||
}
|
||||
|
||||
});
|
||||
aBlock.copyButton.removeAttribute("hidden");
|
||||
}
|
||||
static getSource(some){
|
||||
return new Promise((res, rej) => {
|
||||
if(some && typeof some === "string"){
|
||||
CodeBlock.TryLoadFile(some)
|
||||
.then(res)
|
||||
.catch((e)=>{
|
||||
console.error(e);
|
||||
rej(e)
|
||||
})
|
||||
}else{
|
||||
setTimeout(()=>rej("argument must be a string"));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async setSource(some){
|
||||
this.clearContent();
|
||||
let res = await CodeBlock.getSource(some);
|
||||
if(res.ok){
|
||||
this.consumeData(res,CodeBlock.InsertMode.Replace);
|
||||
}
|
||||
return { ok: res.ok }
|
||||
}
|
||||
|
||||
get src(){
|
||||
return this.getAttribute("src")
|
||||
}
|
||||
set src(some){
|
||||
this.setSource(some);
|
||||
}
|
||||
|
||||
lines(){
|
||||
const lines = this.codeBox.querySelectorAll("tr");
|
||||
const lineCount = lines.length;
|
||||
let currentLine = 0;
|
||||
return {
|
||||
next: function() {
|
||||
return currentLine < lineCount ? {
|
||||
value: lines[currentLine++],
|
||||
done: false
|
||||
} : { done: true }
|
||||
},
|
||||
[Symbol.iterator]: function() { return this; }
|
||||
}
|
||||
}
|
||||
|
||||
getNamedSection(name){
|
||||
let i = 0;
|
||||
let sections = this.codeBox.children;
|
||||
while(i < sections.length){
|
||||
if(sections[i].dataset.name === name){
|
||||
return sections[i]
|
||||
}
|
||||
i++
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async consumeData(some,insertMode){
|
||||
const re = /.*\r?\n/g;
|
||||
if(typeof some.content !== "string"){
|
||||
some.content = some.content.toString();
|
||||
}
|
||||
this.textContent = "";
|
||||
|
||||
let innerbox = this.codeBox;
|
||||
|
||||
if(innerbox.children.length === 1 && innerbox.firstChild.textContent === ""){
|
||||
insertMode = CodeBlock.InsertMode.Replace;
|
||||
}
|
||||
const INSERT_MODE = insertMode || some.insertMode || CodeBlock.InsertMode.Append;
|
||||
|
||||
const aDiv = document.createElement("div");
|
||||
if(some.name){
|
||||
aDiv.setAttribute("data-name",some.name);
|
||||
}
|
||||
|
||||
const hasHighlighter = this.highlighter.ready;
|
||||
const LIMIT = 10000; // Arbitrary limit of 10k lines
|
||||
|
||||
if(hasHighlighter){
|
||||
this.highlighter.fn.reset();
|
||||
const payload = {
|
||||
"match" : re.exec(some.content),
|
||||
"linkMatcher": this.highlighter.linkMatcher,
|
||||
"linkGenerator": this.highlighter.linkGenerator,
|
||||
"linkChanged": true,
|
||||
};
|
||||
Object.defineProperty(payload,"content",{get:()=>payload.match[0]});
|
||||
let counter = 0;
|
||||
let lastIdx = 0;
|
||||
|
||||
while(payload.match && (counter++ < LIMIT)){
|
||||
aDiv.appendChild(CodeBlock.RowFragment.cloneNode(true));
|
||||
this.highlighter.fn.parse(
|
||||
payload,
|
||||
aDiv.lastElementChild.lastChild
|
||||
);
|
||||
payload.linkChanged = false;
|
||||
lastIdx = (payload.match.index + payload.match[0].length);
|
||||
payload.match = re.exec(some.content);
|
||||
}
|
||||
// Handle case where the content does not end with newline
|
||||
aDiv.appendChild(CodeBlock.RowFragment.cloneNode(true));
|
||||
if(lastIdx < some.content.length){
|
||||
payload.match = [some.content.slice(lastIdx)];
|
||||
this.highlighter.fn.parse(
|
||||
payload,
|
||||
aDiv.lastElementChild.lastChild
|
||||
);
|
||||
}
|
||||
}else{
|
||||
let match = re.exec(some.content);
|
||||
let counter = 0;
|
||||
let lastIdx = 0;
|
||||
|
||||
while(match && (counter++ < LIMIT)){
|
||||
aDiv.appendChild(CodeBlock.RowFragment.cloneNode(true));
|
||||
aDiv.lastElementChild.lastChild.textContent = match[0];
|
||||
lastIdx = (match.index + match[0].length);
|
||||
match = re.exec(some.content);
|
||||
}
|
||||
// Handle case where the content does not end with newline
|
||||
aDiv.appendChild(CodeBlock.RowFragment.cloneNode(true));
|
||||
if(lastIdx < some.content.length){
|
||||
aDiv.lastElementChild.lastChild.textContent = some.content.slice(lastIdx);
|
||||
}
|
||||
}
|
||||
|
||||
switch(INSERT_MODE){
|
||||
case CodeBlock.InsertMode.Prepend:
|
||||
aDiv.lastElementChild.lastElementChild.append("\n")
|
||||
innerbox.insertBefore(aDiv,innerbox.firstChild);
|
||||
break;
|
||||
case CodeBlock.InsertMode.Replace:
|
||||
this.clearContent();
|
||||
case CodeBlock.InsertMode.Append:
|
||||
// Push the first "line" of new section to the last line of old content, if old content exists
|
||||
if(innerbox.lastElementChild){
|
||||
let first = aDiv.firstChild.lastElementChild;
|
||||
let lastRowContent = this.lastContentLine;
|
||||
for(let one of Array.from(first.childNodes)){
|
||||
lastRowContent.appendChild(one)
|
||||
}
|
||||
aDiv.firstChild.remove();
|
||||
}
|
||||
if(aDiv.children.length){
|
||||
innerbox.appendChild(aDiv);
|
||||
}
|
||||
break;
|
||||
case CodeBlock.InsertMode.AppendLines:
|
||||
if(aDiv.children.length){
|
||||
let lastRowContent = this.lastContentLine;
|
||||
if(lastRowContent){
|
||||
lastRowContent.append("\n");
|
||||
}
|
||||
innerbox.appendChild(aDiv);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.warn("unimplemented insertMode")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
get lastContentLine(){
|
||||
return this.codeBox.lastElementChild?.lastChild.lastChild;
|
||||
}
|
||||
|
||||
get codeBox(){
|
||||
return this.shadowRoot.querySelector("tbody");
|
||||
}
|
||||
get value(){
|
||||
return this.codeBox.textContent
|
||||
}
|
||||
|
||||
get InsertModes(){
|
||||
return CodeBlock.InsertMode
|
||||
}
|
||||
|
||||
set value(thing){
|
||||
if(typeof thing === "string"){
|
||||
this.consumeData({content:thing,insertMode:CodeBlock.InsertMode.Replace});
|
||||
}else if("content" in thing){
|
||||
this.consumeData(thing,CodeBlock.InsertMode.Replace);
|
||||
}else{
|
||||
this.consumeData({content: thing.toString(), insertMode: CodeBlock.InsertMode.Replace});
|
||||
}
|
||||
}
|
||||
|
||||
insertContent(thing){
|
||||
if(typeof thing === "string"){
|
||||
this.consumeData({content:thing});
|
||||
}else if("content" in thing){
|
||||
this.consumeData(thing);
|
||||
}else{
|
||||
this.consumeData({content: thing.toString()});
|
||||
}
|
||||
}
|
||||
|
||||
static InsertMode = {
|
||||
Replace : Symbol("replace"),
|
||||
Append : Symbol("append"),
|
||||
AppendLines : Symbol("appendlines"),
|
||||
Prepend : Symbol("prepend")
|
||||
}
|
||||
|
||||
static async TryLoadFile(name){
|
||||
let response = await fetch(name);
|
||||
if(response.ok){
|
||||
let content = await response.text();
|
||||
return { content: content, ok: true }
|
||||
}else{
|
||||
throw {error: "no response", ok: false }
|
||||
}
|
||||
}
|
||||
|
||||
static RowFragment = (() => {
|
||||
let frag = new DocumentFragment();
|
||||
let tr = frag.appendChild(document.createElement("tr"));
|
||||
tr.appendChild(document.createElement("td"));
|
||||
tr.firstChild.setAttribute("class","line-number");
|
||||
tr.appendChild(document.createElement("td"));
|
||||
return frag
|
||||
})();
|
||||
|
||||
static Fragment(){
|
||||
let frag = new DocumentFragment();
|
||||
let link = document.createElement("link");
|
||||
link.setAttribute("as","style");
|
||||
link.setAttribute("type","text/css");
|
||||
link.setAttribute("rel","preload prefetch stylesheet");
|
||||
// Change the relative stylesheet address here if required
|
||||
link.setAttribute("href","html_resources/code-block/code-block.css");
|
||||
frag.appendChild(link);
|
||||
let outerBox = frag.appendChild(document.createElement("div"));
|
||||
outerBox.setAttribute("part","outerBox");
|
||||
outerBox.className = "outerBox";
|
||||
let copyButton = outerBox.appendChild(document.createElement("div"));
|
||||
copyButton.setAttribute("part","copyButton");
|
||||
copyButton.className = "copy-button";
|
||||
copyButton.setAttribute("hidden",true);
|
||||
copyButton.setAttribute("role","button");
|
||||
let table = document.createElement("table");
|
||||
let caption = table.appendChild(document.createElement("caption"));
|
||||
caption.setAttribute("part","title");
|
||||
let content = table.appendChild(document.createElement("tbody"));
|
||||
content.setAttribute("part","content");
|
||||
outerBox.appendChild(table)
|
||||
|
||||
return frag
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("code-block",CodeBlock);
|
||||
Reference in New Issue
Block a user