// 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 ''; } 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 ''; }).join(""); var selectHtml = ''; var selectWrapClass = forgeHtmlEscape(("select is-fullwidth " + bulmaClasses).trim()); return '
' + selectHtml + '
'; } 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 '
' + forgeHtmlEscape(content) + '' + childHtml + '
'; } 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()) + ""; }).join(""); return "" + cellsHtml + ""; }).join(""); return '' + htmlRows + '
'; } 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(); // data: URI pré-calculée à l'export (voir gameData.icon_data_uris, // publish/build_scorm_package.py::_build_icon_data_uris) — jamais un // chemin de fichier, même relatif : Firefox traite chaque ressource // file:// comme une origine opaque distincte, et mask-image y déclenche // un fetch en mode CORS quel que soit le chemin ("Blocage d'une requête // multiorigine... la requête CORS n'utilise pas HTTP", signalé par un // utilisateur MALGRÉ un chemin relatif par ailleurs correct). Repli sur // un chemin relatif si jamais l'icône n'a pas été pré-encodée (ne // devrait pas arriver : _collect_icon_slugs scanne tous les widgets // "icone", posés directement ou nichés dans un élément de jeu // réutilisable) — au moins un lien exploitable plutôt qu'un vide. var iconUrl = (gameData.icon_data_uris && gameData.icon_data_uris[iconSlug]) || ("icons/" + iconSlug + ".svg"); var style = forgeStyleString(el, parentFlexDirection, el.widget) + " --icon-url:url('" + iconUrl + "');"; // aria-hidden (RGAA/WCAG 1.1.1) : une icône seule (pas de champ de // description dédié dans l'éditeur) est traitée comme purement // décorative — jamais annoncée par un lecteur d'écran, comme le nom du // slug FontAwesome ("star") n'aurait aucun sens lu à voix haute. if (!("aria-hidden" in attrs)) attrs["aria-hidden"] = "true"; return ""; } 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 '
Onglets — ajoute au moins un onglet dans ses réglages.
'; } 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( '
  • ' + forgeHtmlEscape(String(label)) + '
  • ' ); var hiddenStyle = active ? "" : ' style="display:none;"'; panels.push('
    ' + tabHtml + '
    '); }); var tabsClass = forgeHtmlEscape(("tabs " + tabsExtraClasses).trim()); return ( '
    ' + '
    ' + '
    ' + panels.join("") + '
    ' + '
    ' ); } // 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; // alt (RGAA/WCAG 1.1.1) : voir render-element.js pour le même défaut // décoratif sur le widget "image" générique. if (!("alt" in attrs)) attrs.alt = ""; var style = forgeStyleString(el, parentFlexDirection, el.widget); return ""; } 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]; }); }