Refactoring : sécurité (XSS), découpage en modules inc/* et js/admin/*, IDs résolus par slug, perf (caches, cron Gravatar, assets auto-hébergés), tests
This commit is contained in:
299
js/admin/admin-post-edit.js
Normal file
299
js/admin/admin-post-edit.js
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Page d'édition de post (post.php / post-new.php) : onglets FR/EN du corps,
|
||||
* réordonnancement des metaboxes, groupement des axes, visibilité
|
||||
* conditionnelle des boxes Pods, popovers d'aide, fixes TinyMCE/Gutenberg.
|
||||
* Dépend de admin-base.js (window.ThalimAdmin) — enqueue conditionnel.
|
||||
*/
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
var TA = window.ThalimAdmin;
|
||||
var CONFIG = TA.CONFIG;
|
||||
var safeRun = TA.safeRun;
|
||||
|
||||
// Phase 1: insert the tab bar and relocate #pods-meta-body-en.
|
||||
// The DOM move breaks TinyMCE's iframe (browsers reset iframe content on detach),
|
||||
// so we leave the container visible here and let Pods/TinyMCE initialise normally.
|
||||
// The broken iframe is repaired by reinitEditor() on first EN tab open.
|
||||
function setupBodyTabsDom() {
|
||||
var nativeEditor = document.getElementById('postdivrich') || document.getElementById('postdiv');
|
||||
var bodyEnBox = document.querySelector(CONFIG.boxes.bodyEn);
|
||||
if (!nativeEditor || !bodyEnBox) return;
|
||||
|
||||
var tabBar = document.createElement('div');
|
||||
tabBar.className = 'body-lang-tabs';
|
||||
tabBar.innerHTML =
|
||||
'<button type="button" class="body-lang-tab is-active" data-panel="fr">Français</button>' +
|
||||
'<button type="button" class="body-lang-tab" data-panel="en">English</button>';
|
||||
nativeEditor.parentNode.insertBefore(tabBar, nativeEditor);
|
||||
|
||||
// Move EN metabox to sit right after the native editor for correct visual layout.
|
||||
// Do NOT hide it yet — Pods must init TinyMCE with the container visible so the
|
||||
// iframe can measure its dimensions. Page is still opacity:0 so no flash.
|
||||
nativeEditor.parentNode.insertBefore(bodyEnBox, nativeEditor.nextSibling);
|
||||
}
|
||||
|
||||
// Phase 2: wire tab click handlers — runs at t=100ms after metabox reordering.
|
||||
function initBodyLanguageTabs() {
|
||||
var nativeEditor = document.getElementById('postdivrich') || document.getElementById('postdiv');
|
||||
var bodyEnBox = document.querySelector(CONFIG.boxes.bodyEn);
|
||||
var tabBar = document.querySelector('.body-lang-tabs');
|
||||
if (!nativeEditor || !bodyEnBox || !tabBar) {
|
||||
// body_en not available (e.g. contributor role) — still force visual mode on main editor
|
||||
if (nativeEditor) TA.ensureVisualMode('content');
|
||||
return;
|
||||
}
|
||||
|
||||
var enEditorId = CONFIG.editors.bodyEn;
|
||||
var enTmceReady = false;
|
||||
|
||||
// Hide EN panel — page is still opacity:0, user won't see the switch
|
||||
bodyEnBox.style.display = 'none';
|
||||
|
||||
tabBar.querySelectorAll('.body-lang-tab').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
tabBar.querySelectorAll('.body-lang-tab').forEach(function(b) {
|
||||
b.classList.remove('is-active');
|
||||
});
|
||||
btn.classList.add('is-active');
|
||||
|
||||
var revealedPanel;
|
||||
|
||||
if (btn.dataset.panel === 'fr') {
|
||||
bodyEnBox.style.display = 'none';
|
||||
nativeEditor.style.opacity = '0';
|
||||
nativeEditor.style.display = '';
|
||||
revealedPanel = nativeEditor;
|
||||
} else {
|
||||
nativeEditor.style.display = 'none';
|
||||
bodyEnBox.style.opacity = '0';
|
||||
bodyEnBox.style.display = 'block';
|
||||
revealedPanel = bodyEnBox;
|
||||
|
||||
if (!enTmceReady) {
|
||||
enTmceReady = true;
|
||||
// Reinit while container is visible so TinyMCE can measure dimensions
|
||||
TA.reinitEditor(enEditorId);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify TinyMCE to reflow, then fade in once layout is correct
|
||||
setTimeout(function() {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
requestAnimationFrame(function() {
|
||||
requestAnimationFrame(function() {
|
||||
revealedPanel.style.opacity = '';
|
||||
});
|
||||
});
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
|
||||
// Ensure both editors start in Visual (not Code) mode
|
||||
TA.ensureVisualMode('content');
|
||||
TA.ensureVisualMode(enEditorId);
|
||||
}
|
||||
|
||||
function groupAxesCheckboxes() {
|
||||
if (!window.thalimAxesGroups || !thalimAxesGroups.length) return;
|
||||
|
||||
var row = document.querySelector('.' + CONFIG.rows.axes);
|
||||
if (!row) return;
|
||||
|
||||
var list = row.querySelector('ul');
|
||||
if (!list) return;
|
||||
|
||||
// Already grouped — nothing to do
|
||||
if (list.querySelector('.axes-group-label')) return;
|
||||
|
||||
// Map existing <li> by checkbox value; preserve "add new" button
|
||||
var liMap = {};
|
||||
var addNewItem = null;
|
||||
list.querySelectorAll('li').forEach(function(li) {
|
||||
if (li.classList.contains('pods-pick-add-new')) { addNewItem = li; return; }
|
||||
var cb = li.querySelector('input[type="checkbox"]');
|
||||
if (cb) liMap[cb.value] = li;
|
||||
});
|
||||
|
||||
// Rebuild list in group order
|
||||
list.innerHTML = '';
|
||||
thalimAxesGroups.forEach(function(group) {
|
||||
var labelLi = document.createElement('li');
|
||||
labelLi.className = 'axes-group-label';
|
||||
labelLi.textContent = group.label;
|
||||
list.appendChild(labelLi);
|
||||
group.terms.forEach(function(term) {
|
||||
var li = liMap[String(term.id)];
|
||||
if (li) list.appendChild(li);
|
||||
});
|
||||
});
|
||||
|
||||
if (addNewItem) list.appendChild(addNewItem);
|
||||
}
|
||||
|
||||
var REF_BIB_EDITOR_ID = CONFIG.editors.refBib;
|
||||
var refBibReinited = false;
|
||||
|
||||
// Reinit the référence bibliographique TinyMCE editor.
|
||||
// Called at page load (if the field is already visible) and by the
|
||||
// MutationObserver (when the field becomes visible after a category change).
|
||||
function initRefBibEditor() {
|
||||
if (refBibReinited) return;
|
||||
var row = document.querySelector('.' + CONFIG.rows.refBib);
|
||||
if (!row || row.style.display === 'none') return;
|
||||
refBibReinited = true;
|
||||
TA.reinitEditor(REF_BIB_EDITOR_ID);
|
||||
TA.ensureVisualMode(REF_BIB_EDITOR_ID);
|
||||
}
|
||||
|
||||
function initAxesGroupObserver() {
|
||||
// Pods shows/hides conditional rows by removing inline style="display:none"
|
||||
// Watch the entire Pods meta form for style changes on the axes row
|
||||
var podsForm = document.querySelector('.pods-pick-values, ' + CONFIG.boxes.champsContextuels + ', form#post');
|
||||
if (!podsForm) podsForm = document.body;
|
||||
|
||||
var observer = new MutationObserver(function(mutations) {
|
||||
for (var i = 0; i < mutations.length; i++) {
|
||||
var target = mutations[i].target;
|
||||
if (target.classList && target.classList.contains(CONFIG.rows.axes)) {
|
||||
if (target.style.display !== 'none') {
|
||||
setTimeout(groupAxesCheckboxes, 50);
|
||||
}
|
||||
}
|
||||
// Reinit TinyMCE on the référence bibliographique field when its
|
||||
// row becomes visible — Pods hides it with display:none which breaks
|
||||
// the TinyMCE iframe. Only reinit once per page load.
|
||||
if (!refBibReinited && target.classList &&
|
||||
target.classList.contains(CONFIG.rows.refBib)) {
|
||||
if (target.style.display !== 'none') {
|
||||
setTimeout(initRefBibEditor, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(podsForm, { attributes: true, attributeFilter: ['style'], subtree: true });
|
||||
}
|
||||
|
||||
// Gutenberg's Popover component closes on outside click via focusout detection.
|
||||
// But if focus never enters the popover, focusout never fires and clicking outside
|
||||
// does nothing. Fix: focus the popover container as soon as it appears in the DOM.
|
||||
function initDatePickerPopoverFix() {
|
||||
var observer = new MutationObserver(function(mutations) {
|
||||
for (var i = 0; i < mutations.length; i++) {
|
||||
var added = mutations[i].addedNodes;
|
||||
for (var j = 0; j < added.length; j++) {
|
||||
var node = added[j];
|
||||
if (node.nodeType !== 1) continue;
|
||||
var content = node.classList.contains('components-popover__content')
|
||||
? node
|
||||
: node.querySelector && node.querySelector('.components-popover__content');
|
||||
if (content) {
|
||||
var c = content;
|
||||
requestAnimationFrame(function() {
|
||||
if (!c.hasAttribute('tabindex')) c.setAttribute('tabindex', '-1');
|
||||
c.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
function updateMembresGridSeparator() {
|
||||
var sep = document.querySelector(CONFIG.boxes.membres + ' .membres-grid-separator');
|
||||
if (!sep) return;
|
||||
var autreRows = document.querySelectorAll(CONFIG.boxes.membres + ' [class*="pods-form-ui-row-name-autre-"]');
|
||||
var anyVisible = Array.from(autreRows).some(function(row) {
|
||||
return row.style.display !== 'none';
|
||||
});
|
||||
sep.style.display = anyVisible ? '' : 'none';
|
||||
}
|
||||
|
||||
function initPostEditPage() {
|
||||
// Disable category options (CSS handles the color)
|
||||
const categorieSelect = document.querySelector(CONFIG.categorySelect);
|
||||
if (categorieSelect) {
|
||||
categorieSelect.querySelectorAll('option').forEach(option => {
|
||||
if (CONFIG.disabledCategoryIds.includes(option.value)) {
|
||||
option.disabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Reorder meta boxes
|
||||
const sideSortables = document.querySelector('#side-sortables');
|
||||
if (sideSortables) {
|
||||
const typeDannonce = document.querySelector(CONFIG.boxes.typeDannonce);
|
||||
const affichageAccueil = document.querySelector(CONFIG.boxes.affichageAccueil);
|
||||
const thematique = document.querySelector(CONFIG.boxes.thematique);
|
||||
if (typeDannonce) sideSortables.prepend(typeDannonce);
|
||||
if (affichageAccueil) sideSortables.appendChild(affichageAccueil);
|
||||
if (thematique) sideSortables.appendChild(thematique);
|
||||
}
|
||||
|
||||
const submitDiv = document.querySelector('#submitdiv');
|
||||
if (submitDiv && submitDiv.parentNode) {
|
||||
submitDiv.parentNode.appendChild(submitDiv);
|
||||
}
|
||||
|
||||
const champsContextuels = document.querySelector(CONFIG.boxes.champsContextuels);
|
||||
if (champsContextuels && champsContextuels.parentNode) {
|
||||
champsContextuels.parentNode.prepend(champsContextuels);
|
||||
}
|
||||
|
||||
// Chaque sous-module est isolé : une exception dans l'un
|
||||
// n'empêche pas les suivants de s'initialiser.
|
||||
safeRun('initBodyLanguageTabs', initBodyLanguageTabs);
|
||||
safeRun('initRefBibEditor', initRefBibEditor);
|
||||
safeRun('groupAxesCheckboxes', groupAxesCheckboxes);
|
||||
safeRun('initAxesGroupObserver', initAxesGroupObserver);
|
||||
safeRun('updatePostboxVisibility', TA.updatePostboxVisibility);
|
||||
safeRun('initDatePickerPopoverFix', initDatePickerPopoverFix);
|
||||
safeRun('initInfoPopovers', TA.initInfoPopovers);
|
||||
|
||||
// Place #pods-meta-documents-joints in #normal-sortables, right after
|
||||
// #pods-meta-champs-contextuels. This keeps it out of #post-body-content
|
||||
// (the body editor section) regardless of whether champsContextuels is
|
||||
// currently visible. When champsContextuels is hidden it takes no space,
|
||||
// so documentsJoints simply appears first in #normal-sortables.
|
||||
const documentsJoints = document.querySelector(CONFIG.boxes.documentsJoints);
|
||||
if (documentsJoints) {
|
||||
if (champsContextuels && champsContextuels.parentNode) {
|
||||
champsContextuels.parentNode.insertBefore(documentsJoints, champsContextuels.nextSibling);
|
||||
} else {
|
||||
const normalSortables = document.querySelector('#normal-sortables');
|
||||
if (normalSortables) normalSortables.prepend(documentsJoints);
|
||||
}
|
||||
}
|
||||
|
||||
// Inject separator row for the Membres grid layout
|
||||
var membresTbody = document.querySelector(CONFIG.boxes.membres + ' .form-table tbody');
|
||||
if (membresTbody && !membresTbody.querySelector('.membres-grid-separator')) {
|
||||
var sep = document.createElement('tr');
|
||||
sep.className = 'membres-grid-separator';
|
||||
membresTbody.appendChild(sep);
|
||||
}
|
||||
updateMembresGridSeparator();
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
if (!TA.isPostEditPage()) return;
|
||||
|
||||
safeRun('setupBodyTabsDom', setupBodyTabsDom);
|
||||
|
||||
setTimeout(function() {
|
||||
safeRun('initPostEditPage', initPostEditPage);
|
||||
TA.markReady();
|
||||
}, 100);
|
||||
|
||||
$(CONFIG.categorySelect).change(function() {
|
||||
setTimeout(function() {
|
||||
safeRun('updatePostboxVisibility', TA.updatePostboxVisibility);
|
||||
safeRun('updateMembresGridSeparator', updateMembresGridSeparator);
|
||||
}, 10);
|
||||
});
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
Reference in New Issue
Block a user