From ab5e9b30b557396e899fac4b36cd72c3bfe607b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Mummert?= Date: Wed, 4 Mar 2026 18:38:19 +0100 Subject: [PATCH] Refine muuri pinboard layout, spacing and remove randomization --- contao/templates/frontend/pinnwand.html.twig | 27 +-- public/assets/pinboard.css | 30 ++-- public/assets/pinboard.js | 158 ++++++------------ .../FrontendModule/PinboardController.php | 15 +- 4 files changed, 99 insertions(+), 131 deletions(-) diff --git a/contao/templates/frontend/pinnwand.html.twig b/contao/templates/frontend/pinnwand.html.twig index 0b08fdc..c1516de 100644 --- a/contao/templates/frontend/pinnwand.html.twig +++ b/contao/templates/frontend/pinnwand.html.twig @@ -12,20 +12,22 @@ data-seed="{{ entry.id }}" aria-label="{{ entry.headline }}" > - {% if entry.imageFigure %} -
- {% with {figure: entry.imageFigure} %}{{ block('figure_component') }}{% endwith %} -
- {% endif %} +
+ {% if entry.imageFigure %} +
+ {% with {figure: entry.imageFigure} %}{{ block('figure_component') }}{% endwith %} +
+ {% endif %} -

{{ entry.headline }}

-
{{ entry.text|e|nl2br }}
+

{{ entry.headline }}

+
{{ entry.text|e|nl2br }}
- {% if entry.link %} - - {% endif %} + {% if entry.link %} + + {% endif %} +
{% else %}

Keine veröffentlichten Pinnwandeinträge vorhanden.

@@ -33,4 +35,5 @@ + diff --git a/public/assets/pinboard.css b/public/assets/pinboard.css index 09094cf..f01f537 100644 --- a/public/assets/pinboard.css +++ b/public/assets/pinboard.css @@ -2,12 +2,14 @@ width: 100%; max-width: 1400px; margin: 0 auto; - padding: 1.5rem; + padding: 2em; + box-sizing: border-box; } .pinboard__surface { position: relative; min-height: 42rem; + padding: 1.2rem; border-radius: 1.2rem; overflow: hidden; background: @@ -20,20 +22,29 @@ .pin-note { position: absolute; - width: clamp(220px, 28vw, 340px); + width: clamp(220px, 26vw, 320px); + min-height: 230px; + padding: 1.5em; + box-sizing: border-box; + overflow: visible; +} + +.pin-note__inner { + position: relative; min-height: 190px; padding: 1rem 1rem 1.25rem; border-radius: 0.3rem; background: linear-gradient(160deg, #fff8a8 0%, #f5eb85 100%); color: #2a241c; box-shadow: 0 10px 22px rgba(20, 10, 5, 0.34); - cursor: grab; + cursor: pointer; user-select: none; - touch-action: none; - transition: box-shadow 160ms ease, transform 160ms ease; + touch-action: auto; + transform: translate(0, 0) rotate(0deg); + transition: box-shadow 160ms ease; } -.pin-note::before { +.pin-note__inner::before { content: ''; position: absolute; top: -9px; @@ -46,12 +57,11 @@ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.35); } -.pin-note.is-highlighted { +.pin-note.is-highlighted .pin-note__inner { background: linear-gradient(160deg, #ffd99b 0%, #f7c46f 100%); } -.pin-note.is-dragging { - cursor: grabbing; +.pin-note.is-front .pin-note__inner { box-shadow: 0 18px 34px rgba(20, 10, 5, 0.46); } @@ -97,7 +107,7 @@ @media (max-width: 768px) { .pinboard { - padding: 0.8rem; + padding: 2em; } .pinboard__surface { diff --git a/public/assets/pinboard.js b/public/assets/pinboard.js index 6f7684a..64afed6 100644 --- a/public/assets/pinboard.js +++ b/public/assets/pinboard.js @@ -5,121 +5,63 @@ return; } + if (typeof Muuri === 'undefined') { + return; + } + const notes = Array.from(board.querySelectorAll('[data-pin-note]')); if (!notes.length) { return; } - let zCounter = 200; + let zCounter = 500; - const randomBetween = (min, max) => Math.random() * (max - min) + min; + const highlighted = notes.filter((note) => note.dataset.highlighted === '1'); + const normal = notes.filter((note) => note.dataset.highlighted !== '1'); + const ordered = [...highlighted, ...normal]; - const placeNotes = () => { - const boardRect = board.getBoundingClientRect(); - const maxXBase = Math.max(24, boardRect.width - 300); - const maxYBase = Math.max(24, boardRect.height - 220); - - notes.forEach((note, index) => { - const maxX = Math.max(16, maxXBase - note.offsetWidth * 0.2); - const maxY = Math.max(16, maxYBase - note.offsetHeight * 0.2); - - const x = randomBetween(12, maxX); - const y = randomBetween(12, maxY); - const rotation = randomBetween(-7, 7); - const highlighted = note.dataset.highlighted === '1'; - const level = highlighted ? 1000 + index : 100 + index; - - note.dataset.baseRotation = String(rotation); - note.dataset.x = String(x); - note.dataset.y = String(y); - note.style.zIndex = String(level); - note.style.transform = `translate(${x}px, ${y}px) rotate(${rotation}deg)`; - }); - }; - - const clampToBoard = (note, x, y) => { - const boardRect = board.getBoundingClientRect(); - const noteRect = note.getBoundingClientRect(); - const maxX = Math.max(0, boardRect.width - noteRect.width); - const maxY = Math.max(0, boardRect.height - noteRect.height); - - return { - x: Math.min(Math.max(0, x), maxX), - y: Math.min(Math.max(0, y), maxY), - }; - }; - - const enableDrag = (note) => { - let dragging = false; - let pointerId = null; - let offsetX = 0; - let offsetY = 0; - - note.addEventListener('pointerdown', (event) => { - if (event.target instanceof Element && event.target.closest('a, button, input, select, textarea, label')) { - return; - } - - dragging = true; - pointerId = event.pointerId; - note.setPointerCapture(pointerId); - note.classList.add('is-dragging'); - note.style.zIndex = String(++zCounter + 2000); - - const startX = Number.parseFloat(note.dataset.x ?? '0'); - const startY = Number.parseFloat(note.dataset.y ?? '0'); - offsetX = event.clientX - startX; - offsetY = event.clientY - startY; - }); - - note.addEventListener('pointermove', (event) => { - if (!dragging || event.pointerId !== pointerId) { - return; - } - - const baseRotation = Number.parseFloat(note.dataset.baseRotation ?? '0'); - const nextX = event.clientX - offsetX; - const nextY = event.clientY - offsetY; - const clamped = clampToBoard(note, nextX, nextY); - - note.dataset.x = String(clamped.x); - note.dataset.y = String(clamped.y); - note.style.transform = `translate(${clamped.x}px, ${clamped.y}px) rotate(${baseRotation}deg)`; - }); - - const releaseDrag = (event) => { - if (!dragging || event.pointerId !== pointerId) { - return; - } - - dragging = false; - note.classList.remove('is-dragging'); - note.releasePointerCapture(pointerId); - pointerId = null; - }; - - note.addEventListener('pointerup', releaseDrag); - note.addEventListener('pointercancel', releaseDrag); - }; - - const adjustBoardHeight = () => { - let requiredHeight = 620; - - notes.forEach((note) => { - const y = Number.parseFloat(note.dataset.y ?? '0'); - requiredHeight = Math.max(requiredHeight, y + note.offsetHeight + 36); - }); - - board.style.minHeight = `${Math.ceil(requiredHeight)}px`; - }; - - placeNotes(); - notes.forEach(enableDrag); - adjustBoardHeight(); - - window.addEventListener('resize', () => { - placeNotes(); - adjustBoardHeight(); + ordered.forEach((note) => { + board.appendChild(note); }); + + ordered.forEach((note, index) => { + const isHighlighted = note.dataset.highlighted === '1'; + note.style.zIndex = String(isHighlighted ? 300 + index : 100 + index); + }); + + const grid = new Muuri(board, { + items: '.pin-note', + dragEnabled: false, + layout: { + fillGaps: false, + horizontal: false, + alignRight: false, + alignBottom: false, + rounding: false, + }, + layoutDuration: 250, + layoutEasing: 'ease', + }); + + const bringToFront = (note) => { + zCounter += 1; + note.style.zIndex = String(zCounter); + + ordered.forEach((item) => item.classList.remove('is-front')); + note.classList.add('is-front'); + }; + + ordered.forEach((note) => { + note.addEventListener('pointerdown', () => { + bringToFront(note); + }); + }); + + const relayout = () => { + grid.refreshItems().layout(); + }; + + relayout(); + window.addEventListener('resize', relayout); })(); diff --git a/src/Controller/FrontendModule/PinboardController.php b/src/Controller/FrontendModule/PinboardController.php index 239bc0a..74235d6 100644 --- a/src/Controller/FrontendModule/PinboardController.php +++ b/src/Controller/FrontendModule/PinboardController.php @@ -6,6 +6,7 @@ namespace Eiswurm\ContaoPinboardBundle\Controller\FrontendModule; use Contao\CoreBundle\Controller\FrontendModule\AbstractFrontendModuleController; use Contao\CoreBundle\Image\Studio\Studio; +use Contao\CoreBundle\InsertTag\InsertTagParser; use Contao\CoreBundle\Twig\FragmentTemplate; use Contao\FilesModel; use Contao\ModuleModel; @@ -18,6 +19,7 @@ final class PinboardController extends AbstractFrontendModuleController { public function __construct( private readonly Studio $studio, + private readonly InsertTagParser $insertTagParser, ) { } @@ -60,7 +62,7 @@ final class PinboardController extends AbstractFrontendModuleController 'id' => (int) $entry->id, 'headline' => (string) $entry->ueberschrift, 'text' => (string) $entry->text, - 'link' => (string) $entry->link, + 'link' => $this->resolveLink((string) $entry->link), 'dateAdded' => (int) $entry->dateAdded, 'dateModified' => (int) $entry->dateModified, 'imageFigure' => $imageFigure, @@ -74,4 +76,15 @@ final class PinboardController extends AbstractFrontendModuleController return $template->getResponse(); } + + private function resolveLink(string $link): string + { + $link = trim($link); + + if ('' === $link) { + return ''; + } + + return html_entity_decode($this->insertTagParser->replaceInline($link), \ENT_QUOTES | \ENT_HTML5); + } }