Files
fitme3070.pt/includes/footer.php
T
andreandClaude Opus 5.5 fcf86b60ea perf(backoffice): Summernote só nas páginas com editor
Os ~100 KB do editor e da tradução eram carregados em todas as páginas do
backoffice. Agora o footer só os pede quando existe um textarea.js-wysiwyg.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:43:42 +01:00

110 lines
5.1 KiB
PHP

<?php
// Shared admin shell — close. Closes the main/content containers opened by header.php.
?>
</div><!-- /.p-3 / .p-md-4 -->
</main>
<?= script_tag('assets/vendor/jquery/jquery-3.7.1.min.js') ?>
<?= script_tag('assets/vendor/bootstrap5/js/bootstrap.bundle.min.js') ?>
<?= script_tag('assets/js/password-toggle.js') ?>
<?php // Summernote (~100 KB) só nas páginas que têm um editor: carregado a pedido, abaixo. ?>
<script>window.SUMMERNOTE_SRC = [<?= json_encode(_asset_versioned('assets/vendor/summernote/summernote-bs5.min.js')) ?>, <?= json_encode(_asset_versioned('assets/vendor/summernote/lang/summernote-pt-PT.min.js')) ?>];</script>
<script>
// Attach Summernote to any <textarea class="js-wysiwyg">. The editor keeps the
// underlying textarea in sync, so normal form POSTs submit the generated HTML.
// Inline image upload is intentionally omitted (base64 would bloat the row) —
// modules that need images use their own file-upload fields.
function initWysiwyg() {
if (!window.jQuery || !jQuery.fn || !jQuery.fn.summernote) return;
jQuery('textarea.js-wysiwyg').each(function () {
var $ta = jQuery(this);
$ta.summernote({
lang: 'pt-PT',
height: parseInt($ta.data('wysiwyg-height'), 10) || 240,
disableDragAndDrop: true,
toolbar: [
['style', ['style']],
['font', ['bold', 'italic', 'underline', 'clear']],
['para', ['ul', 'ol', 'paragraph']],
['insert', ['link']],
['view', ['codeview', 'fullscreen']]
],
styleTags: ['p', 'h2', 'h3', 'h4', 'blockquote'],
popover: { image: [], air: [] }
});
});
}
document.addEventListener('DOMContentLoaded', function () {
if (!document.querySelector('textarea.js-wysiwyg')) return;
// Carrega o editor e a tradução por ordem, só aqui.
(function next(i) {
if (i >= window.SUMMERNOTE_SRC.length) return initWysiwyg();
var s = document.createElement('script');
s.src = window.SUMMERNOTE_SRC[i];
s.onload = function () { next(i + 1); };
document.body.appendChild(s);
})(0);
});
// Tema: claro / escuro / automático (o <head> já aplicou o guardado).
(function () {
var root = document.documentElement, btn = document.getElementById('themeToggle');
var mq = window.matchMedia('(prefers-color-scheme: dark)');
var icons = { light: 'bi-sun', dark: 'bi-moon-stars', auto: 'bi-circle-half' };
var names = { light: 'claro', dark: 'escuro', auto: 'automático' };
function apply(pref) {
var dark = pref === 'dark' || (pref === 'auto' && mq.matches);
root.setAttribute('data-bs-theme', dark ? 'dark' : 'light');
root.setAttribute('data-theme-pref', pref);
if (btn) {
btn.querySelector('i').className = 'bi ' + icons[pref];
btn.setAttribute('aria-label', 'Tema: ' + names[pref]);
}
document.querySelectorAll('[data-theme-set]').forEach(function (b) {
var on = b.getAttribute('data-theme-set') === pref;
b.classList.toggle('active', on);
b.setAttribute('aria-pressed', on ? 'true' : 'false');
});
}
var pref = root.getAttribute('data-theme-pref') || 'auto';
apply(pref);
document.querySelectorAll('[data-theme-set]').forEach(function (b) {
b.addEventListener('click', function () {
pref = b.getAttribute('data-theme-set');
try { localStorage.setItem('bo-theme', pref); } catch (e) {}
apply(pref);
});
});
// O sistema mudou de tema: só conta em «automático».
mq.addEventListener('change', function () { if (pref === 'auto') apply('auto'); });
})();
// Mobile off-canvas sidebar: hamburger toggles .sidebar.open + the backdrop;
// tapping the backdrop, a nav link, Escape, or resizing to desktop closes it.
(function () {
var sb = document.getElementById('sidebar');
var tg = document.getElementById('menuToggle');
var bd = document.getElementById('sidebarBackdrop');
if (!sb || !tg) return;
var mq = window.matchMedia('(max-width: 991.98px)');
function open() { sb.classList.add('open'); if (bd) bd.classList.add('show'); document.body.style.overflow = 'hidden'; tg.setAttribute('aria-expanded', 'true'); }
function close() { sb.classList.remove('open'); if (bd) bd.classList.remove('show'); document.body.style.overflow = ''; tg.setAttribute('aria-expanded', 'false'); }
tg.addEventListener('click', function () { sb.classList.contains('open') ? close() : open(); });
if (bd) bd.addEventListener('click', close);
sb.querySelectorAll('.nav-link').forEach(function (a) {
a.addEventListener('click', function () { if (mq.matches) close(); });
});
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
window.addEventListener('resize', function () { if (!mq.matches) close(); });
})();
</script>
<style>
/* Console footer — subtle brand bar */
main:after {
content: ''; display: block; height: 4px;
background: linear-gradient(90deg, var(--bo-accent, #C2D500), transparent);
margin-top: 2rem;
}
</style>
</body>
</html>