diff options
author | Adam Hovorka <[email protected]> | 2020-04-23 11:05:39 -0600 |
---|---|---|
committer | Adam Hovorka <[email protected]> | 2020-04-23 11:05:39 -0600 |
commit | 1b01a8f62ccc476d8058f396dc69dafe1408d162 (patch) | |
tree | c3a2b646c2be4a6cdd01e1d62090d845413bd128 /main.js |
Initial commit
Diffstat (limited to 'main.js')
-rw-r--r-- | main.js | 268 |
1 files changed, 268 insertions, 0 deletions
@@ -0,0 +1,268 @@ +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})); +db.files.get("main").then(d => { data = d.data; + location.hash = "#/"+data.current; + state.path = renderPath(); + setTimeout(() => document.documentElement.scrollTop = + document.getElementById("main").lastChild.offsetTop); +}).catch(e => console.error); + +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]); +}); + +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 => `<a href="#/${p}"${p==path?' class="active"':""}>${p}</a>`).join(" / ") : "UNREACHABLE") : "Start"; + e.innerHTML += markup(page.body); + if (!page.choices.length) e.innerHTML += "<h3 id='the-end'></h3>"; + else if (choice) e.innerHTML += `<a class="choice-made" href="#/${path}">${markup(page.choices[choice-1][1])}</a>`; + else e.innerHTML += page.choices.map((c,i) => + `<a class="choice" href="#/${path}${path?"-":""}${i+1}">${markup(c[1])}</a>`).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 => `<a href="#/${p}"${p==path?' class="active"':""}>${p}</a>`).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(2); + const main = document.getElementById("main"); + const o = main.cloneNode(false); + + state.addcount = 0; + state.editing = path.endsWith("edit"); + if (state.editing) path = path.slice(0,-5); + if (!/[0-9]+(-[0-9]+)*/.test(path)) path = ""; + + const pieces = path.split("-"); + if (pieces[0]) for (let i=0; i<pieces.length; i++) { + if (!data.paths.hasOwnProperty(pieces.slice(0,i+1).join("-"))) { + return location.hash = "#/"+pieces.slice(0,i).join("-"); } + o.appendChild(renderView(pieces.slice(0,i).join("-"), +pieces[i])) + } + + if (state.editing) o.appendChild(renderEdit(path)); + else o.appendChild(renderView(path)); + + main.parentNode.replaceChild(o, main); + data.current = path; + db.files.put({name:"main",data}); + + if (state.editing) { + [].forEach.call(document.querySelectorAll("textarea"), e => { + 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")); + +}); |