Adam Goldsmith
8bc7e43732
Should definitely have been smaller commits, but oh well, too lazy to split them up now important note: switched editor from XMLHttpRequest to fetch
254 lines
8.3 KiB
JavaScript
254 lines
8.3 KiB
JavaScript
//jshint browser:true
|
|
//jshint esversion:6
|
|
//jshint latedef:nofunc
|
|
|
|
let deckJSON, template;
|
|
let selected;
|
|
let deckName = window.location.pathname.split('/')[2];
|
|
|
|
document.title = "Editor|" + deckName;
|
|
|
|
window.addEventListener("load", () => {
|
|
// load deck input json
|
|
fetch("deck.input.json")
|
|
.then(data => data.json())
|
|
.then(json => {
|
|
deckJSON = json;
|
|
makeSVGs(deckJSON);
|
|
})
|
|
.catch(error => console.error(error));
|
|
|
|
// deck JSON uploader
|
|
document.querySelector('#jsonUpload').addEventListener('change', event => {
|
|
let files = event.target.files;
|
|
let reader = new FileReader();
|
|
reader.onload = event => {
|
|
deckJSON = JSON.parse(event.target.result);
|
|
makeSVGs(deckJSON);
|
|
};
|
|
reader.readAsText(files[0]);
|
|
});
|
|
|
|
// Upload on save button
|
|
document.querySelector('#saveButton').addEventListener('click', upload);
|
|
|
|
// download input JSON
|
|
document.querySelector('#jsonInputDownload').addEventListener(
|
|
'click',
|
|
() => downloadFile('data:application/json;charset=utf-8,' +
|
|
encodeURIComponent(JSON.stringify(deckJSON)),
|
|
deckName + '.input.json'));
|
|
|
|
// download input JSON
|
|
document.querySelector('#outputDownload').addEventListener(
|
|
'click',
|
|
() => downloadFile('deck.json',
|
|
deckName + '.json'));
|
|
|
|
|
|
// handle changes to deck editor
|
|
document.querySelector('#deckForm').addEventListener('input', event => {
|
|
let prop = event.target.id.substring(4).toLowerCase();
|
|
deckJSON[prop] = event.target.value;
|
|
if (prop === 'type') {
|
|
makeSVGs(deckJSON);
|
|
}
|
|
});
|
|
});
|
|
|
|
// handle changes to card editor
|
|
document.querySelector('#cardForm').addEventListener('input', event => {
|
|
let prop = event.target.id.substring(5);
|
|
if (prop !== "count") {
|
|
wrapSVGText(selected.svg.querySelector('#' + prop),
|
|
String(event.target.value));
|
|
}
|
|
if (event.target.value) {
|
|
selected.json[prop] = event.target.value;
|
|
}
|
|
else {
|
|
delete selected.json[prop];
|
|
}
|
|
});
|
|
|
|
// chrome doesn't seem to send input event on file select
|
|
document.querySelector('#cardForm').addEventListener('change', event => {
|
|
let prop = event.target.id.substring(5);
|
|
if (prop === "image") {
|
|
let files = event.target.files;
|
|
let reader = new FileReader();
|
|
reader.onload = e => {
|
|
selected.svg.querySelector('#' + prop)
|
|
.setAttributeNS("http://www.w3.org/1999/xlink", "href", e.target.result);
|
|
selected.json[prop] = e.target.result;
|
|
};
|
|
reader.readAsDataURL(files[0]);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('beforeunload',
|
|
e => e.returnValue = "Unsaved changes blah blah");
|
|
});
|
|
|
|
function downloadFile(file, name) {
|
|
let dl = document.createElement('a');
|
|
dl.setAttribute('href', file);
|
|
dl.setAttribute('download', name);
|
|
document.body.appendChild(dl);
|
|
dl.click();
|
|
document.body.removeChild(dl);
|
|
}
|
|
|
|
function getSVGTemplate(name, callback) {
|
|
return fetch("/template/" + name + ".svg")
|
|
.then(response => response.text())
|
|
.then(str => (new window.DOMParser()).parseFromString(str, "text/xml").activeElement);
|
|
}
|
|
|
|
async function makeSVGs(deckJSON) {
|
|
document.querySelector('#deckName').value = deckJSON.name || "";
|
|
document.querySelector('#deckType').value = deckJSON.type || "";
|
|
|
|
let deck = document.querySelector('#deck');
|
|
deck.innerHTML = "";
|
|
|
|
let template = await fetch(`/template/${deckJSON.type}/input.json`)
|
|
.then(data => data.json());
|
|
|
|
let cardCount = Object.entries(template.cardTypes)
|
|
.map(ct => deckJSON[ct[0]].length * (ct[1].back ? 2 : 1))
|
|
.reduce((sum, current) => sum + current, 0);
|
|
|
|
// note: needs to be a for loop because it needs to be synchronous
|
|
// and also have await
|
|
// Although I suppose I could prefetch the SVGs and then do the rest...
|
|
for (let cardType of Object.entries(template.cardTypes)) {
|
|
let backSVG;
|
|
if (cardType[1].back) {
|
|
let backTemplate = cardType[1].back.template || (cardType[0] + "-back");
|
|
backSVG = await getSVGTemplate(deckJSON.type + "/" + backTemplate);
|
|
}
|
|
let templateSVG = await getSVGTemplate(deckJSON.type + "/" + cardType[0]);
|
|
console.log(templateSVG);
|
|
|
|
// build card SVGs
|
|
deckJSON[cardType[0]].forEach(card => {
|
|
makeCardSVG(deck, cardType[1], templateSVG, card);
|
|
|
|
// if there is a back, build it too
|
|
if (cardType[1].back) {
|
|
makeCardSVG(deck, cardType[1].back, backSVG, card, back=true);
|
|
}
|
|
});
|
|
|
|
// set div width/height based on number of cards
|
|
deck.style.width = Math.ceil(Math.sqrt(cardCount)) *
|
|
parseInt(templateSVG.getAttribute("width")) + "pt";
|
|
deck.style.height = Math.ceil(Math.sqrt(cardCount)) *
|
|
parseInt(templateSVG.getAttribute("height")) + "pt";
|
|
};
|
|
}
|
|
|
|
function setForm(cardTemplate, card) {
|
|
let form = document.querySelector('#cardForm');
|
|
form.innerHTML = "";
|
|
|
|
Object.entries(cardTemplate.inputs).forEach(prop => {
|
|
let div = form.appendChild(document.createElement('div'));
|
|
let label = div.appendChild(document.createElement('label'));
|
|
label.textContent = prop[0];
|
|
|
|
let input = label.appendChild(
|
|
document.createElement(prop[1] === 'textarea' ? 'textarea' : 'input'));
|
|
input.id = "card-" + prop[0];
|
|
|
|
if (prop[1] === "image") {
|
|
input.type = "file";
|
|
}
|
|
else {
|
|
input.type = prop[1];
|
|
input.value = card[prop[0]] || "";
|
|
}
|
|
});
|
|
}
|
|
|
|
function makeCardSVG(deck, cardInputTemplate, templateSVG, card, back=false) {
|
|
let propSource = (back && card.back) ? card.back : card;
|
|
let cardSVG = deck.appendChild(templateSVG.cloneNode(true));
|
|
cardSVG.addEventListener('click', () => {
|
|
selected = {svg: cardSVG, json: card};
|
|
setForm(cardInputTemplate, card);
|
|
}, true);
|
|
Object.keys(cardInputTemplate.inputs).forEach(prop => {
|
|
let inputProp = propSource[prop] || card[prop] || "";
|
|
wrapSVGText(cardSVG.querySelector('#' + prop), String(inputProp));
|
|
});
|
|
Object.entries(cardInputTemplate.hide || []).forEach(hidable => {
|
|
cardSVG.querySelector('#' + hidable[0])
|
|
.setAttribute('display', hidable[1] in propSource ? '' : 'none');
|
|
});
|
|
}
|
|
|
|
function upload() {
|
|
let deck = document.querySelector('#deck');
|
|
|
|
// POST the generated SVGs to the server
|
|
let data = (new XMLSerializer()).serializeToString(deck);
|
|
fetch('upload', {
|
|
method: 'post',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({body: data, json: deckJSON})
|
|
});
|
|
}
|
|
|
|
function wrapSVGText(e, string) {
|
|
// TODO: bold or italic text
|
|
e.innerHTML = ""; // clear element
|
|
let lines = string.split("\n");
|
|
if (e.getAttribute('default-font-size'))
|
|
e.setAttribute('font-size', e.getAttribute('default-font-size'));
|
|
e.setAttribute('default-font-size', e.getAttribute('font-size'));
|
|
while (lines.length > 0) {
|
|
let words = lines.shift().split(" ");
|
|
let tspan = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
|
|
tspan.setAttribute('x', e.getAttribute('x'));
|
|
if (e.innerHTML !== "") tspan.setAttribute('dy', e.getAttribute('font-size'));
|
|
e.appendChild(tspan);
|
|
let line = [];
|
|
while(words.length > 0) {
|
|
let word = words.shift();
|
|
if (word === "") word = " ";
|
|
line.push(word);
|
|
tspan.innerHTML = line.join(" ");
|
|
// horizontal overflow
|
|
// TODO: actually use units (also applies to vertical)
|
|
if (parseFloat(e.getAttribute("width")) &&
|
|
tspan.getComputedTextLength() > parseFloat(e.getAttribute("width"))) {
|
|
// if we have height, we can line wrap
|
|
if (parseFloat(e.getAttribute("height")) &&
|
|
e.children.length * parseFloat(e.getAttribute('font-size')) <
|
|
parseFloat(e.getAttribute('height'))) {
|
|
words.unshift(line.pop());
|
|
tspan.innerHTML = line.join(" ");
|
|
line = [];
|
|
|
|
tspan = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
|
|
tspan.setAttribute('x', e.getAttribute('x'));
|
|
tspan.setAttribute('dy', e.getAttribute('font-size'));
|
|
e.appendChild(tspan);
|
|
}
|
|
// vertical overflow or horizontal overflow with no height variable
|
|
// TODO: better with recursion instead?
|
|
else {
|
|
e.innerHTML = ""; // remove all tspans
|
|
// TODO: maybe binary search font size later if I really care
|
|
e.setAttribute('font-size', parseFloat(e.getAttribute('font-size')) * 0.9);
|
|
words = [];
|
|
lines = string.split('\n');
|
|
console.log("resetting, size= " + e.getAttribute('font-size'));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|