Acordeão
O bloco accordion-block combina um Fieldset estático de dois campos com uma interface dinâmica de itens gerida por JavaScript. O Fieldset expõe title e description. Os itens do acordeão entram e saem via accordion-block-admin.js, que injeta inputs de texto e textareas com CKEditor em runtime. Cada item é persistido em o:data[items][i][title|text] e a lista é reindexada ao remover um item.
Como foi construído
Identificação
| Item | Valor |
|---|---|
| Classe | SGMBlocks\Site\BlockLayout\AccordionBlock |
block_layout | accordion-block |
| Form | SGMBlocks\Form\AccordionBlockForm |
| Factory | SGMBlocks\Service\BlockLayout\AccordionBlockFactory |
| Partial | common/block-layout/accordion-block |
Formulário estático
AccordionBlockForm define dois campos. title e description são os campos do cabeçalho do bloco inteiro, não dos itens:
$this->add(['name' => 'o:block[__blockIndex__][o:data][title]',
'type' => Element\Text::class,
'options' => ['label' => 'Título']]);
$this->add(['name' => 'o:block[__blockIndex__][o:data][description]',
'type' => Element\Textarea::class,
'options' => ['label' => 'Descrição (opcional)'],
'attributes' => ['rows' => 3]]);
Os itens dinâmicos não aparecem nessa classe. Eles são gerados pelo JavaScript.
Classe do bloco
AccordionBlock::prepareForm é o ponto crítico. Ele anexa o admin.css para o estilo do gerenciador e o accordion-block-admin.js que cria e controla os itens:
public function prepareForm(PhpRenderer $view)
{
$view->headLink()->appendStylesheet($view->assetUrl('css/admin.css', 'SGMBlocks'));
$view->headScript()->appendFile($view->assetUrl('js/accordion-block-admin.js', 'SGMBlocks'));
}
AccordionBlock::form é incomum: além de formCollection do Fieldset, concatena HTML manual para o container dos itens, incluindo o array existente serializado em JSON num atributo data-items:
$items = [];
if ($block && $block->data()) {
$form->populateValues([
'o:block[__blockIndex__][o:data][title]' => $block->dataValue('title'),
'o:block[__blockIndex__][o:data][description]' => $block->dataValue('description'),
]);
$items = $block->dataValue('items') ?: [];
}
$formHtml = $view->formCollection($form);
$itemsJson = $view->escapeHtmlAttr(json_encode($items, JSON_HEX_APOS | JSON_HEX_QUOT));
$itemsHtml = <<<HTML
<div class="accordion-items-manager" data-items="{$itemsJson}">
<div class="field">
<div class="field-meta"><label>Itens do Acordeão</label></div>
<div class="inputs">
<div class="accordion-items-container"></div>
<button type="button" class="accordion-add-item o-icon-add button">Adicionar item</button>
</div>
</div>
</div>
HTML;
return $formHtml . $itemsHtml;
O JavaScript lê data-items, popula a UI e, a cada interação, atualiza os inputs para manter o formato aninhado o:block[__blockIndex__][o:data][items][i][title|text].
AccordionBlock::render passa items ao partial junto de title e description:
return $view->partial('common/block-layout/accordion-block', [
'title' => $block->dataValue('title'),
'description' => $block->dataValue('description'),
'items' => $block->dataValue('items') ?: [],
]);
Partial
<section class="sgm-accordion-block">
<?php if (!empty($title)): ?>
<h2 class="sgm-accordion-title"><?= $this->escapeHtml($title) ?></h2>
<?php endif; ?>
<?php if (!empty($description)): ?>
<p class="sgm-accordion-description"><?= $this->escapeHtml($description) ?></p>
<?php endif; ?>
<div class="sgm-accordion-list">
<?php foreach ($items as $i => $item): ?>
<?php if (empty($item['title'])) continue; ?>
<div class="sgm-accordion-item">
<button type="button" class="sgm-accordion-trigger"
aria-expanded="false"
aria-controls="sgm-accordion-panel-<?= $i ?>">
<span class="sgm-accordion-item-title"><?= $this->escapeHtml($item['title']) ?></span>
<span class="sgm-accordion-icon" aria-hidden="true">...</span>
</button>
<div id="sgm-accordion-panel-<?= $i ?>"
class="sgm-accordion-panel"
role="region"
hidden>
<div class="sgm-accordion-panel-content">
<?= $item['text'] ?? '' ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
Observe: item['title'] usa escapeHtml, mas item['text'] sai sem escape porque vem do CKEditor. Essa decisão é deliberada para preservar marcação rica. O script inline do partial cuida do toggle via aria-expanded e do atributo hidden do painel.
JavaScript do admin
O accordion-block-admin.js é uma IIFE jQuery. Pontos técnicos:
- Cada
.accordion-items-manageré inicializado uma única vez. O guard usadata-accordion-initialized. - Itens existentes vêm do atributo
data-itemsparseado comJSON.parse. getBlockIndexlêdata-blockIndexdo.blockancestral do gerenciador. Quando não existe, cai para o placeholder__blockIndex__.addItemgera um.accordion-item-entrycom um input de título e uma textarea com as classesblock-html full wysiwyg, depois chamainitWysiwygpara inicializar o CKEditor.initWysiwygchama$(textarea).ckeditor()e guarda a instância emdata-ckeditorInstancepara permitir destruição posterior.reindexItemsrefaz osnamedos inputs após remoções para manter a ordemitems[0..n-1]. Antes de renomear, chamaeditor.updateElement()para sincronizar o valor do CKEditor no DOM.- O listener
o:block-addedreinicializa gerenciadores de blocos criados dinamicamente pelo editor do Omeka.
Trecho do addItem:
var $textarea = $("<textarea/>")
.attr("id", textareaId)
.attr("name", textName)
.addClass("block-html full wysiwyg")
.attr("rows", 6)
.text(text);
$fields.append($titleField, $textField);
$entry.append($header, $fields);
$container.append($entry);
initWysiwyg($textarea[0]);
itemIndex++;
updateLabels();
Persistência
Chave em o:data | Tipo | Observação |
|---|---|---|
title | string | Cabeçalho do bloco. escapeHtml no partial. |
description | string | Subtítulo. escapeHtml no partial. |
items | array | Cada item é ['title' => string, 'text' => string HTML]. text sem escape. |
Exemplo persistido:
{
"title": "Perguntas frequentes",
"description": "Dúvidas comuns sobre o acervo",
"items": [
{ "title": "Como acesso?", "text": "<p>Visite...</p>" },
{ "title": "Horários", "text": "<p>Segunda a sexta...</p>" }
]
}
Pipeline do JSON de itens
flowchart LR
SavedData["items em o:data"] --> PhpForm["AccordionBlock form"]
PhpForm --> DataItems["data-items JSON no HTML"]
DataItems --> JsInit["accordion-block-admin.js init"]
JsInit --> AddItem[addItem renderiza entry]
AddItem --> CkEditor[initWysiwyg cria CKEditor]
CkEditor --> Submit[submit do editor]
Submit --> OmekaPersist[Omeka grava items em o:data]
A cada ciclo de edição, o PHP serializa a lista para o JS, o JS renderiza e torna editável, o CKEditor atualiza o DOM antes do submit, e o Omeka persiste de volta.
Como usar como desenvolvedor
Adicionar um campo novo aos itens
Requer mudanças em três lugares:
accordion-block-admin.js. Acrescente um input emaddItemcomname = 'o:block[' + bi + '][o:data][items][' + idx + '][nome_do_campo]'.AccordionBlock::form. Ajuste odata-itemsse o JSON salvar um valor pré-existente.accordion-block.phtml. Leia$item['nome_do_campo']e renderize.
O Omeka persiste sozinho, desde que o name respeite o padrão aninhado.
Sobrescrever o partial no tema
Copie view/common/block-layout/accordion-block.phtml para themes/<tema>/view/common/block-layout/accordion-block.phtml. O tema pode trocar os ícones, usar <details> nativo em vez de <button>, ou aplicar animações CSS específicas.
Trocar o WYSIWYG por outra biblioteca
Substitua a classe wysiwyg por outro gatilho e reescreva initWysiwyg. Exemplo com Quill ou Trix exige carregar o JS da biblioteca no prepareForm e adaptar destroyWysiwyg para a API correspondente. Cuide do reindexItems para chamar o equivalente a editor.updateElement().
Customizar o toggle do frontend
O script inline do partial é mínimo. Para adicionar animação de altura, substitua a lógica:
document.querySelectorAll(".sgm-accordion-trigger").forEach(function (trigger) {
trigger.addEventListener("click", function () {
var expanded = this.getAttribute("aria-expanded") === "true";
this.setAttribute("aria-expanded", String(!expanded));
var panel = document.getElementById(this.getAttribute("aria-controls"));
if (panel) {
panel.hidden = expanded;
}
});
});
Troque panel.hidden por classes CSS e anime via transition: max-height se preferir transições suaves. Lembre-se de manter aria-expanded e aria-controls por acessibilidade.
Ler itens fora do bloco
foreach ($page->blocks() as $block) {
if ($block->layout() === 'accordion-block') {
$items = $block->dataValue('items') ?: [];
foreach ($items as $item) {
echo $item['title'], $item['text'];
}
}
}
Validar itens no submit
Anexe um listener a form.add_input_filters do AccordionBlockForm. O filter pode rodar Callback que percorre o array e descarta itens sem title. Alternativamente, filtre no próprio AccordionBlock::form antes de popular e no partial ao iterar.
Depurar
- Se os itens somem ao salvar, inspecione o DOM antes do submit. Os
namedevem sero:block[0][o:data][items][0][title]com números reais, nunca com__blockIndex__. Se aparecer__blockIndex__, o.blockancestral não temdata-blockIndexe ogetBlockIndexcaiu no fallback. - Se o CKEditor não inicializa, verifique a ordem de carregamento. O Omeka injeta
CKEDITORglobalmente; o JS do acordeão precisa rodar depois. - Para debugar o JSON persistido, use a API do Omeka:
api read site_pagese inspecioneo:blockdo bloco de acordeão.
Observações
- Itens sem título somem do render. O partial faz
continuequandoempty($item['title']). Isso é proposital, mas pode confundir editores. textsem escape. O HTML do CKEditor entra direto no DOM. Qualquer conteúdo hostil colado no editor vai parar na página pública.- Ids dos paineis. O id é
sgm-accordion-panel-{i}, onde{i}é o índice dentro do array. Se dois blocos de acordeão aparecem na mesma página, dois itens podem acabar com o mesmoid. Para deduplicar, prefixe com o id do bloco. data-itemscomJSON_HEX_APOSeJSON_HEX_QUOT. Ojson_encodeusa essas flags para evitar problemas ao interpolar dentro do atributo. Ao extrair no JS,JSON.parseainda reconhece os códigos.- Reindexação. Remover o item 1 em uma lista de 3 faz o item 2 virar 1 e o 3 virar 2.
reindexItemschamaeditor.updateElement()antes de trocarnameporque o CKEditor só escreve no textarea quando pedido.