@mostajs/schema-form 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/package.json +1 -1
- package/src/index.js +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog — @mostajs/schema-form
|
|
2
2
|
|
|
3
|
+
## 0.4.1 — 2026-10-09
|
|
4
|
+
|
|
5
|
+
**Corrigé** (audit du 09/10/2026) : `name`, `opts.id`, `opts.action`, `min`, `max`, `step`, `rows` sont échappés —
|
|
6
|
+
une valeur d'attribut ne peut plus fermer la balise. Essai T-STY-5.
|
|
7
|
+
|
|
3
8
|
## 0.4.0 — 08/10/2026
|
|
4
9
|
|
|
5
10
|
**L'habillage devient malléable** (demande du fondateur) : un style imposé en ligne ne se surchargeait qu'à
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/schema-form",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Interface de SAISIE g\u00e9n\u00e9rique pilot\u00e9e par sch\u00e9ma (z\u00e9ro-d\u00e9p) : sections + champs typ\u00e9s (text/textarea/number/boolean/select/color/date/localized/repeater), chemins imbriqu\u00e9s, multilingue AR/FR/EN. Rendu HTML (renderForm) + relecture (readValues). R\u00e9utilisable par toute app @mostajs.",
|
|
5
5
|
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
package/src/index.js
CHANGED
|
@@ -112,11 +112,11 @@ function renderControl(f, value, name, mode = 'inline', locale = 'fr') {
|
|
|
112
112
|
const ph = f.placeholder ? ` placeholder="${escapeHtml(label(f.placeholder, locale))}"` : '';
|
|
113
113
|
switch (f.type) {
|
|
114
114
|
case 'textarea':
|
|
115
|
-
return `<textarea name="${name}"${req}${ph} rows="${f.rows || 4}"${at(mode, 'textarea')}>${escapeHtml(value ?? '')}</textarea>`;
|
|
115
|
+
return `<textarea name="${escapeHtml(name)}"${req}${ph} rows="${escapeHtml(f.rows || 4)}"${at(mode, 'textarea')}>${escapeHtml(value ?? '')}</textarea>`;
|
|
116
116
|
case 'number':
|
|
117
|
-
return `<input type="number" name="${name}"${req}${ph} value="${escapeHtml(value ?? '')}"${f.min != null ? ` min="${f.min}"` : ''}${f.max != null ? ` max="${f.max}"` : ''}${f.step != null ? ` step="${f.step}"` : ''}${at(mode, 'input')}>`;
|
|
117
|
+
return `<input type="number" name="${escapeHtml(name)}"${req}${ph} value="${escapeHtml(value ?? '')}"${f.min != null ? ` min="${escapeHtml(f.min)}"` : ''}${f.max != null ? ` max="${escapeHtml(f.max)}"` : ''}${f.step != null ? ` step="${escapeHtml(f.step)}"` : ''}${at(mode, 'input')}>`;
|
|
118
118
|
case 'boolean':
|
|
119
|
-
return `<label${at(mode, 'check')}><input type="checkbox" name="${name}" value="true"${value ? ' checked' : ''}> ${escapeHtml(label(f.help, locale) || label(MOTS.oui, locale))}</label>`;
|
|
119
|
+
return `<label${at(mode, 'check')}><input type="checkbox" name="${escapeHtml(name)}" value="true"${value ? ' checked' : ''}> ${escapeHtml(label(f.help, locale) || label(MOTS.oui, locale))}</label>`;
|
|
120
120
|
// ── LE CHAMP FICHIER (0.3.0) ──────────────────────────────────────────
|
|
121
121
|
//
|
|
122
122
|
// ⚠️ `capture` ouvre l'APPAREIL PHOTO sur mobile, au lieu de la galerie. C'est ce qu'on veut
|
|
@@ -127,12 +127,12 @@ function renderControl(f, value, name, mode = 'inline', locale = 'fr') {
|
|
|
127
127
|
// décide quel fichier est sélectionné (ce serait une exfiltration). Un formulaire de
|
|
128
128
|
// modification doit donc afficher la pièce DÉJÀ envoyée à côté du champ, pas dedans.
|
|
129
129
|
case 'fichier':
|
|
130
|
-
return `<input type="file" name="${name}"${req}`
|
|
130
|
+
return `<input type="file" name="${escapeHtml(name)}"${req}`
|
|
131
131
|
+ `${f.accept ? ` accept="${escapeHtml(f.accept)}"` : ''}`
|
|
132
132
|
+ `${f.capture ? ` capture="${escapeHtml(f.capture)}"` : ''}`
|
|
133
133
|
+ `${f.multiple ? ' multiple' : ''}${at(mode, 'input')}>`;
|
|
134
134
|
case 'select':
|
|
135
|
-
return `<select name="${name}"${req}${at(mode, 'input')}>${(f.options || []).map((o) => `<option value="${escapeHtml(o.value)}"${String(value) === String(o.value) ? ' selected' : ''}>${escapeHtml(label(o.label ?? o.value, locale))}</option>`).join('')}</select>`;
|
|
135
|
+
return `<select name="${escapeHtml(name)}"${req}${at(mode, 'input')}>${(f.options || []).map((o) => `<option value="${escapeHtml(o.value)}"${String(value) === String(o.value) ? ' selected' : ''}>${escapeHtml(label(o.label ?? o.value, locale))}</option>`).join('')}</select>`;
|
|
136
136
|
case 'montant': {
|
|
137
137
|
// Un texte, PAS un `number` : le contrôle numérique du navigateur rend un flottant, et un
|
|
138
138
|
// flottant n'est pas un montant. La conversion en unités mineures se fait à la relecture,
|
|
@@ -140,17 +140,17 @@ function renderControl(f, value, name, mode = 'inline', locale = 'fr') {
|
|
|
140
140
|
const dec = f.decimales ?? 2;
|
|
141
141
|
const aff = value === undefined || value === null || value === '' ? '' : affichageMontant(value, dec);
|
|
142
142
|
return `<div${at(mode, 'montant')}>
|
|
143
|
-
<input type="text" inputmode="decimal" name="${name}"${req} value="${escapeHtml(aff)}" placeholder="${escapeHtml(label(f.placeholder, locale) || (dec ? '0,00' : '0'))}"${at(mode, 'input')}>
|
|
143
|
+
<input type="text" inputmode="decimal" name="${escapeHtml(name)}"${req} value="${escapeHtml(aff)}" placeholder="${escapeHtml(label(f.placeholder, locale) || (dec ? '0,00' : '0'))}"${at(mode, 'input')}>
|
|
144
144
|
${f.devise ? `<span${at(mode, 'devise')}>${escapeHtml(f.devise)}</span>` : ''}</div>`;
|
|
145
145
|
}
|
|
146
146
|
case 'color':
|
|
147
|
-
return `<input type="color" name="${name}" value="${escapeHtml(value || '#000000')}"${at(mode, 'couleur')}>`;
|
|
147
|
+
return `<input type="color" name="${escapeHtml(name)}" value="${escapeHtml(value || '#000000')}"${at(mode, 'couleur')}>`;
|
|
148
148
|
case 'date':
|
|
149
|
-
return `<input type="date" name="${name}"${req} value="${escapeHtml(value ?? '')}"${at(mode, 'input')}>`;
|
|
149
|
+
return `<input type="date" name="${escapeHtml(name)}"${req} value="${escapeHtml(value ?? '')}"${at(mode, 'input')}>`;
|
|
150
150
|
case 'localized': {
|
|
151
151
|
const locs = f.locales || LOCALES;
|
|
152
152
|
return `<div${at(mode, 'localized')}>${locs.map((lc) =>
|
|
153
|
-
`<div${at(mode, 'loc')}><span${at(mode, 'loc-tag', 'locTag')}>${lc}</span><input name="${name}.${lc}" dir="${lc === 'ar' ? 'rtl' : 'ltr'}" value="${escapeHtml((value && value[lc]) ?? '')}"${at(mode, 'input')}></div>`).join('')}</div>`;
|
|
153
|
+
`<div${at(mode, 'loc')}><span${at(mode, 'loc-tag', 'locTag')}>${lc}</span><input name="${escapeHtml(`${name}.${lc}`)}" dir="${lc === 'ar' ? 'rtl' : 'ltr'}" value="${escapeHtml((value && value[lc]) ?? '')}"${at(mode, 'input')}></div>`).join('')}</div>`;
|
|
154
154
|
}
|
|
155
155
|
case 'repeater': {
|
|
156
156
|
const arr = Array.isArray(value) ? value : [];
|
|
@@ -159,10 +159,10 @@ function renderControl(f, value, name, mode = 'inline', locale = 'fr') {
|
|
|
159
159
|
// rien à cloner, et une liste qu'on ne peut pas allonger n'est pas une liste — c'est un
|
|
160
160
|
// formulaire figé qui en a l'air.
|
|
161
161
|
const gabarit = `<template class="sf-tpl">${renderRepeaterItem(f, {}, `${name}[__i__]`, 0, mode, locale)}</template>`;
|
|
162
|
-
return `<div class="sf-repeater" data-name="${name}">${items}${gabarit}<button type="button"${at(mode, 'add')}>${escapeHtml(label(f.addLabel, locale) || label(MOTS.ajouter, locale))}</button></div>`;
|
|
162
|
+
return `<div class="sf-repeater" data-name="${escapeHtml(name)}">${items}${gabarit}<button type="button"${at(mode, 'add')}>${escapeHtml(label(f.addLabel, locale) || label(MOTS.ajouter, locale))}</button></div>`;
|
|
163
163
|
}
|
|
164
164
|
default: // text
|
|
165
|
-
return `<input type="text" name="${name}"${req}${ph} value="${escapeHtml(value ?? '')}"${at(mode, 'input')}>`;
|
|
165
|
+
return `<input type="text" name="${escapeHtml(name)}"${req}${ph} value="${escapeHtml(value ?? '')}"${at(mode, 'input')}>`;
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
function renderRepeaterItem(f, item, prefix, i, mode = 'inline', locale = 'fr') {
|
|
@@ -248,7 +248,7 @@ export function renderForm(schema, values = {}, opts = {}) {
|
|
|
248
248
|
// schéma — y compris DANS un répéteur — l'attribut est posé.
|
|
249
249
|
const aUnFichier = JSON.stringify(schema).includes('"fichier"');
|
|
250
250
|
const classe = `sf-form${opts.classe ? ` ${escapeHtml(opts.classe)}` : ''}`;
|
|
251
|
-
return `<form class="${classe}"${opts.id ? ` id="${opts.id}"` : ''}${opts.action ? ` method="post" action="${opts.action}"` : ''}${aUnFichier ? ' enctype="multipart/form-data"' : ''}${locale === 'ar' ? ' dir="rtl"' : ''}>
|
|
251
|
+
return `<form class="${classe}"${opts.id ? ` id="${escapeHtml(opts.id)}"` : ''}${opts.action ? ` method="post" action="${escapeHtml(opts.action)}"` : ''}${aUnFichier ? ' enctype="multipart/form-data"' : ''}${locale === 'ar' ? ' dir="rtl"' : ''}>
|
|
252
252
|
${schema.title ? `<h2${at(mode, 'title')}>${escapeHtml(label(schema.title, locale))}</h2>` : ''}${body}${submit}</form>`;
|
|
253
253
|
}
|
|
254
254
|
|