mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-11-09 11:10:08 +01:00
feat: migrate admin system page to HTMX with tab-based navigation and log viewer
This commit is contained in:
1 parent
05002ccee4
commit
b03be51b92
10 files changed
+146
-165
No files matched your search
@@ -24,3 +24,4 @@ Thumbs.db
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
/node_modules
|
||||
@@ -12,3 +12,4 @@
|
||||
- [x] Delete public/admin/pages-edit.php
|
||||
- [x] Delete public/admin/actions/page.php
|
||||
- [x] Update storage/schema.sql with apropos_contents table + trigger
|
||||
- [x] Rework system.php/system.js: replace custom fetch() JS with HTMX, inline onclick for copy + collapse
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "posterg",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"vendor": "cp node_modules/htmx.org/dist/htmx.min.js public/assets/js/htmx.min.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"dependencies": {
|
||||
"htmx.org": "^2.0.8"
|
||||
}
|
||||
}
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
htmx.org:
|
||||
specifier: ^2.0.8
|
||||
version: 2.0.8
|
||||
|
||||
packages:
|
||||
|
||||
htmx.org@2.0.8:
|
||||
resolution: {integrity: sha512-fm297iru0iWsNJlBrjvtN7V9zjaxd+69Oqjh4F/Vq9Wwi2kFisLcrLCiv5oBX0KLfOX/zG8AUo9ROMU5XUB44Q==}
|
||||
|
||||
snapshots:
|
||||
|
||||
htmx.org@2.0.8: {}
|
||||
@@ -77,7 +77,8 @@ if ($tab === 'nginx_config') {
|
||||
<div class="log-empty">Le fichier de configuration est vide.</div>
|
||||
<?php else: ?>
|
||||
<div class="log-output" id="log-output" role="region" aria-label="Configuration nginx">
|
||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier la configuration">Copier</button>
|
||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier la configuration"
|
||||
onclick="copyLogContent(this);return false">Copier</button>
|
||||
<?php foreach ($lines as $i => $line): ?>
|
||||
<span class="log-line <?= SystemController::nginxLineClass($line) ?>"
|
||||
data-n="<?= $i + 1 ?>"><?= htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?></span>
|
||||
@@ -93,12 +94,19 @@ if ($tab === 'nginx_config') {
|
||||
$logMeta = $data['meta'];
|
||||
?>
|
||||
<div class="log-toolbar">
|
||||
<label for="lines-select">Afficher</label>
|
||||
<select id="lines-select" aria-label="Nombre de lignes">
|
||||
<form id="lines-form" hx-get="/admin/system-fragment.php"
|
||||
hx-target="#sys-tab-panel"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#sys-tab-panel"
|
||||
hx-trigger="change"
|
||||
hx-vals='{"tab":"<?= htmlspecialchars($tab) ?>"}'>
|
||||
<label for="lines-select">Afficher</label>
|
||||
<select id="lines-select" name="n" aria-label="Nombre de lignes">
|
||||
<?php foreach (SystemController::ALLOWED_LINES as $opt): ?>
|
||||
<option value="<?= $opt ?>" <?= $opt === $n ? 'selected' : '' ?>><?= $opt ?> dernières lignes</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</select>
|
||||
</form>
|
||||
<?php if ($logLines !== null && count($logLines) > 0): ?>
|
||||
<span class="log-count-badge"><?= count($logLines) ?> ligne(s)</span>
|
||||
<?php endif; ?>
|
||||
@@ -127,7 +135,8 @@ if ($tab === 'nginx_config') {
|
||||
<div class="log-empty">Le fichier journal est vide.</div>
|
||||
<?php else: ?>
|
||||
<div class="log-output" id="log-output" role="log" aria-live="off" aria-label="Contenu du journal">
|
||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier le contenu">Copier</button>
|
||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier le contenu"
|
||||
onclick="copyLogContent(this);return false">Copier</button>
|
||||
<?php foreach ($logLines as $i => $line): ?>
|
||||
<span class="log-line <?= SystemController::logLineClass($line) ?>"
|
||||
data-n="<?= count($logLines) - $i ?>"><?= htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?></span>
|
||||
|
||||
+87
-11
@@ -70,8 +70,12 @@ if ($activeTab === 'nginx_config') {
|
||||
|
||||
$isAdmin = true; $bodyClass = 'admin-body';
|
||||
$extraCss = ['/assets/css/system.css'];
|
||||
$extraJs = ['/assets/js/system.js'];
|
||||
// HTMX loaded once in footer; status collapse + copy via inline JS
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
|
||||
// Restore collapsed state from cookie
|
||||
$collapsed = $_COOKIE['sys_collapsed'] ?? null;
|
||||
$statusInitiallyCollapsed = $collapsed === '1';
|
||||
?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
@@ -103,11 +107,14 @@ require_once APP_ROOT . '/templates/head.php';
|
||||
<?php endif; ?>
|
||||
</h2>
|
||||
<button id="sys-status-toggle" class="sys-status-toggle"
|
||||
aria-expanded="true" aria-controls="sys-status-body"
|
||||
type="button">▲ Réduire</button>
|
||||
aria-expanded="<?= $statusInitiallyCollapsed ? 'false' : 'true' ?>" aria-controls="sys-status-body"
|
||||
type="button"
|
||||
onclick="var b=document.getElementById('sys-status-body');var c=b.hidden;b.hidden=!c;this.setAttribute('aria-expanded',c);this.textContent=c?'▲ Réduire':'▼ Développer';document.cookie='sys_collapsed='+(!c)+';path=/;max-age=31536000';return false">
|
||||
<?= $statusInitiallyCollapsed ? '▼ Développer' : '▲ Réduire' ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="sys-status-body">
|
||||
<div id="sys-status-body"<?= $statusInitiallyCollapsed ? ' hidden' : '' ?>>
|
||||
<div class="srv-grid">
|
||||
<?php foreach ($checks as $check): ?>
|
||||
<?php $st = $check['status'] ?? 'unknown'; ?>
|
||||
@@ -154,6 +161,11 @@ require_once APP_ROOT . '/templates/head.php';
|
||||
<?php foreach (SystemController::LOG_FILES as $key => $def): ?>
|
||||
<a href="?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
||||
class="sys-tab <?= $activeTab === $key ? 'active' : '' ?>"
|
||||
hx-get="/admin/system-fragment.php?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
||||
hx-target="#sys-tab-panel"
|
||||
hx-push-url="?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#sys-tab-panel"
|
||||
data-tab="<?= htmlspecialchars($key) ?>"
|
||||
<?= $activeTab === $key ? 'aria-current="page"' : '' ?>>
|
||||
<?= htmlspecialchars($def['label']) ?>
|
||||
@@ -161,14 +173,17 @@ require_once APP_ROOT . '/templates/head.php';
|
||||
<?php endforeach; ?>
|
||||
<a href="?tab=nginx_config"
|
||||
class="sys-tab <?= $activeTab === 'nginx_config' ? 'active' : '' ?>"
|
||||
hx-get="/admin/system-fragment.php?tab=nginx_config"
|
||||
hx-target="#sys-tab-panel"
|
||||
hx-push-url="?tab=nginx_config"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#sys-tab-panel"
|
||||
data-tab="nginx_config"
|
||||
<?= $activeTab === 'nginx_config' ? 'aria-current="page"' : '' ?>>nginx — config</a>
|
||||
</nav>
|
||||
|
||||
<!-- Tab panel — content swapped by fetch(); data-* attrs drive JS state -->
|
||||
<div id="sys-tab-panel"
|
||||
data-tab="<?= htmlspecialchars($activeTab) ?>"
|
||||
data-n="<?= $selectedN ?>">
|
||||
<!-- Tab panel — content swapped by HTMX -->
|
||||
<div id="sys-tab-panel">
|
||||
|
||||
<?php if ($activeTab === 'nginx_config'): ?>
|
||||
<!-- ════════════════════════════════════════════════════════════════════
|
||||
@@ -205,7 +220,8 @@ require_once APP_ROOT . '/templates/head.php';
|
||||
|
||||
<?php else: ?>
|
||||
<div class="log-output" id="log-output" role="region" aria-label="Configuration nginx">
|
||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier la configuration">
|
||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier la configuration"
|
||||
onclick="copyLogContent(this);return false">
|
||||
Copier
|
||||
</button>
|
||||
<?php foreach ($nginxConfigLines as $i => $line): ?>
|
||||
@@ -223,13 +239,21 @@ require_once APP_ROOT . '/templates/head.php';
|
||||
<!-- Lines selector (submits via JS on change; no button needed) -->
|
||||
<div class="log-toolbar">
|
||||
<label for="lines-select">Afficher</label>
|
||||
<select id="lines-select" aria-label="Nombre de lignes">
|
||||
<form id="lines-form" hx-get="/admin/system-fragment.php"
|
||||
hx-target="#sys-tab-panel"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#sys-tab-panel"
|
||||
hx-trigger="change"
|
||||
hx-vals='{"tab":"<?= htmlspecialchars($activeTab) ?>"}'>
|
||||
<label for="lines-select">Afficher</label>
|
||||
<select id="lines-select" name="n" aria-label="Nombre de lignes">
|
||||
<?php foreach (SystemController::ALLOWED_LINES as $n): ?>
|
||||
<option value="<?= $n ?>" <?= $n === $selectedN ? 'selected' : '' ?>>
|
||||
<?= $n ?> dernières lignes
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</form>
|
||||
<?php if ($logLines !== null && count($logLines) > 0): ?>
|
||||
<span class="log-count-badge"><?= count($logLines) ?> ligne(s)</span>
|
||||
<?php endif; ?>
|
||||
@@ -262,7 +286,8 @@ require_once APP_ROOT . '/templates/head.php';
|
||||
|
||||
<?php else: ?>
|
||||
<div class="log-output" id="log-output" role="log" aria-live="off" aria-label="Contenu du journal">
|
||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier le contenu">
|
||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier le contenu"
|
||||
onclick="copyLogContent(this);return false">
|
||||
Copier
|
||||
</button>
|
||||
<?php foreach ($logLines as $i => $line): ?>
|
||||
@@ -278,5 +303,56 @@ require_once APP_ROOT . '/templates/head.php';
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function copyLogContent(btn) {
|
||||
var logOut = document.querySelector('#log-output');
|
||||
if (!logOut) return;
|
||||
var text = Array.from(logOut.querySelectorAll('.log-line'))
|
||||
.map(function(el){ return el.textContent; }).join('\n');
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(function(){
|
||||
btn.textContent = '\u2713 Copi\u00e9';
|
||||
btn.classList.add('copied');
|
||||
setTimeout(function(){ btn.textContent = 'Copier'; btn.classList.remove('copied'); }, 2000);
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(text, btn);
|
||||
}
|
||||
}
|
||||
function fallbackCopy(text, btn) {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.cssText = 'position:fixed;opacity:0';
|
||||
document.body.appendChild(ta); ta.select();
|
||||
try { document.execCommand('copy'); btn.textContent = '\u2713 Copi\u00e9'; btn.classList.add('copied');
|
||||
setTimeout(function(){ btn.textContent = 'Copier'; btn.classList.remove('copied'); }, 2000);
|
||||
} catch(e) {}
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
// Update active tab class after each HTMX swap on #sys-tab-panel
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||
if (evt.detail.target && evt.detail.target.id === 'sys-tab-panel') {
|
||||
var rc = evt.detail.requestConfig;
|
||||
var tab = null;
|
||||
// Tab clicks carry ?tab=… in the path
|
||||
var qIdx = rc.path.indexOf('?');
|
||||
if (qIdx !== -1) {
|
||||
tab = new URLSearchParams(rc.path.substring(qIdx + 1)).get('tab');
|
||||
}
|
||||
// Line-count form sends tab via hx-vals in parameters
|
||||
if (!tab && rc.parameters && rc.parameters.tab) {
|
||||
tab = rc.parameters.tab;
|
||||
}
|
||||
if (tab) {
|
||||
document.querySelectorAll('.sys-tabs .sys-tab').forEach(function(a) {
|
||||
var isActive = a.getAttribute('data-tab') === tab;
|
||||
a.classList.toggle('active', isActive);
|
||||
if (isActive) a.setAttribute('aria-current', 'page');
|
||||
else a.removeAttribute('aria-current');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||
@@ -184,12 +184,12 @@
|
||||
min-height: 8rem;
|
||||
position: relative;
|
||||
}
|
||||
#sys-tab-panel.sys-panel-loading {
|
||||
#sys-tab-panel.htmx-request {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
#sys-tab-panel.sys-panel-loading::after {
|
||||
#sys-tab-panel.htmx-request::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.4"};Q.onLoad=j;Q.process=kt;Q.on=ye;Q.off=be;Q.trigger=he;Q.ajax=Rn;Q.find=u;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=Fn;Q.removeExtension=Bn;Q.logAll=V;Q.logNone=_;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:le,canAccessLocalStorage:B,findThisElement:Se,filterValues:hn,swap:$e,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:o,getExpressionVars:En,getHeaders:fn,getInputValues:cn,getInternalData:ie,getSwapSpecification:gn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:ce,makeSettleInfo:xn,oobSwap:He,querySelectorExt:ae,settleImmediately:Kt,shouldCancel:ht,triggerEvent:he,triggerErrorEvent:fe,withExtensions:Ft};const r=["get","post","put","delete","patch"];const H=r.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function m(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function o(e,t){while(e&&!t(e)){e=c(e)}return e||null}function i(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;o(t,function(e){return!!(r=i(t,ue(e),n))});if(r!=="unset"){return r}}function h(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function T(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function q(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function A(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function N(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(N(e)){const t=A(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){O(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i,"");const n=T(t);let r;if(n==="html"){r=new DocumentFragment;const i=q(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=q(t);L(r,i.body);r.title=i.title}else{const i=q('<body><template class="internal-htmx-wrapper">'+t+"</template></body>");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toStrinLine truncated
|
||||
var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=dn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true,historyRestoreAsHxRequest:true,reportValidityOfForms:false},parseInterval:null,location:location,_:null,version:"2.0.8"};Q.onLoad=V;Q.process=Ft;Q.on=xe;Q.off=be;Q.trigger=ae;Q.ajax=Ln;Q.find=f;Q.findAll=x;Q.closest=g;Q.remove=_;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=ze;Q.defineExtension=_n;Q.removeExtension=zn;Q.logAll=j;Q.logNone=$;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:se,canAccessLocalStorage:X,findThisElement:Se,filterValues:yn,swap:ze,hasAttribute:s,getAttributeValue:a,getClosestAttributeValue:ne,getClosestMatch:q,getExpressionVars:Rn,getHeaders:mn,getInputValues:dn,getInternalData:oe,getSwapSpecification:bn,getTriggerSpecs:st,getTarget:Ee,makeFragment:D,mergeObjects:le,makeSettleInfo:Sn,oobSwap:Te,querySelectorExt:ue,settleImmediately:Yt,shouldCancel:ht,triggerEvent:ae,triggerErrorEvent:fe,withExtensions:Vt};const de=["get","post","put","delete","patch"];const R=de.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function a(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function te(){return document}function y(e,t){return e.getRootNode?e.getRootNode({composed:t}):te()}function q(e,t){while(e&&!t(e)){e=u(e)}return e||null}function o(e,t,n){const r=a(t,n);const o=a(t,"hx-disinherit");var i=a(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function ne(t,n){let r=null;q(t,function(e){return!!(r=o(t,ce(e),n))});if(r!=="unset"){return r}}function h(e,t){return e instanceof Element&&e.matches(t)}function A(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function L(e){if("parseHTMLUnsafe"in Document){return Document.parseHTMLUnsafe(e)}const t=new DOMParser;return t.parseFromString(e,"text/html")}function N(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function r(e){const t=te().createElement("script");ie(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function i(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(i(e)){const t=r(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){H(e)}finally{e.remove()}}})}function D(e){const t=e.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i,"");const n=A(t);let r;if(n==="html"){r=new DocumentFragment;const i=L(e);N(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=L(t);N(r,i.body);r.title=i.title}else{const i=L('<body><template class="internal-htmx-wrapper">'+t+"</template></body>");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function re(e){if(e){e()}}function t(e,t){return Object.protoLine truncated
|
||||
@@ -1,146 +0,0 @@
|
||||
(function () {
|
||||
|
||||
// ── Status section collapse toggle ────────────────────────────────
|
||||
var toggleBtn = document.getElementById('sys-status-toggle');
|
||||
var statusBody = document.getElementById('sys-status-body');
|
||||
if (toggleBtn && statusBody) {
|
||||
toggleBtn.addEventListener('click', function () {
|
||||
var collapsed = statusBody.hidden;
|
||||
statusBody.hidden = !collapsed;
|
||||
toggleBtn.setAttribute('aria-expanded', collapsed ? 'true' : 'false');
|
||||
toggleBtn.textContent = collapsed ? '▲ Réduire' : '▼ Développer';
|
||||
try { localStorage.setItem('sys_status_collapsed', collapsed ? '0' : '1'); } catch(e) {}
|
||||
});
|
||||
try {
|
||||
if (localStorage.getItem('sys_status_collapsed') === '1') {
|
||||
statusBody.hidden = true;
|
||||
toggleBtn.setAttribute('aria-expanded', 'false');
|
||||
toggleBtn.textContent = '▼ Développer';
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// ── fetch()-based tab panel loading ───────────────────────────────
|
||||
var panel = document.getElementById('sys-tab-panel');
|
||||
var tabNav = document.querySelector('.sys-tabs');
|
||||
if (!panel || !tabNav || !window.fetch) return;
|
||||
|
||||
var currentTab = panel.dataset.tab;
|
||||
var currentN = panel.dataset.n;
|
||||
|
||||
function loadPanel(tab, n, pushState) {
|
||||
var url = new URL(window.location.href);
|
||||
var fragUrl = new URL('/admin/system-fragment.php', window.location.origin);
|
||||
fragUrl.searchParams.set('tab', tab);
|
||||
fragUrl.searchParams.set('n', n);
|
||||
|
||||
// Mark the panel as loading
|
||||
panel.classList.add('sys-panel-loading');
|
||||
|
||||
fetch(fragUrl.toString(), { credentials: 'same-origin' })
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
return r.text();
|
||||
})
|
||||
.then(function (html) {
|
||||
panel.innerHTML = html;
|
||||
panel.dataset.tab = tab;
|
||||
panel.dataset.n = n;
|
||||
currentTab = tab;
|
||||
currentN = n;
|
||||
panel.classList.remove('sys-panel-loading');
|
||||
|
||||
// Update active tab indicators
|
||||
tabNav.querySelectorAll('.sys-tab').forEach(function (a) {
|
||||
var isActive = a.dataset.tab === tab;
|
||||
a.classList.toggle('active', isActive);
|
||||
if (isActive) {
|
||||
a.setAttribute('aria-current', 'page');
|
||||
} else {
|
||||
a.removeAttribute('aria-current');
|
||||
}
|
||||
});
|
||||
|
||||
// Re-bind inner controls (lines-select, copy btn)
|
||||
bindPanelControls();
|
||||
|
||||
// Update browser URL without reloading
|
||||
if (pushState) {
|
||||
url.searchParams.set('tab', tab);
|
||||
url.searchParams.set('n', n);
|
||||
history.pushState({ tab: tab, n: n }, '', url.toString());
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
// Graceful degradation: fall back to full page load
|
||||
panel.classList.remove('sys-panel-loading');
|
||||
url.searchParams.set('tab', tab);
|
||||
url.searchParams.set('n', n);
|
||||
window.location.href = url.toString();
|
||||
});
|
||||
}
|
||||
|
||||
// Wire tab links
|
||||
tabNav.querySelectorAll('.sys-tab').forEach(function (a) {
|
||||
a.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
var tab = a.dataset.tab;
|
||||
if (tab && tab !== currentTab) {
|
||||
loadPanel(tab, currentN, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle browser back/forward
|
||||
window.addEventListener('popstate', function (e) {
|
||||
if (e.state && e.state.tab) {
|
||||
loadPanel(e.state.tab, e.state.n || 100, false);
|
||||
}
|
||||
});
|
||||
|
||||
function bindPanelControls() {
|
||||
// Lines-select
|
||||
var sel = panel.querySelector('#lines-select');
|
||||
if (sel) {
|
||||
sel.addEventListener('change', function () {
|
||||
loadPanel(currentTab, parseInt(this.value, 10), true);
|
||||
});
|
||||
}
|
||||
// Copy button
|
||||
var copyBtn = panel.querySelector('#log-copy-btn');
|
||||
var logOut = panel.querySelector('#log-output');
|
||||
if (copyBtn && logOut) {
|
||||
copyBtn.addEventListener('click', function () {
|
||||
var text = Array.from(logOut.querySelectorAll('.log-line'))
|
||||
.map(function (el) { return el.textContent; })
|
||||
.join('\n');
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(function () {
|
||||
copyBtn.textContent = '✓ Copié';
|
||||
copyBtn.classList.add('copied');
|
||||
setTimeout(function () {
|
||||
copyBtn.textContent = 'Copier';
|
||||
copyBtn.classList.remove('copied');
|
||||
}, 2000);
|
||||
}).catch(function () { fallbackCopy(text); });
|
||||
} else {
|
||||
fallbackCopy(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Bind controls already present on first load
|
||||
bindPanelControls();
|
||||
|
||||
function fallbackCopy(text) {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.cssText = 'position:fixed;opacity:0;top:0;left:0';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
try { document.execCommand('copy'); } catch (e) {}
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -4,5 +4,6 @@
|
||||
<?php if (!empty($extraJsInline)): ?>
|
||||
<script><?= $extraJsInline ?></script>
|
||||
<?php endif; ?>
|
||||
<script src="/assets/js/htmx.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user