From 80ba98478bfe5f24ddb6a613b85ae73b833fa4da Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 18 Aug 2026 18:21:56 +0200 Subject: [PATCH] refactor(frontend): encapsulate JS state, add wikipedia TOC tree visualization, collapsible nodes, language select, and favicon animation --- index.html | 102 +++++++++--- index.js | 470 ++++++++++++++++++++++++++++++++++++++++------------- style.css | 339 ++++++++++++++++++++++++++++++++++---- 3 files changed, 741 insertions(+), 170 deletions(-) diff --git a/index.html b/index.html index 96e33c5..48ee37d 100644 --- a/index.html +++ b/index.html @@ -1,36 +1,92 @@ - + - + + Phikipathia - + -
+
-

Phikipathia

-

Philosophy Wikipedia Path (ia) visualisatie

-
-
- - - - - - - - -
-
-
+
+

Phikipathia

+

Philosophy Wikipedia Path (ia) visualisatie

+
+ + +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+ + + + +
+ + +
+
+ + + +
+ diff --git a/index.js b/index.js index 0f1e06c..9351740 100644 --- a/index.js +++ b/index.js @@ -1,138 +1,386 @@ - -// TODO Optimalisatie: De huidige lijst meegeven en controleren tijdens het opstellen zodat er gestopt wordt vanaf er een titel gevonden is - -// Initialiseer de webpagina -const taal = document.getElementById('taal'); -const start = document.getElementById('start'); -const einde = document.getElementById('einde'); -const toevoegKnop = document.getElementById('toevoegKnop'); -const visualisatie = document.getElementById('visualisatie'); -const logo = document.getElementById('logo'); -const waarschuwing = document.getElementById('waarschuwing'); - -let huidigeTaal; -let huidigeDoel; - -let timer; -let rotatie; - -refreshTree(); - /** - * Zoek een pad met python backend en voeg deze toe in de lijst. + * Phikipathia - Philosophy Wikipedia Path Visualization + * Features: Clickable Wikipedia links, Collapsible Tree Nodes, Clean Branch Lines, Wikipedia Infobox, Tab Favicon Spinner */ -function zoek() { +(() => { + 'use strict'; - // Schakel de knop 'toevoegen' uit zolang het zoekproces bezig is, zodat er geen meerdere - // paden tegelijk toegevoegd kunnen worden. - toggleZoeken(false); + // 1. Cached DOM Element References + const favicon = document.getElementById('favicon'); + const taal = document.getElementById('taal'); + const start = document.getElementById('start'); + const einde = document.getElementById('einde'); + const toevoegKnop = document.getElementById('toevoegKnop'); + const wisKnop = document.getElementById('wisKnop'); + const visualisatie = document.getElementById('visualisatie'); + const logo = document.getElementById('logo'); + const waarschuwing = document.getElementById('waarschuwing'); + const errorBanner = document.getElementById('errorBanner'); + const errorMessage = document.getElementById('errorMessage'); + const errorDismiss = document.getElementById('errorDismiss'); - refreshTree(); + // Wikipedia Infobox Stats Elements + const statistieken = document.getElementById('statistieken'); + const statHops = document.getElementById('statHops'); + const statStatusText = document.getElementById('statStatusText'); + const statReroutesRow = document.getElementById('statReroutesRow'); + const statReroutes = document.getElementById('statReroutes'); + const statTotalPaths = document.getElementById('statTotalPaths'); - const verzoek = { // Standaardwaarden gebruiken indien het veld leeg is. - taal: getOrDefault(taal), - start: getOrDefault(start), - einde: getOrDefault(einde) - }; + // 2. Favicon Animation Engine (Canvas Offscreen Rendering) + const faviconSrc = './images/wikipedia_edit.png'; + const faviconImg = new Image(); + faviconImg.src = faviconSrc; - fetch(`cgi-bin/init.py?data=${JSON.stringify(verzoek)}`) - .then(antwoord => antwoord.json()) - .then(data => voegToe(data)) - .catch(reason => alert('‼ ' + reason)) - .finally(() => toggleZoeken()); // Herstel de knop. Ook als er ondertussen iets mis ging. + const faviconCanvas = document.createElement('canvas'); + faviconCanvas.width = 32; + faviconCanvas.height = 32; + const faviconCtx = faviconCanvas.getContext('2d'); -} + let faviconAngle = 0; + let faviconTimer = null; -function voegToe(data) { - if (data.hasOwnProperty('error')) { - alert('↯ ' + data.error); - } else { - let pad = data.pad; + function renderFaviconFrame() { + if (!favicon || !faviconImg.complete || !faviconCtx) return; + faviconCtx.clearRect(0, 0, 32, 32); + faviconCtx.save(); + faviconCtx.translate(16, 16); + faviconCtx.rotate((faviconAngle * Math.PI) / 180); + faviconCtx.drawImage(faviconImg, -16, -16, 32, 32); + faviconCtx.restore(); + favicon.href = faviconCanvas.toDataURL('image/png'); + faviconAngle = (faviconAngle + 12) % 360; + } + + function startFaviconSpin() { + if (faviconTimer) return; + faviconAngle = 0; + faviconTimer = setInterval(renderFaviconFrame, 40); + } + + function stopFaviconSpin() { + if (faviconTimer) { + clearInterval(faviconTimer); + faviconTimer = null; + } + if (favicon) { + favicon.href = faviconSrc; + } + } + + // 3. Encapsulated Application State + let huidigeTaal = null; + let huidigeDoel = null; + let isSearching = false; + let totalPathsExplored = 0; + + // 4. Helper: Extract input value or placeholder + function getOrDefault(inputElem) { + if (!inputElem) return ''; + const val = inputElem.value.trim(); + return val === '' ? (inputElem.getAttribute('placeholder') || '').trim() : val; + } + + // Helper: Construct Wikipedia Article URL + function getWikiUrl(title, lang) { + const langCode = lang || getOrDefault(taal) || 'en'; + const slug = encodeURIComponent(title.replace(/ /g, '_')); + return `https://${langCode}.wikipedia.org/wiki/${slug}`; + } + + // 5. UI: Error & Stats Infobox Management + function showError(message, prefix = '↯ ') { + if (!errorBanner || !errorMessage) return; + errorMessage.textContent = `${prefix}${message}`; + errorBanner.hidden = false; + } + + function hideError() { + if (!errorBanner) return; + errorBanner.hidden = true; + if (errorMessage) { + errorMessage.textContent = ''; + } + } + + function updateStats(data) { + if (!statistieken) return; + statistieken.hidden = false; + + const hops = data.stappen !== undefined ? data.stappen : (Array.isArray(data.pad) ? Math.max(0, data.pad.length - 1) : 0); + if (statHops) statHops.textContent = hops; + + if (statStatusText) { + if (data.status === 'success' || (!data.status && data.pad && !data.error)) { + statStatusText.textContent = `${huidigeDoel} bereikt`; + statStatusText.style.color = '#196f3d'; + statStatusText.style.fontWeight = 'bold'; + } else if (data.status === 'cyclus' || (data.error && data.error.includes('Cyclus'))) { + statStatusText.textContent = 'Cyclus gedetecteerd'; + statStatusText.style.color = '#7d6608'; + statStatusText.style.fontWeight = 'bold'; + } else { + statStatusText.textContent = 'Fout / Doodlopend'; + statStatusText.style.color = '#78281f'; + statStatusText.style.fontWeight = 'bold'; + } + } + + if (statReroutesRow && statReroutes) { + if (data.herleid && data.herleid > 0) { + statReroutes.textContent = data.herleid; + statReroutesRow.hidden = false; + } else { + statReroutesRow.hidden = true; + } + } + + if (statTotalPaths) { + statTotalPaths.textContent = totalPathsExplored; + } + } + + // 6. UI: Dynamic Warning Visibility + function inputWaarschuwing() { + if (!waarschuwing) return; + const currentLangVal = getOrDefault(taal); + const currentTargetVal = getOrDefault(einde); + waarschuwing.hidden = (huidigeTaal === currentLangVal && huidigeDoel === currentTargetVal); + } + + // 7. UI: Loading State & Animation Toggles + function setLoadingState(isLoading) { + isSearching = isLoading; + if (toevoegKnop) toevoegKnop.disabled = isLoading; + if (logo) { + if (isLoading) logo.classList.add('spinning'); + else logo.classList.remove('spinning'); + } + if (isLoading) { + startFaviconSpin(); + hideError(); + } else { + stopFaviconSpin(); + } + } + + // 8. DOM: Tree Node Creation + function insertTreeNode(title, parentElem, elementId, badgeText = null) { + if (!parentElem) return null; + + const existingLists = parentElem.querySelectorAll(':scope > ul'); + const isBranchSplit = existingLists.length >= 1; + + if (isBranchSplit && parentElem.tagName === 'LI') { + parentElem.classList.add('has-split-branch'); + // Mark primary branch (Branch 1) so a clean vertical connector line is drawn + if (existingLists[0] && !existingLists[0].classList.contains('branch-primary')) { + existingLists[0].classList.add('branch-primary'); + } + } + + const wrap = document.createElement('ul'); + if (isBranchSplit) { + wrap.classList.add('branch-split'); + } + + const newItem = document.createElement('li'); + newItem.setAttribute('item', title); + + // Clickable Wikipedia Article Link + const wikiLink = document.createElement('a'); + wikiLink.className = 'wiki-link'; + wikiLink.href = getWikiUrl(title, huidigeTaal); + wikiLink.target = '_blank'; + wikiLink.rel = 'noopener noreferrer'; + wikiLink.textContent = title; + newItem.appendChild(wikiLink); + + if (badgeText) { + const badge = document.createElement('span'); + badge.className = 'loop-marker'; + badge.textContent = badgeText; + newItem.appendChild(badge); + } + + wrap.appendChild(newItem); + if (elementId !== undefined) { + wrap.id = elementId; + } + + parentElem.appendChild(wrap); + + // Add Collapsible toggle button to parent element + if (parentElem.tagName === 'LI' && !parentElem.querySelector(':scope > .toggle-btn')) { + const toggleBtn = document.createElement('span'); + toggleBtn.className = 'toggle-btn'; + toggleBtn.textContent = '˅'; + toggleBtn.title = 'Klap in / uit'; + toggleBtn.addEventListener('click', (e) => { + e.stopPropagation(); + parentElem.classList.toggle('collapsed'); + }); + parentElem.insertBefore(toggleBtn, parentElem.firstChild); + } + + return newItem; + } + + // 9. Tree Reset & Clear + function refreshTree() { + const langVal = getOrDefault(taal); + const targetVal = getOrDefault(einde); + + if (huidigeDoel !== targetVal || huidigeTaal !== langVal) { + huidigeTaal = langVal; + huidigeDoel = targetVal; + totalPathsExplored = 0; + + if (visualisatie) { + visualisatie.replaceChildren(); + insertTreeNode(huidigeDoel, visualisatie, 'wortel'); + } + + if (statistieken) statistieken.hidden = true; + inputWaarschuwing(); + } + } + + function clearTree() { + if (visualisatie) { + visualisatie.replaceChildren(); + insertTreeNode(getOrDefault(einde), visualisatie, 'wortel'); + } + totalPathsExplored = 0; + if (statistieken) statistieken.hidden = true; + hideError(); + inputWaarschuwing(); + } + + // 10. Path Rendering + function renderPath(data) { + if (!data) return; + + totalPathsExplored += 1; + + if (data.error && (!data.pad || data.pad.length === 0)) { + showError(data.error, '↯ '); + return; + } + + const padList = Array.isArray(data.pad) ? [...data.pad] : []; + if (padList.length === 0) { + showError(data.error || 'Geen geldig pad ontvangen van de server.', '↯ '); + return; + } + + let parentNode = visualisatie; + const existingNodes = visualisatie.getElementsByTagName('li'); + const existingNames = Array.from(existingNodes).map(item => item.getAttribute('item')); - // Locatie van invoegen bepalen. - let moeder = visualisatie; - let kinderen = visualisatie.getElementsByTagName('li'); - let kindernamen = Array.from(kinderen).map(item => item.getAttribute('item')); let i = 0; - while (moeder === visualisatie && i < pad.length) { - if (kindernamen.indexOf(pad[i]) !== -1) { - moeder = kinderen[kindernamen.indexOf(pad[i])]; + while (parentNode === visualisatie && i < padList.length) { + const foundIdx = existingNames.indexOf(padList[i]); + if (foundIdx !== -1) { + parentNode = existingNodes[foundIdx]; } i += 1; } - // Alleen nog die elementen toevoegen die nog niet op het pad staan. - pad = pad.slice(0, i - 1); + if (parentNode === visualisatie) { + const rootLi = visualisatie.querySelector('#wortel li'); + if (rootLi) { + parentNode = rootLi; + } + } - // Nieuw element creëren (zonder toe te voegen aan het DOM). - for (let item of pad.reverse()) { - moeder = maakIn(item, moeder); + const remainingItems = padList.slice(0, i - 1); + const reversedItems = remainingItems.reverse(); + for (let idx = 0; idx < reversedItems.length; idx++) { + const item = reversedItems[idx]; + let badge = null; + if (idx === 0 && data.status === 'cyclus') { + badge = 'cyclus'; + } + parentNode = insertTreeNode(item, parentNode, undefined, badge); + } + + updateStats(data); + + if (data.error) { + showError(data.error, 'ℹ '); } } -} -/** - * Maakt een element aan binnen een ander element, en geeft het nieuwste (diepste) element terug. - */ -function maakIn(titel, moeder, id) { - let wrap = document.createElement('ul'); - let nieuw = document.createElement('li'); - nieuw.innerHTML = titel; - nieuw.setAttribute('item', titel); - wrap.appendChild(nieuw); + // 11. API Search Execution + function zoek() { + if (isSearching) return; - if (id !== undefined) { - wrap.id = id; + setLoadingState(true); + refreshTree(); + + const verzoek = { + taal: getOrDefault(taal), + start: getOrDefault(start), + einde: getOrDefault(einde) + }; + + const encodedData = encodeURIComponent(JSON.stringify(verzoek)); + const endpointUrl = `cgi-bin/init.py?data=${encodedData}`; + + fetch(endpointUrl) + .then(response => { + if (!response.ok) { + return response.json() + .then(errJson => { + throw new Error(errJson.error || `Server HTTP ${response.status}`); + }) + .catch(parseErr => { + if (parseErr.message && !parseErr.message.includes('JSON')) throw parseErr; + throw new Error(`Server HTTP ${response.status}`); + }); + } + return response.json(); + }) + .then(data => { + renderPath(data); + }) + .catch(err => { + showError(err.message || 'Er is een netwerkfout opgetreden.', '‼ '); + }) + .finally(() => { + setLoadingState(false); + }); } - moeder.appendChild(wrap); - return nieuw; -} + // 12. Event Binding & Initialization + function init() { + if (toevoegKnop) toevoegKnop.addEventListener('click', zoek); + if (wisKnop) wisKnop.addEventListener('click', clearTree); + if (taal) { + taal.addEventListener('input', inputWaarschuwing); + taal.addEventListener('change', inputWaarschuwing); + } + if (einde) einde.addEventListener('input', inputWaarschuwing); + if (errorDismiss) errorDismiss.addEventListener('click', hideError); -/** - * Probeer of de huidige boom verwijderd moet worden en voeg de bestemming reeds toe aan de nieuwe boom. - */ -function refreshTree() { - if (huidigeDoel !== getOrDefault(einde) || huidigeTaal !== getOrDefault(taal)) { - huidigeTaal = getOrDefault(taal); - huidigeDoel = getOrDefault(einde); + [taal, start, einde].forEach(input => { + if (input) { + input.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + event.preventDefault(); + zoek(); + } + }); + } + }); - visualisatie.innerHTML = ''; - maakIn(huidigeDoel, visualisatie, 'wortel'); - - inputWaarschuwing(); + refreshTree(); } -} -/** - * Toon of verberg een speciale waarschuwing die zegt of de huidige boom verwijderd zal worden - * bij een nieuwe toevoeging. - */ -function inputWaarschuwing() { - waarschuwing.hidden = (huidigeTaal === getOrDefault(taal) && huidigeDoel === getOrDefault(einde)); -} - -function getOrDefault(element) { - return element.value === '' ? element.getAttribute('placeholder') : element.value; -} - -/** - * Toggle tussen de zoek-status en de wacht-op-input-status. Dit heeft invloed op zowel de indienkop als op het - * roterende logo. Standaardaanroep zal alles stoppen, om überveel animaties te vermijden bij fout gebruik. - */ -function toggleZoeken(stop = true) { - toevoegKnop.disabled = !stop; - - rotatie = 0; - if (stop) { - clearInterval(timer); - logo.style.transform = 'rotate(0)'; + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); } else { - timer = setInterval(draai, 24); + init(); } -} - -function draai() { - rotatie += 5; - logo.style.transform = `rotate(${rotatie % 360}deg`; -} +})(); diff --git a/style.css b/style.css index 02be329..bd84e7a 100644 --- a/style.css +++ b/style.css @@ -1,17 +1,16 @@ -/* -Waarschijnlijk eerder de enigste file die niet deftig gedocumenteerd is. -Ach, de kans dat jullie dit bekijken is hoe dan ook klein. - */ - /* Algemeen */ body { - font-family: 'Linux Libertine','Georgia','Times',serif; + font-family: 'Linux Libertine', 'Georgia', 'Times', serif; padding: 0 1.5em 1.5em 1.5em; line-height: 1.6; + margin: 0; + color: #202122; + background-color: #ffffff; } h1 { + font-family: 'Linux Libertine', 'Georgia', 'Times', serif; margin-bottom: 0.25em; padding: 0; line-height: 1.3; @@ -20,97 +19,365 @@ h1 { border-bottom: 1px solid #a2a9b1; } -li ul { - margin: 0.2vh 1ch 0 1ch; - padding: 0.2vh 1vw; -} - /* Header */ +.header { + display: flex; + align-items: center; + gap: 0.8em; + padding: 0.5em 0; +} + .header img { - height: 1.8em; - margin-top: 0.3em; - float: left; + height: 2.2em; + width: auto; + flex-shrink: 0; +} + +.header-content { + flex: 1; +} + +.header h1 { + font-family: 'Linux Libertine', 'Georgia', 'Times', serif; + margin: 0; + padding: 0; + line-height: 1.2; + font-size: 1.8em; + font-weight: normal; + border-bottom: none; } .header p { + font-family: 'Linux Libertine', 'Georgia', 'Times', serif; + margin: 0.2em 0 0 0; font-size: 92%; - display: block; color: #202122; - font-family: sans-serif; } -/* Box */ +/* Loading Animation */ + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.spinning { + animation: spin 1.2s linear infinite; + will-change: transform; +} + +/* Controls & Box Layout */ .box { font-family: sans-serif; font-size: 95%; - - padding: 7px; + padding: 10px 14px; border: 1px solid #a2a9b1; background-color: #f8f9fa; color: #202122; - - position: sticky; - top: 7px; } -.box p { - padding: 0; - margin: 0; +main > .box:first-child { + position: sticky; + top: 7px; + z-index: 10; +} + +.form-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 12px; +} + +.form-group { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.form-group input[type="text"], +.form-group select { + padding: 3px 6px; + border: 1px solid #a2a9b1; + border-radius: 2px; + font-size: 0.95em; + font-family: sans-serif; + background-color: #ffffff; +} + +#taal { + min-width: 9em; +} + +#start, #einde { + min-width: 140px; + flex: 1 1 auto; +} + +/* Full-Width Statistics Box at Bottom */ + +.stats-box { + margin-top: 1.2em; +} + +.stats-box[hidden] { + display: none !important; +} + +.stats-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px 24px; + font-size: 92%; +} + +.stat-item { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.stat-label { + color: #54595d; + font-weight: bold; +} + +.stat-val { + color: #202122; +} + +.stats-row button#wisKnop { + margin-left: auto; +} + +/* Warning & Error Banners */ + +#waarschuwing { + display: block; + margin-top: 8px; + font-size: 90%; + color: #54595d; +} + +#waarschuwing[hidden] { + display: none !important; } #waarschuwing b { color: #ba0000; } -/* Visualisatie */ +.error-banner { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 8px; + padding: 8px 12px; + background-color: #fdf2f2; + border: 1px solid #d33; + border-left: 4px solid #ba0000; + color: #721c24; + border-radius: 2px; + font-size: 90%; + animation: fadeIn 0.2s ease-in-out; +} + +.error-banner[hidden] { + display: none !important; +} + +.error-message { + flex: 1; + word-break: break-word; +} + +.error-dismiss { + background: none; + border: none; + color: #ba0000; + font-size: 1.3em; + line-height: 1; + cursor: pointer; + padding: 0 4px; + margin-left: 8px; +} + +.error-dismiss:hover { + color: #000; + text-decoration: none; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-3px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Wikipedia Table of Contents (TOC) Style Visualisatie */ #visualisatie { - margin: 0.5em 0; - font-size: 1em; + margin: 1em 0; + font-family: sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; + font-size: 0.92em; + line-height: 1.7; + overflow-x: auto; + max-width: 100%; + padding-bottom: 0.5em; } #visualisatie ul { - list-style-type: symbols("⬑"); /* alternatieven "↪" "⬉" */ + list-style-type: none; + padding-left: 1.2em; + margin: 0.15em 0; } - ul#wortel { - padding: 0; + padding-left: 0; list-style-type: none; + font-weight: bold; +} + +ul#wortel > li > a.wiki-link { + font-weight: bold; + color: #202122; + font-size: 1.05em; +} + +#visualisatie li { + padding: 0.1em 0; + position: relative; + white-space: nowrap; +} + +/* Clickable Wikipedia Article Links - Wikipedia Blue */ +#visualisatie a.wiki-link { + color: #3366bb; + text-decoration: none; + white-space: nowrap; + transition: color 0.1s, text-decoration 0.1s; +} + +#visualisatie a.wiki-link:hover { + color: #0645ad; + text-decoration: underline; +} + +#visualisatie a.wiki-link:visited { + color: #0b0080; +} + +/* Pixel-Perfect Tree Connector Lines: + - Primary branch (Branch 1) draws a clean vertical line starting at parent and ending at split point. + - Secondary branch (Branch 2) draws a horizontal tick starting at x=0 that seamlessly meets the vertical line. */ + +#visualisatie ul.branch-primary { + border-left: 2px solid #3366bb; + padding-left: 1.2em; + margin-left: 0; +} + +#visualisatie ul.branch-split { + margin-top: 6px; + padding-left: 1.2em; + position: relative; +} + +#visualisatie ul.branch-split::before { + content: ""; + position: absolute; + top: 0.85em; + left: 0; + width: 0.85em; + height: 0; + border-top: 2px solid #3366bb; +} + +/* Collapsible Node Toggle Chevron (Wikipedia Style) */ +.toggle-btn { + display: inline-block; + cursor: pointer; + user-select: none; + font-size: 70%; + margin-right: 5px; + color: #72777d; + transition: transform 0.15s ease; + width: 0.9em; + text-align: center; + vertical-align: middle; +} + +.toggle-btn:hover { + color: #202122; +} + +li.collapsed > ul { + display: none !important; +} + +li.collapsed > .toggle-btn { + transform: rotate(-90deg); +} + +/* Loop node marker in tree */ + +.loop-marker { + display: inline-block; + margin-left: 6px; + padding: 1px 6px; + font-size: 80%; + font-family: sans-serif; + border-radius: 2px; + background-color: #fcf3cf; + color: #7d6608; + border: 1px solid #f9e79f; + font-weight: normal; } /* Footer */ footer { text-align: center; + margin-top: 1.5em; } -/* Wikipedia-achtige knop */ +/* Wikipedia Buttons */ button { cursor: pointer; border: none; background: none; color: #3366bb; + font-size: 0.95em; } button:disabled { - color: #ba0000; + color: #72777d; + cursor: not-allowed; } -button:hover { +button:not(:disabled):hover { text-decoration: underline; } -button:before { +#toevoegKnop:before { color: #54595d; margin-right: 0.25em; content: "["; } -button:after { +#toevoegKnop:after { color: #54595d; margin-left: 0.25em; content: "]";