Gantry 5 pozwala tworzyć własne bloki treści zwane particles — karuzele, galerie, liczniki, formularze
— zarządzane bezpośrednio z panelu administracyjnego, bez pisania PHP. W tym artykule pokazuję jak je budować od zera i czego unikać.
Struktura plików
Każda particle to obowiązkowo dwa pliki w katalogu custom/particles/ szablonu. Opcjonalnie można wydzielić CSS i JS do osobnych plików:
templates/{szablon}/custom/
├── particles/
│ ├── nazwa.yaml ← pola w panelu admina Gantry (wymagany)
│ └── nazwa.html.twig ← szablon HTML + CSS + JS (wymagany)
├── css/
│ └── nazwa.css ← opcjonalny zewnętrzny plik CSS
└── js/
└── nazwa.js ← opcjonalny zewnętrzny plik JS
Zewnętrzne pliki CSS i JS ładuje się w szablonie Twig przez protokół gantry-theme://:
{% block stylesheets %}
<link rel="stylesheet" href="/{{ url('gantry-theme://css/nazwa.css') }}">
{% endblock %}
{% block javascript_footer %}
<script src="/{{ url('gantry-theme://js/nazwa.js') }}"></script>
{% endblock %}
Pliki w katalogu custom/ nadpisują domyślne particle szablonu i nie są usuwane przy aktualizacji. Katalog particles/ jest obowiązkowy — bez niego Gantry nie wykryje particle.
Plik YAML — definicja panelu admina
YAML definiuje wszystkie pola widoczne podczas edycji particle w Gantry Layout Manager. Minimalna wymagana struktura:
name: Moja Particle
description: Opis widoczny w panelu.
type: particle
icon: fa-star
configuration:
caching:
type: static
form:
overrideable: false
fields:
enabled:
type: input.checkbox
label: Enabled
default: true
copyright:
type: separator.note
class: alert alert-success
content: 'Moja Particle — v1.0'
Zakładki
Pola warto grupować w zakładki — panel staje się znacznie czytelniejszy przy większej liczbie opcji. Wymaga Gantry 5.3.2+.
_tabs:
type: container.tabs
fields:
_tab_content:
label: Treść
fields:
title:
type: input.text
label: Tytuł
_tab_advanced:
label: Zaawansowane
fields:
class:
type: input.text
label: Dodatkowe klasy CSS
Najważniejsze typy pól
| Typ | Zastosowanie |
|---|---|
input.text |
Zwykłe pole tekstowe |
input.checkbox |
Tak / Nie |
input.number |
Liczba z zakresem (min/max/step) |
input.imagepicker |
Wybieracz obrazu z mediów Joomla |
input.colorpicker |
Kolor (hex) |
select.select |
Lista rozwijana z opcjami |
textarea.textarea |
Wieloliniowy tekst |
collection.list |
Lista powtarzalnych elementów (slajdy, karty, galeria) |
collection.keyvalue |
Dodatkowe atrybuty HTML (data-*, style) |
separator.note |
Wizualny nagłówek sekcji w panelu |
Lista powtarzalnych elementów (collection.list)
items:
type: collection.list
array: true
label: Elementy
value: name
ajax: true
overrideable: false
fields:
.name:
type: input.text
label: Tytuł
.image:
type: input.imagepicker
label: Zdjęcie
.disable:
type: input.checkbox
label: Ukryj element
default: false
Plik Twig — cztery bloki szablonu
Każda particle rozszerza bazowy szablon Gantry i może definiować cztery niezależne bloki:
{% extends '@nucleus/partials/particle.html.twig' %}
{% block stylesheets %}
{# CSS — renderowany w head #}
{% endblock %}
{% block particle %}
{# Właściwy HTML particle — wymagany #}
{% endblock %}
{% block javascript %}
{# JS w head (rzadziej używany) #}
{% endblock %}
{% block javascript_footer %}
{# JS przed </body> — zalecany dla wydajności #}
{% endblock %}
Pułapka nr 1 — zasięg zmiennych między blokami
Zmienne ustawione wewnątrz bloku NIE są widoczne w innych blokach. To najczęstsza przyczyna błędów w particles Gantry 5.
Zmienne potrzebne w wielu blokach ustaw poza blokami, na poziomie szablonu:
{% extends '@nucleus/partials/particle.html.twig' %}
{# Dostępne we WSZYSTKICH blokach #}
{% set active_items = [] %}
{% for item in particle.items|default([]) %}
{% if not item.disable %}
{% set active_items = active_items|merge([item]) %}
{% endif %}
{% endfor %}
{% set component_id = 'g-myparticle-' ~ id %}
{% block particle %}
{# active_items i component_id dostępne tutaj #}
{% endblock %}
{% block javascript_footer %}
{# i tutaj też #}
{% endblock %}
Pułapka nr 2 — URL z imagepicker
input.imagepicker przechowuje ścieżki w formacie gantry-media://.... Gantry tłumaczy ten protokół na prawdziwy URL wyłącznie przez funkcję url():
{# Poprawnie — url() tłumaczy gantry-media:// na prawdziwy URL #}
<img src="/{{ url(item.image)|e }}" alt="{{ item.alt|e }}">
{# Błędnie — przeglądarka dostaje "gantry-media://..." i nie może załadować obrazu #}
<img src="/{{ item.image|raw }}" alt="">
Gdy URL zdjęcia trafia do JavaScript (np. lightbox), przekaż go przez atrybut data-*:
{# Twig tłumaczy URL podczas renderowania — JS czyta gotową ścieżkę #}
<div data-src="{{ url(item.image)|e }}"
data-title="{{ item.name|default('')|e }}"></div>
// JavaScript — czyta gotowy URL, nie surowy protokół
var src = klikniety.dataset.src;
obrazEl.src = src;
Nigdy nie serializuj URL-i imagepicker przez json_encode. JSON zachowa surowy protokół gantry-media://, którego przeglądarka nie rozumie.
CSS zależny od ustawień — block stylesheets
{% block stylesheets %}
{% if particle.enabled %}
<style>
#{{ component_id }} { background: {{ particle.bg_color|default('#fff')|e }}; }
#{{ component_id }} .item { min-height: {{ particle.height|default(300) }}px; }
</style>
{% endif %}
{% endblock %}
Siatka responsywna — CSS Grid zamiast Bootstrap row/col
Gantry 5 oparty na UIkit nie ładuje Bootstrap CSS. Klasy row, col-4 itp. nie działają bez Bootstrap. Rozwiązanie: własne CSS Grid w {% block stylesheets %}:
{% set cols_xs = particle.cols_xs|default(1) %}
{% set cols_md = particle.cols_md|default(2) %}
{% set cols_lg = particle.cols_lg|default(3) %}
{% block stylesheets %}
<style>
#{{ component_id }} .g-grid {
display: grid;
grid-template-columns: repeat({{ cols_xs }}, 1fr);
gap: 16px;
}
@media (min-width: 768px) {
#{{ component_id }} .g-grid { grid-template-columns: repeat({{ cols_md }}, 1fr); }
}
@media (min-width: 992px) {
#{{ component_id }} .g-grid { grid-template-columns: repeat({{ cols_lg }}, 1fr); }
}
</style>
{% endblock %}
Ładowanie Bootstrap JS w Joomla
Joomla nie ładuje automatycznie komponentów Bootstrap JS. Każdy komponent to osobny plik ES moduł w katalogu vendor:
/media/vendor/bootstrap/js/carousel.min.js
/media/vendor/bootstrap/js/modal.min.js
/media/vendor/bootstrap/js/collapse.min.js
Poprawne podejście to dynamic-import z URL-em root pobranym z konfiguracji Joomla:
{% block javascript_footer %}
<script type="module">
// Pobierz root strony — obsługa instalacji w podkatalogu
const root = Joomla.getOptions('system.paths').root;
// Załaduj komponent z vendor — eksportuje klasę jako module.C
const { C: Carousel } = await import(root + '/media/vendor/bootstrap/js/carousel.min.js');
new Carousel(document.getElementById('{{ component_id }}'), {
interval : 5000,
ride : 'carousel',
touch : true,
wrap : true
});
</script>
{% endblock %}
| Komponent | Eksport (minified) | Fallback |
|---|---|---|
carousel.min.js |
module.C |
module.C || module.Carousel || module.default |
modal.min.js |
module.M |
module.M || module.Modal || module.default |
Bootstrap CSS — Joomla 4 vs Joomla 5 vs Gantry 5
Przed użyciem klas Bootstrap (.modal, .btn, .d-flex) sprawdź <head> strony. Jeśli nie ma bootstrap.min.css — dodaj potrzebne style inline w {% block stylesheets %} particle.
Studium przypadku — galeria z lightboxem
Budując particle galerii dla szablonu Gantry 5 na Joomla 4, napotkałem kilka nieoczywistych problemów:
Problem 1
Zdjęcia nie ładowały się w lightboxie — imagepicker zwraca gantry-media://, który JS przekazywał bezpośrednio jako src.
Rozwiązanie: Atrybut data-src="{{ url(item.image)|e }}" zamiast JSON.
Problem 2
Siatka nie działała — Bootstrap row/col-* bez załadowanego Bootstrap CSS.
Rozwiązanie: CSS Grid w {% block stylesheets %} — niezależny od żadnego frameworka.
Problem 3
Modal nie miał wyglądu — Bootstrap Modal CSS nie ładuje się na Joomla 4 z Gantry/UIkit.
Rozwiązanie: Minimalne style modalu wbudowane inline w particle.
Bonus — naprawa instalacji szablonu Gantry na Joomla 6
Starsze szablony Gantry 5 podczas instalacji na Joomla 6 zgłaszają błąd — używają klasy usuniętej w nowej wersji. Naprawa w pliku install.php szablonu:
// Było (Joomla 3/4):
use Joomla\CMS\Filesystem\Folder;
// Powinno być (Joomla 5+/6):
use Joomla\Filesystem\Folder;
Podsumowanie
- Zmienne globalne (poza blokami) są dostępne wszędzie — zmienne w bloku tylko w nim
- URL-e z
imagepickerzawsze przezurl()— nigdy|rawani JSON - Siatka responsywna przez CSS Grid, nie Bootstrap
row/col - Bootstrap JS ładowany przez dynamic-import z katalogu vendor Joomla
- Bootstrap CSS sprawdź w
<head>— nie zakładaj że jest załadowany
Potrzebujesz niestandardowej funkcjonalności na swojej stronie Joomla?
Chętnie pomogę — sprawdź moje portfolio lub napisz bezpośrednio.