diff --git a/layouts/shortcodes/titlesTable.html b/layouts/shortcodes/titlesTable.html
index ebc1532..981c368 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?recursive=true" }}
{{/* generate HTML */}}
-
+
- | Title |
+ Titel |
System |
@@ -22,4 +24,11 @@
{{ end }}
+
+ Stand:
+
+
+
diff --git a/static/css/librarium.css b/static/css/librarium.css
index 376fa65..8b6d4fc 100644
--- a/static/css/librarium.css
+++ b/static/css/librarium.css
@@ -149,18 +149,103 @@ 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,
+.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 {
+ text-align: left;
+}
+.titles.full-list .system {
+ text-align: right;
+}
+.titles.full-list tbody .system > th {
+ padding-top: 1.42em;
+ text-align: center;
+}
+
+.titlesTable .updatedAt {
+ margin-top: .5em;
+ font-size: 0.9em;
+}
+.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 +253,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..62310a0
--- /dev/null
+++ b/static/js/titleList.js
@@ -0,0 +1,250 @@
+'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', {
+ 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?recursive=true`)
+ .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.sort((a,b) => a.name.localeCompare(b.name));
+ titles.forEach(t => {
+ titlesMap[t.id] = t;
+ let sysId = t.rpg_system.id;
+ if (!systemsMap[sysId]) {
+ systemsMap[sysId] = {...t.rpg_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 _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));
+ 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;
+ };
+ 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'});
+ changeStyle(activeStyle);
+ titlesRoot.querySelector('table.titles').replaceWith(container);
+ 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', {
+ 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 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];
+ })
+ ],
+ });
+ return element;
+ }
+
+ 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;
+ }
+
+ 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.titles
+ .sort((a,b) => a.name.localeCompare(b.name))
+ .forEach(title => {
+ let sys = title.rpg_system;
+ 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 => {
+ 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})`;
+ });
+});