aboutsummaryrefslogtreecommitdiff
path: root/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'main.js')
-rw-r--r--main.js268
1 files changed, 268 insertions, 0 deletions
diff --git a/main.js b/main.js
new file mode 100644
index 0000000..8bc647f
--- /dev/null
+++ b/main.js
@@ -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"));
+
+});