From 0b2b0cd49547d63b91a2b9ce0e79c5d135e46634 Mon Sep 17 00:00:00 2001 From: Richard Zameitat Date: Sat, 24 Aug 2019 14:18:02 +0200 Subject: [PATCH 1/5] first verion of js-powered title list --- layouts/shortcodes/titlesTable.html | 13 ++- static/css/librarium.css | 52 ++++++++++++ static/js/titleList.js | 119 ++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 static/js/titleList.js diff --git a/layouts/shortcodes/titlesTable.html b/layouts/shortcodes/titlesTable.html index 36ea6c7..acb976d 100644 --- a/layouts/shortcodes/titlesTable.html +++ b/layouts/shortcodes/titlesTable.html @@ -1,10 +1,12 @@ +{{/* add script for dynamic interaction */}} + {{/* fetch JSON from file */}} {{ $titles := getJSON "https://liberation.rpg-librarium.de/v1/titles" }} {{/* generate HTML */}} -
+
- + @@ -22,4 +24,11 @@ {{ end }}
TitleTitel System
+
+ Stand: + +
+
diff --git a/static/css/librarium.css b/static/css/librarium.css index f67ae29..ec1b06a 100644 --- a/static/css/librarium.css +++ b/static/css/librarium.css @@ -149,18 +149,69 @@ header.page-header { } /* content elements */ + +.titles.full-list .title { + text-align: left; +} +.titles.full-list .system { + text-align: right; +} +.titles.full-list tbody .system > th { + padding-top: .75em; +} + +.titlesTable .updatedAt { + margin-top: .5em; +} +.titlesTable .updatedAt .updating { + display: inline-block; + color: var(--color-cd-brown-ish); + font-size: .7em; + margin-left: 1em; +} +.titlesTable .updatedAt .updating.error { + color: #ff0077; +} +.loadingTitles .titlesTable .updatedAt .updating::before { + content: 'aktualisiere ...'; +} +.loadingTitles .titlesTable .updatedAt .updating::after { + content: ''; + display: inline-block; + box-sizing: border-box; + margin-left: .5em; + width: 1.2em; + height: 1.2em; + border-radius: .6em; + border: .15em solid; + border-color: var(--color-cd-light-green) transparent transparent transparent; + animation: 2s infinite linear spinningLoadingCircle; +} +@keyframes spinningLoadingCircle { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.titlesTable, .multiversumTable { font-size: .85em; } + +table.titles, .multiversumTable table { border: 0 none; border-collapse: collapse; width: 100%; } + + +table.titles th, +table.titles td, .multiversumTable table th, .multiversumTable table td { padding: .21em .5em; } +table.titles thead th, .multiversumTable table thead th { border-bottom: .2em solid var(--color-cd-dark-ish); } @@ -168,6 +219,7 @@ header.page-header { min-width: 10em; text-align: left; } +table.titles tbody td, .multiversumTable table tbody td { border: solid var(--color-cd-dark-ish); border-width: .1em 0; diff --git a/static/js/titleList.js b/static/js/titleList.js new file mode 100644 index 0000000..3a7e6a4 --- /dev/null +++ b/static/js/titleList.js @@ -0,0 +1,119 @@ +const LIBERATION_BASE = 'https://liberation.rpg-librarium.de/v1'; +const _DEBUG_PRINT = text => thing => (console.debug(text, thing), thing); +const _FORMAT_DATE = date => date.toLocaleString('de-DE', { + hourCycle: 'h23', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', +}); + +const _ROOT = document.documentElement; +_ROOT.classList.add('loadingTitles'); + +// load as early as posible +let titlesPromise = fetch(`${LIBERATION_BASE}/titles`) + .then(response => response.json()) + .then(_DEBUG_PRINT('titles json')) + .then(data => processTitleData(data)) + .then(_DEBUG_PRINT('titles processed')) + +function processTitleData(titlesData) { + let systemsMap = {}; + let titlesMap = {}; + let titles = titlesData.titles.sort((a,b) => a.name.localeCompare(b.name)); + titles.forEach(t => { + titlesMap[t.id] = t; + let sysId = t.system.id; + if (!systemsMap[sysId]) { + systemsMap[sysId] = {...t.system}; + } + let sys = systemsMap[sysId]; + if (!sys.titles) sys.titles = []; + sys.titles.push(t.id); + }); + let systems = Object.values(systemsMap) + .sort((a,b) => a.name.localeCompare(b.name)); + + return { + now: new Date(), + systems, + systemsMap, + titles, + titlesMap, + }; +} + +function doTheMagic(data) { + const _C = (tag, opts={}) => { + let el = document.createElement(tag); + if (opts.classes) opts.classes.forEach(c => el.classList.add(c)); + if (opts.id) el.id = opts.id; + if (opts.children) opts.children.forEach(c => el.appendChild(c)); + if (opts.text) el.innerText = opts.text; + if (opts.attrs) Object.keys(opts.attrs).forEach(k=>el.setAttribute(k, opts.attrs[k])); + return el; + }; + let container = _C('div', {id:'titleListContainer'}); + container.appendChild(renderFullList()); + document.querySelector('table.titles').replaceWith(container); + + function renderFullList() { + let table = _C('table', { + classes:['titles', 'full-list'], + children: [ + _C('thead', {children:[ + _C('tr', {children:[ + _C('th', {classes:['title'], text:'Titel'}), + _C('th', {classes:['system'], text:'System'}), + ]}) + ]}), + ] + }); + let tbody = _C('tbody'); + table.appendChild(tbody); + data.systems.forEach(sys => { + let displayName = sys.shortname ? + `[${sys.shortname}] ${sys.name}` : + sys.name; + tbody.appendChild(_C('tr', { + classes: ['system'], + children:[_C('th', { + text: displayName, + attrs: {colspan: 2}, + })] + })); + sys.titles.forEach(tId => { + let title = data.titlesMap[tId]; + tbody.appendChild(_C('tr', { + classes: ['title'], + children:[_C('td', { + text: title.name, + attrs: {colspan: 2}, + })] + })); + }) + }) + + return table; + } +} + +document.addEventListener('DOMContentLoaded', evt => { + titlesPromise + .then(data => { + doTheMagic(data); + let updateTimeNode = document.querySelector('.titles+.updatedAt time, #titleListContainer+.updatedAt time'); + updateTimeNode.datetime = data.now.toISOString(); + updateTimeNode.innerText = _FORMAT_DATE(data.now); + _ROOT.classList.remove('loadingTitles'); + }) + .catch(e => { + _ROOT.classList.remove('loadingTitles'); + let statusNode = document.querySelector('.titles+.updatedAt .updating'); + statusNode.classList.add('error'); + statusNode.innerText = `Aktualisierung fehlgeschlagen. (${e})`; + }); +}); From 0eea95c61872b5028a7d31845d860216b3766739 Mon Sep 17 00:00:00 2001 From: Richard Zameitat Date: Mon, 16 Sep 2019 23:18:03 +0200 Subject: [PATCH 2/5] render a more "like the static one" list by default --- static/js/titleList.js | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/static/js/titleList.js b/static/js/titleList.js index 3a7e6a4..f2ba3fd 100644 --- a/static/js/titleList.js +++ b/static/js/titleList.js @@ -57,7 +57,7 @@ function doTheMagic(data) { return el; }; let container = _C('div', {id:'titleListContainer'}); - container.appendChild(renderFullList()); + container.appendChild(renderStaticLikeList()); document.querySelector('table.titles').replaceWith(container); function renderFullList() { @@ -99,6 +99,39 @@ function doTheMagic(data) { return table; } + + function renderStaticLikeList() { + let table = _C('table', { + classes:['titles', 'static-like-list'], + children: [ + _C('thead', {children:[ + _C('tr', {children:[ + _C('th', {classes:['title'], text:'Titel'}), + _C('th', {classes:['system'], text:'System'}), + ]}) + ]}), + ] + }); + let tbody = _C('tbody'); + table.appendChild(tbody); + data.systems.forEach(sys => { + sys.titles.forEach(tId => { + let title = data.titlesMap[tId]; + tbody.appendChild(_C('tr', {children:[ + _C('td', { + classes: ['title'], + text: title.name, + }), + _C('td', { + classes: ['system'], + text: sys.name, + }), + ]})); + }); + }); + + return table; + } } document.addEventListener('DOMContentLoaded', evt => { From 03a6c0dfa149f041e1b25bc8dce16c08758f284b Mon Sep 17 00:00:00 2001 From: Richard Zameitat Date: Thu, 3 Oct 2019 17:01:53 +0200 Subject: [PATCH 3/5] wip: style switcher --- static/css/librarium.css | 29 +++++++++++++++ static/js/titleList.js | 76 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/static/css/librarium.css b/static/css/librarium.css index ec1b06a..882f659 100644 --- a/static/css/librarium.css +++ b/static/css/librarium.css @@ -150,6 +150,35 @@ header.page-header { /* content elements */ +.titlesTable .styleSelect { + display: flex; + justify-content: flex-start; + align-items: baseline; + margin-bottom: .5em; +} +.titlesTable .styleSelect > * { + margin: .2em 1em .2em 0; +} +.titlesTable .styleSelect input[type=radio] { + display: none; +} +.titlesTable .styleSelect label[for] { + display: inline-block; + cursor: pointer; + user-select: none; + padding: .2em .5em; + border: .1em solid var(--color-cd-dark-ish); + color: var(--color-cd-light-green); +} +.titlesTable .styleSelect label[for]:hover { + border-color: var(--color-cd-dark-green); +} +.titlesTable .styleSelect input[type=radio]:checked + label[for] { + background: var(--color-cd-dark-green); + border-color: var(--color-cd-dark-green); + color: var(--color-cd-light-ish); +} + .titles.full-list .title { text-align: left; } diff --git a/static/js/titleList.js b/static/js/titleList.js index f2ba3fd..f2d2d2b 100644 --- a/static/js/titleList.js +++ b/static/js/titleList.js @@ -47,6 +47,19 @@ function processTitleData(titlesData) { } function doTheMagic(data) { + const _LIST_STYLES = [ + { + id: 'static', + renderFn: renderStaticLikeList, + name: 'Einfach Alles', + description: 'eine einfache Liste mit allen Titeln', + },{ + id: 'grouped', + renderFn: renderFullList, + name: 'Gruppiert', + description: 'eine etwas weniger einfache Liste, gruppiert nach Systemen', + }, + ]; const _C = (tag, opts={}) => { let el = document.createElement(tag); if (opts.classes) opts.classes.forEach(c => el.classList.add(c)); @@ -56,9 +69,70 @@ function doTheMagic(data) { if (opts.attrs) Object.keys(opts.attrs).forEach(k=>el.setAttribute(k, opts.attrs[k])); return el; }; + const titlesRoot = document.querySelector('.titlesTable'); let container = _C('div', {id:'titleListContainer'}); container.appendChild(renderStaticLikeList()); - document.querySelector('table.titles').replaceWith(container); + titlesRoot.querySelector('table.titles').replaceWith(container); + titlesRoot.prepend(renderStyleSelector()); + + function renderStyleSelector() { + let element = _C('div', { + classes:['styleSelect'], + children: [ + _C('span', {text: 'Ansicht:'}), + ... _LIST_STYLES.flatMap(style => { + let inputId = `_listStyleSelect_${style.id}`; + let input = _C('input', { + id: inputId, + attrs: { + type: 'radio', + name: '_listStyleSelect', + value: style.id, + }, + }); + let label = _C('label', { + attrs: { + for: inputId, + title: style.description, + tabindex: 0, + role: 'button', + }, + text: style.name, + }); + let changeStyle = newStyle => { + console.log('new style: ', newStyle); + } + let labelListener = evt => { + let target = evt.target; + if (evt.type === 'click' + || (evt.type === 'keydown' + && [' ', 'Enter'].includes(evt.key))) { + evt.preventDefault(); + input.checked = true; + changeStyle(style.id); + } + }; + label.addEventListener('click', labelListener); + label.addEventListener('keydown', labelListener); + input.addEventListener('change', evt => { + if (input.checked) { + changeStyle(style.id); + } + }); + return [input, label]; + }) + ], + }); + let styleChangeListener = evt => { + let target = evt.target; + console.log(evt); + if (target.matches('input[type=radio]:checked')) { + let newStyle = target.value; + console.log('new Style:', newStyle); + } + }; + return element; + } function renderFullList() { let table = _C('table', { From 359ab032e700426b8ba61af070675fe729fa3ff6 Mon Sep 17 00:00:00 2001 From: Richard Zameitat Date: Fri, 4 Oct 2019 21:26:48 +0200 Subject: [PATCH 4/5] apply style selection --- static/css/librarium.css | 5 +++- static/js/titleList.js | 58 ++++++++++++++++++++++++++++------------ 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/static/css/librarium.css b/static/css/librarium.css index 882f659..29b84cb 100644 --- a/static/css/librarium.css +++ b/static/css/librarium.css @@ -170,13 +170,16 @@ header.page-header { border: .1em solid var(--color-cd-dark-ish); color: var(--color-cd-light-green); } -.titlesTable .styleSelect label[for]:hover { +.titlesTable .styleSelect label[for]:hover, +.titlesTable .styleSelect label[for]:focus { border-color: var(--color-cd-dark-green); + color: var(--color-cd-dark-green); } .titlesTable .styleSelect input[type=radio]:checked + label[for] { background: var(--color-cd-dark-green); border-color: var(--color-cd-dark-green); color: var(--color-cd-light-ish); + cursor: default; } .titles.full-list .title { diff --git a/static/js/titleList.js b/static/js/titleList.js index f2d2d2b..48780b0 100644 --- a/static/js/titleList.js +++ b/static/js/titleList.js @@ -1,3 +1,5 @@ +'use strict'; + const LIBERATION_BASE = 'https://liberation.rpg-librarium.de/v1'; const _DEBUG_PRINT = text => thing => (console.debug(text, thing), thing); const _FORMAT_DATE = date => date.toLocaleString('de-DE', { @@ -70,10 +72,43 @@ function doTheMagic(data) { return el; }; const titlesRoot = document.querySelector('.titlesTable'); + let activeStyle = _LIST_STYLES[0].id; // default style + let activeStyleNode = null; + const styleSelector = renderStyleSelector(); let container = _C('div', {id:'titleListContainer'}); - container.appendChild(renderStaticLikeList()); + changeStyle(activeStyle); titlesRoot.querySelector('table.titles').replaceWith(container); - titlesRoot.prepend(renderStyleSelector()); + titlesRoot.prepend(styleSelector); + + function changeStyle(newStyleId) { + // retrieve style info + let filtered = _LIST_STYLES.filter(s => s.id === newStyleId); + if (filtered.length !== 1) { + console.debug('could not find list style ', newStyleId); + changeStyle(activeStyle); + return false; + } + let newStyle = filtered[0]; + let previousStyle = activeStyle; + activeStyle = newStyle; + + // update selector + styleSelector + .querySelector(`input[type=radio][value=${newStyle.id}]`) + .checked = true; + + // apply style, if not already active + let prevNode = container.querySelector('.titles'); + if (previousStyle.id === newStyle.id && prevNode) { + return true; + } + let newNode = newStyle.renderFn(); + if (prevNode) { + prevNode.replaceWith(newNode); + } else { + container.appendChild(newNode); + } + } function renderStyleSelector() { let element = _C('div', { @@ -99,9 +134,6 @@ function doTheMagic(data) { }, text: style.name, }); - let changeStyle = newStyle => { - console.log('new style: ', newStyle); - } let labelListener = evt => { let target = evt.target; if (evt.type === 'click' @@ -123,14 +155,6 @@ function doTheMagic(data) { }) ], }); - let styleChangeListener = evt => { - let target = evt.target; - console.log(evt); - if (target.matches('input[type=radio]:checked')) { - let newStyle = target.value; - console.log('new Style:', newStyle); - } - }; return element; } @@ -188,9 +212,10 @@ function doTheMagic(data) { }); let tbody = _C('tbody'); table.appendChild(tbody); - data.systems.forEach(sys => { - sys.titles.forEach(tId => { - let title = data.titlesMap[tId]; + data.titles + .sort((a,b) => a.name.localeCompare(b.name)) + .forEach(title => { + let sys = title.system; tbody.appendChild(_C('tr', {children:[ _C('td', { classes: ['title'], @@ -202,7 +227,6 @@ function doTheMagic(data) { }), ]})); }); - }); return table; } From ec7a6239e6873b1dc89f234c15effa8ab15193c8 Mon Sep 17 00:00:00 2001 From: Richard Z Date: Sun, 6 Mar 2022 21:44:43 +0100 Subject: [PATCH 5/5] [#14] update script to work with the new API --- static/css/librarium.css | 4 +++- static/js/titleList.js | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/static/css/librarium.css b/static/css/librarium.css index 49cd2cb..8b6d4fc 100644 --- a/static/css/librarium.css +++ b/static/css/librarium.css @@ -189,11 +189,13 @@ header.page-header { text-align: right; } .titles.full-list tbody .system > th { - padding-top: .75em; + padding-top: 1.42em; + text-align: center; } .titlesTable .updatedAt { margin-top: .5em; + font-size: 0.9em; } .titlesTable .updatedAt .updating { display: inline-block; diff --git a/static/js/titleList.js b/static/js/titleList.js index 48780b0..62310a0 100644 --- a/static/js/titleList.js +++ b/static/js/titleList.js @@ -16,7 +16,7 @@ const _ROOT = document.documentElement; _ROOT.classList.add('loadingTitles'); // load as early as posible -let titlesPromise = fetch(`${LIBERATION_BASE}/titles`) +let titlesPromise = fetch(`${LIBERATION_BASE}/titles?recursive=true`) .then(response => response.json()) .then(_DEBUG_PRINT('titles json')) .then(data => processTitleData(data)) @@ -25,12 +25,12 @@ let titlesPromise = fetch(`${LIBERATION_BASE}/titles`) function processTitleData(titlesData) { let systemsMap = {}; let titlesMap = {}; - let titles = titlesData.titles.sort((a,b) => a.name.localeCompare(b.name)); + let titles = titlesData.sort((a,b) => a.name.localeCompare(b.name)); titles.forEach(t => { titlesMap[t.id] = t; - let sysId = t.system.id; + let sysId = t.rpg_system.id; if (!systemsMap[sysId]) { - systemsMap[sysId] = {...t.system}; + systemsMap[sysId] = {...t.rpg_system}; } let sys = systemsMap[sysId]; if (!sys.titles) sys.titles = []; @@ -215,7 +215,7 @@ function doTheMagic(data) { data.titles .sort((a,b) => a.name.localeCompare(b.name)) .forEach(title => { - let sys = title.system; + let sys = title.rpg_system; tbody.appendChild(_C('tr', {children:[ _C('td', { classes: ['title'],