Files
Forge-Engine/static/js/play/offline/render-special-widgets.js
T
williamandClaude Sonnet 5 7bc4c89eaa
Build and deploy / test-python (push) Successful in 13m15s
Build and deploy / test-js (push) Successful in 1m4s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Export Web/SCORM : corrige les URLs absolues cassées en local (file://)
Bug signalé : icône disparue et texte suivant du dialogue absent après
export. render_icone.py (et son miroir JS, forgeRenderIcone) bakaient
une URL "/static/icons/..." en dur, correcte en ligne (racine du
serveur Flask) mais bloquée par CORS une fois ouverte en local
(file:///static/...). Même souci pour les fichiers envoyés par le
créateur (/game/<slug>/uploads/..., jamais dans static/, jamais copiés
par le paquet). Ajoute _relativize_absolute_urls (réécriture globale
post-rendu du HTML) + copie du dossier uploads/, et relativise le port
JS de l'icône pour toute régénération après action en mémoire.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 09:31:44 +02:00

150 lines
8.0 KiB
JavaScript

// Export Web/SCORM (voir le plan "port complet du runtime jouable côté
// navigateur") — étape 4 : port des widgets à "special_render" les plus
// simples (screens/rendering/render_checkbox_or_radio.py, render_select.py,
// render_fieldset.py, render_table.py, render_icone.py, render_onglets.py).
// Jauge/Superposition/Personnage/Répéteur (logique/données plus lourde)
// vivent dans des fichiers dédiés (render-jauge.js, render-repeater.js).
// Dépend de render-element.js (forgeVisibleAttrs/forgeStyleString/
// forgeAttrString/forgeApplyCtx/forgeHtmlEscape/forgeAutoId/
// forgeParentFlexDirection/forgeRenderChildren), chargé avant ce fichier.
function forgeRenderCheckboxOrRadio(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
var inputType = el.widget === "case_a_cocher" ? "checkbox" : "radio";
var attrs = forgeVisibleAttrs(el, meta, ctx);
attrs.type = inputType;
var style = forgeStyleString(el, parentFlexDirection, el.widget) + " display:flex; align-items:center; gap:8px;";
var content = forgeApplyCtx(el.content || "", ctx);
return '<label ' + forgeAttrString({ class: inputType }, style) + '><input ' + forgeAttrString(attrs, "") + '> ' + forgeHtmlEscape(content) + '</label>';
}
function forgeRenderSelect(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
var outerAttrs = forgeVisibleAttrs(el, meta, ctx);
var bulmaClasses = outerAttrs.class || "";
var selectAttrs = {}, wrapperAttrs = {};
Object.keys(outerAttrs).forEach(function (k) {
if (k === "id" || k === "data-element-id" || k === "data-anim-target") wrapperAttrs[k] = outerAttrs[k];
else if (k !== "class") selectAttrs[k] = outerAttrs[k];
});
var style = forgeStyleString(el, parentFlexDirection, el.widget) + " display:flex;";
var content = forgeApplyCtx(el.content || "", ctx);
var options = content.split("\n").map(function (o) { return o.trim(); }).filter(Boolean);
var optsHtml = options.map(function (o) {
var esc = forgeHtmlEscape(o);
return '<option value="' + esc + '">' + esc + '</option>';
}).join("");
var selectHtml = '<select ' + forgeAttrString(selectAttrs, "") + '>' + optsHtml + '</select>';
var selectWrapClass = forgeHtmlEscape(("select is-fullwidth " + bulmaClasses).trim());
return '<div ' + forgeAttrString(wrapperAttrs, style) + '><div class="' + selectWrapClass + '">' + selectHtml + '</div></div>';
}
function forgeRenderFieldset(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
var attrs = forgeVisibleAttrs(el, meta, ctx);
var style = forgeStyleString(el, parentFlexDirection, el.widget);
var content = forgeApplyCtx(el.content || "", ctx) || "Groupe de champs";
var childHtml = forgeRenderChildren(el, gameData, childrenMap, ctx);
return '<fieldset ' + forgeAttrString(attrs, style) + '><legend>' + forgeHtmlEscape(content) + '</legend>' + childHtml + '</fieldset>';
}
function forgeRenderTable(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
var attrs = forgeVisibleAttrs(el, meta, ctx);
var headerRow = ["1", "true", "on"].indexOf((el.attributes || {})._header_row) !== -1;
var content = forgeApplyCtx(el.content || "", ctx);
var style = forgeStyleString(el, parentFlexDirection, el.widget);
var rows = content.split("\n").filter(function (r) { return r.trim(); });
var htmlRows = rows.map(function (r, i) {
var cells = r.split(";");
var cellTag = (headerRow && i === 0) ? "th" : "td";
var cellsHtml = cells.map(function (c) {
return "<" + cellTag + ">" + forgeHtmlEscape(c.trim()) + "</" + cellTag + ">";
}).join("");
return "<tr>" + cellsHtml + "</tr>";
}).join("");
return '<table ' + forgeAttrString(attrs, style) + '>' + htmlRows + '</table>';
}
function forgeRenderIcone(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
// Contrairement au serveur, ne revalide pas _icon_slug contre le
// catalogue curaté (screens/widgets/fontawesome_icons.py) : une valeur
// invalide ne peut être enregistrée que depuis l'éditeur, qui la valide
// déjà à la sauvegarde — un jeu EXPORTÉ n'en produit jamais.
var rawAttrs = el.attributes || {};
var iconSlug = rawAttrs._icon_slug || "star";
var attrs = forgeVisibleAttrs(el, meta, ctx);
var existingClass = attrs.class || "";
delete attrs.class;
attrs.class = (existingClass + " icon-svg").trim();
// Chemin RELATIF (jamais "/static/...") : ce fichier ne s'exécute que
// dans le paquet exporté (window.FORGE_OFFLINE, voir render-element.js),
// ouvert en local (file://) sans racine de serveur — un chemin absolu y
// déclenche un blocage CORS ("la requête CORS n'utilise pas HTTP"),
// exactement le bug qui affectait aussi le rendu initial côté serveur
// (voir _relativize_absolute_urls dans publish/build_scorm_package.py).
var iconUrl = "static/icons/" + iconSlug + ".svg";
var style = forgeStyleString(el, parentFlexDirection, el.widget) + " --icon-url:url('" + iconUrl + "');";
return "<i " + forgeAttrString(attrs, style) + "></i>";
}
function forgeRenderOnglets(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
var tabs = (childrenMap && childrenMap[el.id]) || [];
var attrs = forgeVisibleAttrs(el, meta, ctx);
var tabsExtraClasses = attrs.class || "";
delete attrs.class;
var style = forgeStyleString(el, parentFlexDirection, el.widget) + " display:flex; flex-direction:column;";
var groupId = forgeAutoId(el);
if (!tabs.length) {
return '<div ' + forgeAttrString(attrs, style) + '><em style="opacity:.6;">Onglets — ajoute au moins un onglet dans ses réglages.</em></div>';
}
var flexDir = forgeParentFlexDirection(el);
var barItems = [], panels = [];
tabs.forEach(function (tab, i) {
var label = tab.name || ("Onglet " + (i + 1));
var active = i === 0;
var tabHtml = forgeRenderElementHtml(tab, gameData, childrenMap, ctx, flexDir);
var targetId = "tabpanel-" + tab.id;
barItems.push(
'<li class="' + (active ? "is-active" : "") + '" data-tab-btn="' + groupId + '" data-tab-target="' + targetId +
'" onclick="forgeShowTab(this)"><a>' + forgeHtmlEscape(String(label)) + '</a></li>'
);
var hiddenStyle = active ? "" : ' style="display:none;"';
panels.push('<div class="forgeTabPanel" id="' + targetId + '" data-tab-group="' + groupId + '"' + hiddenStyle + '>' + tabHtml + '</div>');
});
var tabsClass = forgeHtmlEscape(("tabs " + tabsExtraClasses).trim());
return (
'<div ' + forgeAttrString(attrs, style) + '>' +
'<div class="' + tabsClass + '"><ul>' + barItems.join("") + '</ul></div>' +
'<div class="forgeTabPanels" style="flex:1 1 auto; min-height:0;">' + panels.join("") + '</div>' +
'</div>'
);
}
// Port de render_personnage.py — lit directement gameData.personnage_animations
// (déjà résolu une fois par full_game_payload.py, à l'export) au lieu de
// re-résoudre la bibliothèque de sprites Forge/l'URL personnalisée : ce
// travail est fait UNE fois, à la génération du paquet, jamais à chaque
// rendu côté client.
function forgeRenderPersonnage(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
var attrs = forgeVisibleAttrs(el, meta, ctx);
attrs["data-personnage"] = "1";
var animations = gameData.personnage_animations && gameData.personnage_animations[String(el.id)];
var frames = animations ? (animations.idle || animations[Object.keys(animations)[0]]) : null;
var idleFrame = frames && frames.length ? frames[0] : "";
if (idleFrame) attrs.src = idleFrame;
var style = forgeStyleString(el, parentFlexDirection, el.widget);
return "<img " + forgeAttrString(attrs, style) + ">";
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
forgeRenderCheckboxOrRadio, forgeRenderSelect, forgeRenderFieldset, forgeRenderTable,
forgeRenderIcone, forgeRenderOnglets, forgeRenderPersonnage,
};
var renderMod = require('./render-element.js');
["forgeVisibleAttrs", "forgeStyleString", "forgeAttrString", "forgeApplyCtx", "forgeHtmlEscape",
"forgeAutoId", "forgeParentFlexDirection", "forgeRenderChildren", "forgeRenderElementHtml"].forEach(function (name) {
global[name] = global[name] || renderMod[name];
});
}