X-Git-Url: http://git.shiar.nl/sheet.git/blobdiff_plain/20332e82f5c6365a8d65c75192985d846a99a2dd..HEAD:/word/quiz.js diff --git a/word/quiz.js b/word/quiz.js index d32368b..9436f7a 100644 --- a/word/quiz.js +++ b/word/quiz.js @@ -6,62 +6,155 @@ Array.prototype.shuffle = function () { return this; }; -class WordQuiz { - dataselect(json) { - this.data = json; - this.cats = {}; // category lookup - for (let i in json) { - let cat = json[i][3]; - if (this.cats[cat]) { - this.cats[cat].push(i); +function hashparams() { + const encodedhash = window.location.href.split('#').slice(1) || ''; + if (encodedhash.length == 1) { + // location.hash is not encoded in firefox + return decodeURIComponent(encodedhash).split('#'); + } + return encodedhash; +} + +class Words { + constructor(data, root = undefined) { + this.data = data; + this.selection = root || this.data[''][3]; + this.visible = new Set(root || Object.keys(data).flatMap(id => id && parseInt(id))); + if (root) { + let children = root; + for (let loop = 0; children.length && loop < 20; loop++) { + for (let child of children) this.visible.add(child); + children = children.map(cat => data[cat][3]).filter(is => is).flat(); } - else { - this.cats[cat] = [i]; + } + } + + filter(f) { + // keep only matching entries, and root selection regardless + this.visible = new Set([...this.visible].filter(f).concat(this.selection)); + } + + *root() { + for (let i of this.selection) { + if (!this.has(i)) { + continue; + } + yield this.get(i); + } + } + + *random() { + let order = [...this.visible.keys()].shuffle(); + for (let i of order) { + if (!this.has(i)) { + continue; + } + yield this.get(i); + } + } + + has(id) { + return this.visible.has(id); + } + + subs(id) { + let refs = this.data[id][3]; + if (!refs) { + return []; + } + for (let ref of refs) { + // retain orphaned references in grandparent categories + if (!this.has(ref)) { + refs = refs.concat(this.subs(ref)); } } - return this.datafilter(json); + return refs; + } + + get(id) { + if (!this.has(id)) { + return; + } + const p = this; + const row = this.data[id]; + return row && { + id: id, + title: row[0], + get label() { + return row[0].replace(/\/.*/, ''); // primary form + }, + get html() { + let aliases = this.title.split('/'); + let html = aliases.shift(); + html = html.replace(/\((.+)\)/, '$1'); + for (let alias of aliases) { + html += ` (${alias})`; + } + return html; + }, + level: row[1], + imgid: row[2], + thumb(size = 32) { + return `/data/word/${size}/${row[2]}.jpg`; + }, + get subs() { + return p.subs(id).map(e => p.get(e)); + }, + }; + } +} + +class WordQuiz { + dataselect(json) { + this.data = this.datafilter(json); + return [...this.data.random()]; } datafilter(json) { // find viable rows from json data - let rows = Object.values(json); + const selection = new Words(json, this.preset.cat); - if (this.preset.cat !== undefined) { - rows = []; - let children = this.cats[this.preset.cat]; - for (let loop = 0; children.length && loop < 20; loop++) { - rows.push(...children); - children = children.map(cat => this.cats[cat]).filter(is => is).flat(); - } - rows = rows.map(row => json[row]).filter(row => row[2]); + if (this.preset.images) { + selection.filter(id => json[id][2]); } if (this.preset.level !== undefined) { - rows = rows.filter(row => row[1] <= this.preset.level); + selection.filter(id => json[id][1] <= this.preset.level); } - { - let cats = new Set(); - let subcats = rows.map(row => row[3]); // direct parents - for (let loop = 0; subcats.length && loop < 20; loop++) { - subcats.forEach(cat => cats.add(cat)); - subcats = subcats.map(row => json[row] && json[row][3]).filter(val => val); // recurse grandparents - } - rows = rows.filter(row => !cats.has(row[2])); // remove referenced categories + if (this.preset.distinct) { + // remove referenced categories + selection.filter(id => !selection.get(id).subs.length); } - return rows.shuffle(); + + return selection; } - load(dataurl) { - this.preset = {}; - let input; - if (input = window.location.hash.match(/\d+/)) { - this.preset.cat = input[0]; - } - if (window.location.hash.match(/a/)) { - this.preset.level = 3; + configure(params = hashparams()) { + const opts = new Map(params.map(arg => arg.split(/[:=](.*)/))); + for (let [query, val] of opts) { + if (query.match(/^\d+$/)) { + this.preset.cat = [parseInt(query)]; + } + else if (query === 'level') { + this.preset.level = parseInt(val); + } + else if (query === 'debug') { + this.preset.debug = true; + } + else { + this.preset[query] = val; + } } + this.preset.dataurl = `/data/wordlist.${this.preset.lang}.json` + } - fetch(dataurl).then(res => res.json()).then(json => { + setup() { + this.form = document.getElementById('quiz'); + } + + load() { + this.configure(); + fetch(this.preset.dataurl).then(res => res.json()).then(json => { this.words = this.dataselect(json) this.setup(); }); @@ -77,11 +170,15 @@ class WordQuiz { fetch('/word/report', {method: 'POST', body: JSON.stringify(this.history)}); } - constructor(dataurl) { - this.load(dataurl); + constructor() { + this.preset = {images: true, lang: 'en'}; + this.load(); this.history = []; window.onbeforeunload = e => { this.stop('abort'); }; + window.onhashchange = e => { + this.load(); + }; } }