mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
- Auto-save: new autosave.js with 1.5s debounce, watches all forms with
data-autosave, POSTs to form action with Accept: application/json, shows
saving/saved/error status indicator
- All action handlers (page.php, apropos.php, form-help.php) now detect
JSON Accept header and return {success, csrf_token} or {error} responses
- OverType toolbar enabled (toolbar:true) on all three markdown editors
(page, about_page, form_help)
- Sidebar links: replaced fixed erg_site_url / source_code_url rows with
dynamic sidebar_links array of {label, url} objects. Add/remove via JS.
Fallback migration reads legacy keys if sidebar_links is empty.
- Updated AboutController and about.php template to render dynamic links
- Updated apropos.css: unified .apropos-toc-link replacing .apropos-toc-erg
and .apropos-toc-source
- New CSS: autosave-status states, sidebar-link-row layout
- Removed all Enregistrer + Annuler buttons — auto-save and h1 back-arrow
make them redundant
172 lines
7.9 KiB
PHP
172 lines
7.9 KiB
PHP
<?php
|
|
/**
|
|
* Reusable partial for apropos contacts groups form.
|
|
* Expected variables:
|
|
* $aproposKey string 'contacts'
|
|
* $groups array Existing groups data
|
|
*/
|
|
?>
|
|
<form action="/admin/actions/apropos.php" method="post" class="admin-form" id="apropos-form-<?= $aproposKey ?>" data-autosave>
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
|
<input type="hidden" name="apropos_key" value="<?= htmlspecialchars($aproposKey) ?>">
|
|
|
|
<?php foreach ($groups as $gi => $group): ?>
|
|
<fieldset class="apropos-group">
|
|
<legend>Contact <?= $gi + 1 ?></legend>
|
|
<label for="group_f_<?= $aproposKey ?>_<?= $gi ?>_role">Rôle :</label>
|
|
<input type="text" id="group_f_<?= $aproposKey ?>_<?= $gi ?>_role"
|
|
name="groups[<?= $gi ?>][role]"
|
|
value="<?= htmlspecialchars($group['role'] ?? '') ?>">
|
|
|
|
<?php $entries = is_array($group['entries'] ?? null) ? $group['entries'] : []; ?>
|
|
<?php foreach ($entries as $ei => $entry): ?>
|
|
<div class="apropos-entry">
|
|
<div>
|
|
<label for="entry_f_<?= $aproposKey ?>_<?= $gi ?>_<?= $ei ?>_text">Nom :</label>
|
|
<input type="text" id="entry_f_<?= $aproposKey ?>_<?= $gi ?>_<?= $ei ?>_text"
|
|
name="groups[<?= $gi ?>][entries][<?= $ei ?>][text]"
|
|
value="<?= htmlspecialchars($entry['text'] ?? '') ?>">
|
|
</div>
|
|
<div>
|
|
<label for="entry_f_<?= $aproposKey ?>_<?= $gi ?>_<?= $ei ?>_email">Email :</label>
|
|
<input type="email" id="entry_f_<?= $aproposKey ?>_<?= $gi ?>_<?= $ei ?>_email"
|
|
name="groups[<?= $gi ?>][entries][<?= $ei ?>][email]"
|
|
value="<?= htmlspecialchars($entry['email'] ?? '') ?>">
|
|
</div>
|
|
<div>
|
|
<label for="entry_f_<?= $aproposKey ?>_<?= $gi ?>_<?= $ei ?>_url">Lien (optionnel) :</label>
|
|
<input type="url" id="entry_f_<?= $aproposKey ?>_<?= $gi ?>_<?= $ei ?>_url"
|
|
name="groups[<?= $gi ?>][entries][<?= $ei ?>][url]"
|
|
value="<?= htmlspecialchars($entry['url'] ?? '') ?>">
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
|
|
<button type="button" class="btn btn--primary btn--sm add-entry-btn-f"
|
|
data-group="<?= $gi ?>"
|
|
data-key="<?= $aproposKey ?>">+ Ajouter une entrée</button>
|
|
<button type="button" class="admin-icon-btn admin-icon-btn--delete delete-group-btn-f"
|
|
data-key="<?= $aproposKey ?>" title="Supprimer ce contact">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM112,168V104a8,8,0,0,1,16,0v64a8,8,0,0,1-16,0Zm48,0V104a8,8,0,0,1,16,0v64a8,8,0,0,1-16,0Z"></path></svg>
|
|
</button>
|
|
</fieldset>
|
|
<?php endforeach; ?>
|
|
|
|
<button type="button" class="btn btn--primary add-group-btn-f"
|
|
data-key="<?= $aproposKey ?>">+ Ajouter un contact</button>
|
|
|
|
<div class="autosave-status" data-autosave-status></div>
|
|
|
|
<template id="entry-template-f-<?= $aproposKey ?>">
|
|
<div class="apropos-entry">
|
|
<div>
|
|
<label>Nom :</label>
|
|
<input type="text" name="groups[{{gi}}][entries][{{ei}}][text]">
|
|
</div>
|
|
<div>
|
|
<label>Email :</label>
|
|
<input type="email" name="groups[{{gi}}][entries][{{ei}}][email]">
|
|
</div>
|
|
<div>
|
|
<label>Lien (optionnel) :</label>
|
|
<input type="url" name="groups[{{gi}}][entries][{{ei}}][url]">
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template id="group-template-f-<?= $aproposKey ?>">
|
|
<fieldset class="apropos-group">
|
|
<legend>Contact {{gi}}</legend>
|
|
<label>Rôle :</label>
|
|
<input type="text" name="groups[{{gi}}][role]">
|
|
<button type="button" class="btn btn--primary btn--sm add-entry-btn-f"
|
|
data-group="{{gi}}"
|
|
data-key="<?= $aproposKey ?>">+ Ajouter une entrée</button>
|
|
<button type="button" class="admin-icon-btn admin-icon-btn--delete delete-group-btn-f"
|
|
data-key="<?= $aproposKey ?>" title="Supprimer ce contact">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM112,168V104a8,8,0,0,1,16,0v64a8,8,0,0,1-16,0Zm48,0V104a8,8,0,0,1,16,0v64a8,8,0,0,1-16,0Z"></path></svg>
|
|
</button>
|
|
</fieldset>
|
|
</template>
|
|
</form>
|
|
|
|
<script>
|
|
(function() {
|
|
var key = '<?= $aproposKey ?>';
|
|
var form = document.getElementById('apropos-form-' + key);
|
|
var groupCount = <?= count($groups) ?>;
|
|
var entryTpl = document.getElementById('entry-template-f-' + key).innerHTML;
|
|
var groupTpl = document.getElementById('group-template-f-' + key).innerHTML;
|
|
|
|
function reindexGroups() {
|
|
var fieldsets = form.querySelectorAll('fieldset.apropos-group');
|
|
groupCount = fieldsets.length;
|
|
fieldsets.forEach(function(fs, i) {
|
|
var newIdx = i;
|
|
var legend = fs.querySelector('legend');
|
|
if (legend) legend.textContent = 'Contact ' + (newIdx + 1);
|
|
|
|
// Update name attributes on all inputs
|
|
fs.querySelectorAll('input').forEach(function(inp) {
|
|
if (inp.name) {
|
|
inp.name = inp.name.replace(/groups\[\d+\]/, 'groups[' + newIdx + ']');
|
|
}
|
|
if (inp.id) {
|
|
inp.id = inp.id.replace(/(group_f_contacts_|entry_f_contacts_)\d+/, '$1' + newIdx);
|
|
}
|
|
});
|
|
|
|
// Update for attributes on labels
|
|
fs.querySelectorAll('label[for]').forEach(function(lbl) {
|
|
lbl.setAttribute('for', lbl.getAttribute('for').replace(/(group_f_contacts_|entry_f_contacts_)\d+/, '$1' + newIdx));
|
|
});
|
|
|
|
// Update data-group on add-entry button
|
|
var addBtn = fs.querySelector('.add-entry-btn-f');
|
|
if (addBtn) addBtn.dataset.group = newIdx;
|
|
});
|
|
}
|
|
|
|
function bindAddEntry(btn) {
|
|
btn.addEventListener('click', function() {
|
|
var gi = parseInt(this.dataset.group);
|
|
var fieldset = this.closest('fieldset');
|
|
var entryCount = fieldset.querySelectorAll('.apropos-entry').length;
|
|
var html = entryTpl.replaceAll('{{gi}}', gi).replaceAll('{{ei}}', entryCount);
|
|
this.insertAdjacentHTML('beforebegin', html);
|
|
});
|
|
}
|
|
|
|
function bindDeleteGroup(btn) {
|
|
btn.addEventListener('click', function() {
|
|
var fieldset = this.closest('fieldset');
|
|
if (fieldset) {
|
|
fieldset.remove();
|
|
reindexGroups();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Bind existing buttons
|
|
form.querySelectorAll('.add-entry-btn-f').forEach(bindAddEntry);
|
|
form.querySelectorAll('.delete-group-btn-f').forEach(bindDeleteGroup);
|
|
|
|
form.querySelector('.add-group-btn-f').addEventListener('click', function() {
|
|
groupCount++;
|
|
var html = groupTpl.replaceAll('{{gi}}', groupCount);
|
|
this.insertAdjacentHTML('beforebegin', html);
|
|
|
|
var newGroup = this.previousElementSibling;
|
|
if (newGroup && newGroup.classList.contains('apropos-group')) {
|
|
var addBtn = newGroup.querySelector('.add-entry-btn-f');
|
|
if (addBtn) {
|
|
addBtn.dataset.group = groupCount;
|
|
bindAddEntry(addBtn);
|
|
}
|
|
var delBtn = newGroup.querySelector('.delete-group-btn-f');
|
|
if (delBtn) bindDeleteGroup(delBtn);
|
|
}
|
|
});
|
|
})();
|
|
</script>
|