document.addEventListener("DOMContentLoaded", () => { let data = { // Default current: "", pages: [ {body: "It was a dark and stormy night.\n\nAll was quiet.", choices: [[1,"Continue"]], paths: []}, {body: "Or was it...?", choices: [], paths: ["1"]} ], paths: {"":0, "1":1} }; let state = { editing: false, saveinterval: 0, addcount: 0, path: "" }; const db = new Dexie("vanguard-editor"); db.version(1).stores({files:"name"}); db.on("populate", () => db.files.add({name: "main", data})); if (!/^#[0-9a-z.-]+\.json$/i.test(location.hash)) { db.files.get("main").then(d => { data = d.data; if (!location.hash || /^#\/?$/.test(location.hash)) location.hash = "#/"+data.current; state.path = renderPath(); setTimeout(() => document.documentElement.scrollTop = document.getElementById("main").lastChild.offsetTop); }).catch(e => console.error); } else loadFile(location.hash.slice(1)); function download(a, text, name, type) { const file = new Blob([text], {type: type}); a.href = URL.createObjectURL(file); a.download = name; } const prettyDate = d => d.getFullYear() + (d.getMonth()+1).toString().padStart(2, "0") + d.getDate().toString().padStart(2, "0") + "-" + d.getHours().toString().padStart(2, "0") + d.getMinutes().toString().padStart(2, "0") + d.getSeconds().toString().padStart(2, "0"); document.getElementById("save").addEventListener("mouseover", e => { download(e.target, JSON.stringify(data), `Vanguard-${prettyDate(new Date())}.json`, "application/json"); }); document.getElementById("open-file").addEventListener("change", e => { const r = new FileReader(); r.onload = () => { try { data = JSON.parse(r.result); db.files.put({name:"main",data}); location.hash = "#/"+data.current; state.path = renderPath(); setTimeout(() => document.documentElement.scrollTop = document.getElementById("main").lastChild.offsetTop); } catch(e) { alert(e); }}; r.readAsText(e.target.files[0]); }); function loadFile(path) { fetch("./stories/"+path).then(r => r.json()).then(d => { d.readOnly = true; db.files.put({name:"main",data:(data=d)}); //location.hash = "#/"+data.current; location.hash = "#/"; state.path = renderPath(); //setTimeout(() => document.documentElement.scrollTop = // document.getElementById("main").lastChild.offsetTop); }).catch(e => { console.error(e); alert("Error: Couldn't load file"); location.hash = "#/"; }); } const viewtpl = document.getElementById("view-entry").content; const edittpl = document.getElementById("edit-entry").content; const choicetpl = document.getElementById("edit-choice").content; function renderView(path, choice) { const pageid = data.paths[path]; const page = data.pages[pageid]; const e = document.createElement("section"); e.appendChild(viewtpl.cloneNode(true)); e.querySelector("a").href = path? `#/${path}/edit` : "#/edit"; e.querySelector("h2").innerHTML = path? (page.paths.length? `#${pageid} / ` + page.paths.map(p => `${p}`) .join(" / ") : "UNREACHABLE") : "Start"; e.innerHTML += markup(page.body); if (!page.choices.length) e.innerHTML += "

"; else if (choice) e.innerHTML += `${markup(page.choices[choice-1][1])}`; else e.innerHTML += page.choices.map((c,i) => `${markup(c[1])}`).join(""); return e; } function renderEdit(path) { const pageid = data.paths[path]; const page = data.pages[pageid]; const e = document.createElement("section"); e.appendChild(edittpl.cloneNode(true)); e.classList.add("editing"); e.querySelector("a").href = `#/${path}`; e.querySelector("h2").innerHTML = path? (page.paths.length? `#${pageid} / ` + page.paths.map(p => `${p}`) .join(" / ") : "UNREACHABLE") : "Start"; e.querySelector("textarea").value = page.body; page.choices.forEach(c => { const l = document.createElement("div"); l.appendChild(choicetpl.cloneNode(true)); l.classList.add("choice"); l.querySelector("textarea").value = `#${c[0]}: ${c[1]}`; l.querySelector(".choice-delete").addEventListener("click", () => l.parentNode.removeChild(l)); e.querySelector(".choices").appendChild(l); }); e.querySelector(".add-choice").addEventListener("click", () => { const l = document.createElement("div"); l.appendChild(choicetpl.cloneNode(true)); l.classList.add("choice"); const t = l.querySelector("textarea"); t.value = `#${data.pages.length + (state.addcount++)}: Choice text...`; window.addEventListener("resize", () => resizeTA(t)); t.addEventListener("input", () => resizeTA(t)); l.querySelector(".choice-delete").addEventListener("click", () => l.parentNode.removeChild(l)); e.querySelector(".choices").appendChild(l); resizeTA(t); t.focus(); }); return e; }; function resizeTA(e) { const c = e.parentNode; c.style.height = c.scrollHeight + "px"; e.style.height = ""; e.style.height = e.scrollHeight + "px"; c.style.height = ""; } function renderPath() { let path = location.hash.slice(1); const main = document.getElementById("main"); const o = main.cloneNode(false); document.documentElement.classList[data.readOnly?"add":"remove"]("read-only"); state.addcount = 0; if (!path.startsWith("/")) { if (/^[0-9]+$/.test(path) && data.pages[+path] && data.pages[+path].paths[0]) path = data.pages[+path].paths[0]; else if (/^[0-9a-z.-]+\.json$/i.test(path)) { loadFile(path); return; } else path = ""; } else path = path.slice(1); state.editing = path.endsWith("edit"); if (state.editing) { path = path.slice(0,-5); if (data.readOnly) { state.editing = false; location.hash = "#/"+path; }} if (!/^[0-9]+(-[0-9]+)*$/.test(path)) path = ""; const pieces = path.split("-"); if (pieces[0]) for (let i=0; i { e.addEventListener("input", () => resizeTA(e)); window.addEventListener("resize", () => resizeTA(e)); resizeTA(e); }); document.getElementById("body").focus(); state.saveinterval = setInterval(() => { const path = state.path; const pageid = data.paths[path]; const page = data.pages[pageid]; page.body = document.getElementById("body").value; db.files.put({name:"main",data}); }, 15000); } const pageArr = new Array(data.pages.length).fill(0); Object.values(data.paths).forEach(p => pageArr[p] = 1); const unreach = pageArr.reduce((a,e,i) => { if (!e) a.push(i); return a; }, []) if (unreach.length) { //console.error("Unreachable", unreach); document.getElementById("unreachable").innerHTML = "Unreachable: " + unreach.map(u => "#"+u).join(", "); } else document.getElementById("unreachable").innerHTML = ""; return path; } window.addEventListener("hashchange", () => { if (state.error) { delete state.error; return; } clearInterval(state.saveinterval); try { if (state.editing) { const path = state.path; const pageid = data.paths[path]; const page = data.pages[pageid]; page.body = document.getElementById("body").value; let newid = 0; const newidmap = {}; page.choices = Array.from(document.querySelectorAll(".choice")).reduce((a,c,i) => { const v = c.querySelector("textarea").value; if (!/^#[0-9]+: /.test(v)) throw new Error("Invalid choice destination"); a.push([+v.slice(1, v.indexOf(":")), v.slice(v.indexOf(" ")+1)]); return a; }, []).map(c => { const id = c[0]; if (id < data.pages.length) return c; if (newidmap[id]) return [newidmap[id], c[1]]; return [newidmap[id] = data.pages.length + newid++, c[1]]; }); const newpaths = {}; data.pages.forEach(p => p.paths = []); page.choices.forEach((c,i) => { const id = c[0]; if (id == data.pages.length) { data.pages.push({body: "Page content...", choices: [], paths: []}); } else if (id > data.pages.length) throw new Error("ID Overflow"); }); function walkTree(id, path) { const page = data.pages[id]; page.paths.forEach(p => { if (path.startsWith(p)) throw new Error("Link loop detected"); }); page.paths.push(path); page.choices.forEach((c,i) => walkTree(c[0], path+(path?"-":"")+(i+1))); newpaths[path] = id; } walkTree(0, ""); data.paths = newpaths; db.files.put({name:"main",data}); } const oldscroll = document.documentElement.scrollTop; const oldoff = document.getElementById("main").lastChild.offsetTop; const oldid = data.paths[state.path]; const newpath = renderPath(); const newid = data.paths[newpath]; if (newid == oldid) { const newoff = document.getElementById("main").lastChild.offsetTop; document.documentElement.scrollTop = newoff - (oldoff - oldscroll) } state.path = newpath; } catch(e) { state.error = 1; console.error(e); alert(e); location.hash = "#/"+state.path+(state.path?"/":"")+"edit"; } }, true); const invertCookie = encodeURIComponent("vanguard-invert"); if (new RegExp("(?:^|;\\s*)"+invertCookie.replace(/[\-\.\+\*]/g,"\\$&")+"\\s*\\=").test(document.cookie)) document.documentElement.classList.add("invert"); document.getElementById("theme").addEventListener("click", () => document.cookie = invertCookie + (document.documentElement.classList.toggle("invert")? "=1; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "=; expires=Thu, 01 Jan 1970 00:00:00 GMT")); document.getElementById("enable-edit").addEventListener("click", () => { delete data.readOnly; state.path = renderPath(); }); document.getElementById("disable-edit").addEventListener("click", () => { data.readOnly = true; state.path = renderPath(); }); });