Répartition assistée sur le formulaire + widget compacté
- ledger-form.js : nouveau behavior figliLedgerRepartition -- pré-remplit les montants de répartition (1re ligne = HT entier, chaque ajout partage au centime avec report du reste sur les lignes suivantes), valeurs saisies manuellement ou chargées de la base « figées » (plus jamais déplacées), écart en direct dans la cellule de titre du widget (miroir exact du round(HT − somme, 2) et de la tolérance 0,01 du presave). Zéro changement PHP : #validate et node_presave() restent l'autorité. Les verrous vivent hors du DOM (survie aux re-rendus AJAX du widget) ; la détection manuel/assist repose sur le fait qu'une écriture programmatique ne déclenche pas d'événement input. - ledger-form.css : compactage du widget -- titre « Répartition » par ligne, bouton Collapse et colonne Order masqués, paragraph-top en absolu (coin haut-droit, zéro hauteur), une seule ligne par répartition « Compte [input] Montant (€) [input] » où l'input du Compte est contraint en pourcentage de son wrapper claro-autocomplete (size=60 : il débordait sur le libellé Montant), header réordonné titre / écart / menu trois-points, inputs visibles au repos (--flform-bg, atténué en mode sombre), marqueur « figé ». - install : update_8015 (features du widget vidées) puis update_8016 (collapse_edit_all rétabli à la demande, duplicate reste off) + settings de l'install fraîche synchronisés.
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
--flform-border-soft: #e8eaed;
|
||||
--flform-label: #4b5563;
|
||||
--flform-text: #1a1a1a;
|
||||
--flform-bg: #ffffff;
|
||||
--flform-bg-subtle: #f7f8fa;
|
||||
--flform-accent: #2f6f4f;
|
||||
--flform-danger: #b3261e;
|
||||
@@ -43,6 +44,7 @@ html.gin--dark-mode .figli-ledger-form {
|
||||
--flform-border-soft: #333438;
|
||||
--flform-label: #a1a5ab;
|
||||
--flform-text: #e8e9ea;
|
||||
--flform-bg: #3a3b40;
|
||||
--flform-bg-subtle: #2a2b2e;
|
||||
--flform-accent: #5fb98a;
|
||||
--flform-danger: #ff6b6b;
|
||||
@@ -212,10 +214,10 @@ html.gin--dark-mode .figli-ledger-form {
|
||||
}
|
||||
.figli-ledger-form table.field-multiple-table thead th {
|
||||
text-align: left;
|
||||
padding: 0.4rem 0.6rem 0.25rem;
|
||||
padding: 0.3rem 0.5rem 0.2rem;
|
||||
}
|
||||
.figli-ledger-form table.field-multiple-table tbody td {
|
||||
padding: 0.35rem 0.6rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
vertical-align: top;
|
||||
border-top: 1px solid var(--flform-border-soft);
|
||||
}
|
||||
@@ -224,12 +226,25 @@ html.gin--dark-mode .figli-ledger-form {
|
||||
}
|
||||
.figli-ledger-form .paragraphs-subform {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
gap: 0.5rem;
|
||||
/* Réserve le coin haut-droit (bouton Remove du paragraph-top, en
|
||||
position:absolute) : les inputs ne passent plus dessous et
|
||||
rétrécissent d'autant. */
|
||||
padding-right: 4.4rem;
|
||||
}
|
||||
.figli-ledger-form .paragraphs-subform > .js-form-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
/* Le Compte (nom, souvent long) prend une part plus large que le
|
||||
Montant (chiffre court) -- tous deux plus étroits qu'avant, la zone
|
||||
du Remove étant réservée ci-dessus. */
|
||||
.figli-ledger-form .paragraphs-subform > .js-form-wrapper.field--name-field-compte {
|
||||
flex: 1.6;
|
||||
}
|
||||
.figli-ledger-form .paragraphs-subform > .js-form-wrapper.field--name-field-montant {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Buttons: "Ajouter Répartition" / "Add another item", and the per-row
|
||||
Remove/Duplicate/Collapse actions -- all plain Drupal form-submit
|
||||
@@ -249,7 +264,7 @@ html.gin--dark-mode .figli-ledger-form {
|
||||
color: var(--flform-accent);
|
||||
}
|
||||
.figli-ledger-form .field-add-more-submit {
|
||||
margin-top: 0.4rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.figli-ledger-form .paragraphs-dropdown-toggle {
|
||||
border: none;
|
||||
@@ -287,6 +302,164 @@ html.gin--dark-mode .figli-ledger-form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ==== Répartition : compactage du widget (une ligne = Compte + Montant,
|
||||
rien d'autre) + assist (PLAN-repartition-assistee.md) ==== */
|
||||
|
||||
/* Une ligne de répartition ne doit montrer que ses deux champs. Le
|
||||
titre "Répartition" de chaque ligne est redondant (le titre du champ
|
||||
est déjà en thead), le bouton Collapse n'a pas de sens (edit_mode:
|
||||
open, les lignes sont toujours ouvertes -- et le CSS les masque de
|
||||
toute façon côté JS), et paragraph-info/paragraph-summary sont rendus
|
||||
vides pour ce bundle : chacun coûtait une ligne de bruit vertical. */
|
||||
.figli-ledger-form .field--name-field-repartition .paragraph-type,
|
||||
.figli-ledger-form .field--name-field-repartition .paragraph-info,
|
||||
.figli-ledger-form .field--name-field-repartition .paragraph-summary,
|
||||
.figli-ledger-form .field--name-field-repartition .paragraphs-icon-button-collapse {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Le "paragraph-top" ne porte plus que le menu d'actions de la ligne
|
||||
(Remove) : en ABSOLU dans le coin haut-droit de la ligne (td en
|
||||
position:relative), aligné sur le padding horizontal de la cellule --
|
||||
il n'ajoute donc AUCUNE hauteur, la ligne se réduit à ses deux
|
||||
champs. */
|
||||
.figli-ledger-form .field--name-field-repartition tr.paragraph-type--repartition > td {
|
||||
position: relative;
|
||||
/* Aucun padding vertical sur les lignes de répartition (demande
|
||||
explicite) : la hauteur de ligne vient uniquement des champs, le
|
||||
bordure-top reste comme séparateur. */
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.figli-ledger-form .field--name-field-repartition .paragraph-top {
|
||||
position: absolute;
|
||||
top: 0.15rem;
|
||||
right: 0.5rem;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
margin: 0;
|
||||
}
|
||||
.figli-ledger-form .field--name-field-repartition .paragraph-top .paragraphs-actions {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Colonne "Order" (poids des lignes) des tables multi-valeurs : l'ordre
|
||||
ne compte jamais ici (cf. handles de drag déjà masqués plus haut),
|
||||
deux champs de largeur récupérés. */
|
||||
.figli-ledger-form .field-multiple-table thead th:last-child,
|
||||
.figli-ledger-form .field-multiple-table td.delta-order {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Menu "Toggle Actions" du thead : il porte le bouton "Collapse / Edit
|
||||
all" (feature rétablie par update_8016) -- seul le mode "Drag & drop"
|
||||
y est masqué, un réordonnancement sans objet (handles masqués plus
|
||||
haut). Le menu par ligne (Remove) est hors de ce sélecteur. */
|
||||
.figli-ledger-form .field--name-field-repartition thead input[name="field_repartition_dragdrop_mode"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Barre de titre du widget : titre "Répartition" À GAUCHE, écart AU
|
||||
MILIEU, menu trois-points À DROITE (l'ordre DOM est titre, actions,
|
||||
puis l'écart injecté par JS, d'où les order). */
|
||||
.figli-ledger-form .field--name-field-repartition th.field-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.figli-ledger-form .field--name-field-repartition th.field-label h4 {
|
||||
order: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.figli-ledger-form .figli-repartition-ecart {
|
||||
order: 1;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 400;
|
||||
color: var(--flform-label);
|
||||
}
|
||||
.figli-ledger-form .field--name-field-repartition th.field-label .paragraphs-actions {
|
||||
order: 2;
|
||||
}
|
||||
.figli-ledger-form .figli-repartition-ecart-val {
|
||||
font-weight: 600;
|
||||
}
|
||||
.figli-ledger-form .figli-repartition-ecart.is-ok .figli-repartition-ecart-val {
|
||||
color: var(--flform-accent);
|
||||
}
|
||||
.figli-ledger-form .figli-repartition-ecart.is-ko .figli-repartition-ecart-val {
|
||||
color: var(--flform-danger);
|
||||
}
|
||||
|
||||
/* Une ligne par répartition : "Compte [input] Montant (€) [input]".
|
||||
Le piège : le form-item de Compte n'a PAS l'input pour enfant direct
|
||||
-- claro l'emballe dans div.claro-autocomplete (le Montant, lui, a
|
||||
l'input direct). Sans flex/min-width sur ce wrapper, l'input
|
||||
(size=60) force sa largeur native, pousse le Montant hors de la
|
||||
cellule et les deux champs disparaissent : chaque niveau doit être
|
||||
flex et compressible. */
|
||||
.figli-ledger-form .paragraphs-subform .form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.figli-ledger-form .paragraphs-subform .form-item__label {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.figli-ledger-form .paragraphs-subform .claro-autocomplete {
|
||||
display: block;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* L'input du Compte (size="60", ~420px de largeur intrinsèque) : la
|
||||
chaîne flex/min-width ne suffit pas à le contenir de façon fiable à
|
||||
tous les niveaux -- contrainte dure en pourcentage de son wrapper à
|
||||
la place : physiquement incapable de déborder sur la colonne du
|
||||
Montant, quoi que dise l'attribut size. */
|
||||
.figli-ledger-form .paragraphs-subform .claro-autocomplete input.form-element {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Inputs visibles au repos (fond opaque + bordure franche, les deux
|
||||
modes via --flform-bg) et compressibles. flex:1 s'applique : au
|
||||
Montant comme item direct du form-item, au Compte comme item du
|
||||
claro-autocomplete flex ci-dessus. */
|
||||
.figli-ledger-form .paragraphs-subform input.form-element {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
background: var(--flform-bg);
|
||||
border: 1px solid var(--flform-border);
|
||||
color: var(--flform-text);
|
||||
-webkit-text-fill-color: var(--flform-text);
|
||||
}
|
||||
|
||||
/* Marqueur "figé" (ledger-form.js) : une valeur que l'assist ne
|
||||
touchera plus -- saisie manuelle, ou chargée de la base. En item flex
|
||||
(le form-item est en ligne ci-dessus), il suit l'input ; pointillé =
|
||||
même convention que la référence bancaire, "valeur à regarder, pas à
|
||||
retaper". */
|
||||
.figli-ledger-form .figli-repartition-locked::after {
|
||||
content: "figé";
|
||||
flex: none;
|
||||
align-self: center;
|
||||
font-size: 0.64rem;
|
||||
line-height: 1;
|
||||
padding: 0.16rem 0.28rem;
|
||||
border-radius: 5px;
|
||||
border: 1px dashed var(--flform-border);
|
||||
color: var(--flform-label);
|
||||
}
|
||||
|
||||
/* A répartition row flagged by the sum-mismatch #validate error (see
|
||||
figli_compta_ledger_validate_repartition()) -- kept subtle (a red
|
||||
outline, not a solid fill) to match the same red-liseré convention
|
||||
|
||||
@@ -313,7 +313,7 @@ function _figli_compta_ledger_create_node_type_ligne_comptable() {
|
||||
// full-width row) when "Autre" is picked there. See
|
||||
// css/ledger-form.css's grid-column rules for both.
|
||||
->setComponent('field_tva', ['type' => 'number', 'weight' => 10])
|
||||
->setComponent('field_repartition', ['type' => 'paragraphs', 'weight' => 11, 'settings' => ['title' => 'Répartition', 'title_plural' => 'Répartitions', 'edit_mode' => 'open', 'add_mode' => 'button']])
|
||||
->setComponent('field_repartition', ['type' => 'paragraphs', 'weight' => 11, 'settings' => ['title' => 'Répartition', 'title_plural' => 'Répartitions', 'edit_mode' => 'open', 'add_mode' => 'button', 'features' => ['collapse_edit_all' => 'collapse_edit_all']]])
|
||||
->setComponent('field_notes', ['type' => 'string_textarea', 'weight' => 12])
|
||||
->setComponent('field_flag', ['type' => 'entity_reference_autocomplete_tags', 'weight' => 13])
|
||||
->save();
|
||||
@@ -929,3 +929,47 @@ function figli_compta_ledger_update_8014() {
|
||||
|
||||
return 'field_montant_releve placé sous Montant TTC (weights décalés).';
|
||||
}
|
||||
|
||||
/**
|
||||
* Empties the Paragraphs widget's "features" setting for
|
||||
* field_repartition: with duplicate and collapse_edit_all off, the
|
||||
* widget stops rendering the per-row Duplicate action and the thead's
|
||||
* "Collapse / Edit all" -- pure noise on a 1-3 row répartition whose
|
||||
* order never matters (see ledger-form.css's compacting rules for the
|
||||
* rest: per-row title and Collapse button are hidden in CSS, they have
|
||||
* no widget setting).
|
||||
*/
|
||||
function figli_compta_ledger_update_8015() {
|
||||
$form_display = EntityFormDisplay::load('node.ligne_comptable.default');
|
||||
if ($form_display) {
|
||||
$component = $form_display->getComponent('field_repartition');
|
||||
if ($component) {
|
||||
$component['settings']['features'] = [];
|
||||
$form_display->setComponent('field_repartition', $component);
|
||||
$form_display->save();
|
||||
}
|
||||
}
|
||||
|
||||
return 'Widget Répartition compacté (features duplicate/collapse_edit_all désactivées).';
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-enables the Paragraphs widget's collapse_edit_all feature (the
|
||||
* "Collapse / Edit all" action in the widget's title cell) -- removed
|
||||
* along with duplicate by update_8015, asked back by the associates
|
||||
* the same day. Duplicate stays off: it has no use on a 1-3 row
|
||||
* répartition whose order never matters.
|
||||
*/
|
||||
function figli_compta_ledger_update_8016() {
|
||||
$form_display = EntityFormDisplay::load('node.ligne_comptable.default');
|
||||
if ($form_display) {
|
||||
$component = $form_display->getComponent('field_repartition');
|
||||
if ($component) {
|
||||
$component['settings']['features'] = ['collapse_edit_all' => 'collapse_edit_all'];
|
||||
$form_display->setComponent('field_repartition', $component);
|
||||
$form_display->save();
|
||||
}
|
||||
}
|
||||
|
||||
return 'Bouton "Collapse / Edit all" rétabli sur le widget Répartition (duplicate reste désactivé).';
|
||||
}
|
||||
|
||||
@@ -136,4 +136,235 @@
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Répartition assistée (see PLAN-repartition-assistee.md): auto-fills
|
||||
* the répartition amounts so the associate never does the small
|
||||
* arithmetic by hand. Purely client-side pre-filling -- the server
|
||||
* stays the authority (figli_compta_ledger_validate_repartition() +
|
||||
* figli_compta_ledger_node_presave() unchanged), same philosophy as
|
||||
* the TTC live preview above: with JS off, the form behaves exactly
|
||||
* as before.
|
||||
*
|
||||
* Rules (plan §Comportement):
|
||||
* - a value loaded from the database, or typed by the user, is
|
||||
* "figée" (pinned) and never moved again;
|
||||
* - the remaining rows split (HT − somme(figées)) to the centime,
|
||||
* rounding remainders carried by the later rows, 0 when the
|
||||
* remainder is negative;
|
||||
* - triggers: row added/removed (Paragraphs AJAX re-render ->
|
||||
* behavior attach), HT change, manual amount edit;
|
||||
* - live "Réparti / Écart" line under the widget, mirroring the
|
||||
* server's round(HT − somme, 2) and its 0.01 tolerance.
|
||||
*
|
||||
* "Manual vs assist" detection rests on a DOM property: assigning
|
||||
* input.value programmatically fires no event, only a real user edit
|
||||
* fires 'input'. Locks therefore survive the widget's full AJAX
|
||||
* re-renders (every add/remove rebuilds the inputs) because they live
|
||||
* in this closure, keyed by input name (deltas are stable -- drag
|
||||
* handles are hidden, order never matters); the assisted{} memory is
|
||||
* what makes a never-assisted value (i.e. anything loaded from the
|
||||
* database) figée by construction, and keeps a stale lock harmless
|
||||
* after a row deletion reindexes the deltas.
|
||||
*/
|
||||
Drupal.behaviors.figliLedgerRepartition = {
|
||||
attach: function (context) {
|
||||
// Behaviors attach on every AJAX response with the replaced
|
||||
// fragment as context -- the form itself only once (which is when
|
||||
// the delegated listener below is set up), any later attach is a
|
||||
// Paragraphs re-render and just needs a refresh. Like the TVA
|
||||
// behavior's comment explains, context may BE the form (modal) or
|
||||
// a fragment INSIDE it (add/remove) -- collect all three cases,
|
||||
// then dedupe through once().
|
||||
var ctx = (context && (context.nodeType === 1 || context.nodeType === 9)) ? context : document;
|
||||
var forms = [];
|
||||
if (ctx.nodeType === 1 && ctx.matches('.figli-ledger-form')) {
|
||||
forms.push(ctx);
|
||||
}
|
||||
if (ctx.querySelectorAll) {
|
||||
Array.prototype.forEach.call(ctx.querySelectorAll('.figli-ledger-form'), function (f) {
|
||||
forms.push(f);
|
||||
});
|
||||
}
|
||||
if (!forms.length && ctx.nodeType === 1 && ctx.closest) {
|
||||
var ancestor = ctx.closest('.figli-ledger-form');
|
||||
if (ancestor) {
|
||||
forms.push(ancestor);
|
||||
}
|
||||
}
|
||||
forms.forEach(function (form) {
|
||||
if (once('figli-ledger-repartition', form).length) {
|
||||
form.figliLedgerRepartition = initRepartitionAssist(form);
|
||||
}
|
||||
if (form.figliLedgerRepartition) {
|
||||
form.figliLedgerRepartition.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function initRepartitionAssist(form) {
|
||||
// input.name -> true (manual edit at some point, never move again).
|
||||
var figees = {};
|
||||
// input.name -> last value WE wrote (the assist's own writes).
|
||||
var assisted = {};
|
||||
|
||||
var MONTANT_SEL = 'input[name$="[field_montant][0][value]"]';
|
||||
var HT_NAME = 'field_montant_ht[0][value]';
|
||||
|
||||
function round2(x) {
|
||||
return Math.round(x * 100) / 100;
|
||||
}
|
||||
|
||||
function eur(x) {
|
||||
return (x < 0 ? '-' : '') + Math.abs(x).toFixed(2).replace('.', ',') + ' €';
|
||||
}
|
||||
|
||||
function widget() {
|
||||
return form.querySelector('.field--name-field-repartition');
|
||||
}
|
||||
|
||||
function amounts() {
|
||||
var w = widget();
|
||||
if (!w) {
|
||||
return [];
|
||||
}
|
||||
return Array.prototype.slice.call(w.querySelectorAll(MONTANT_SEL));
|
||||
}
|
||||
|
||||
function htValue() {
|
||||
var ht = form.querySelector('[name="' + HT_NAME + '"]');
|
||||
var v = ht ? parseFloat(ht.value) : NaN;
|
||||
return isNaN(v) ? NaN : v;
|
||||
}
|
||||
|
||||
// The écart lives inside the table's own th.field-label ("Répartition"
|
||||
// heading cell), next to the Collapse-all menu -- requested layout:
|
||||
// everything the réparation needs to know, in the table's title bar.
|
||||
// That puts it INSIDE the widget's AJAX re-render zone (every
|
||||
// add/remove rebuilds the table), so the element gets wiped and
|
||||
// recreated on each refresh -- exactly what refresh() below does.
|
||||
function ecartRow() {
|
||||
var existing = form.querySelector('.figli-repartition-ecart');
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
var w = widget();
|
||||
var th = w ? w.querySelector('th.field-label') : null;
|
||||
if (!th) {
|
||||
return null;
|
||||
}
|
||||
var span = document.createElement('span');
|
||||
span.className = 'figli-repartition-ecart';
|
||||
span.innerHTML = '<span class="figli-repartition-ecart-sum"></span> · <span class="figli-repartition-ecart-val"></span>';
|
||||
th.appendChild(span);
|
||||
return span;
|
||||
}
|
||||
|
||||
function writeAssisted(input, value) {
|
||||
var s = value.toFixed(2);
|
||||
if (input.value !== s) {
|
||||
input.value = s;
|
||||
}
|
||||
assisted[input.name] = s;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
var row = ecartRow();
|
||||
var inputs = amounts();
|
||||
if (row) {
|
||||
// Hidden until the first répartition exists, wiped with the rest
|
||||
// of the table on add/remove -- re-created on the next refresh.
|
||||
row.style.display = inputs.length ? '' : 'none';
|
||||
}
|
||||
if (!inputs.length) {
|
||||
return;
|
||||
}
|
||||
var ht = htValue();
|
||||
|
||||
// Classify + mark figées + sum their values.
|
||||
var sommeFigees = 0;
|
||||
var libres = [];
|
||||
inputs.forEach(function (input) {
|
||||
var value = input.value.trim();
|
||||
var figee = !!figees[input.name]
|
||||
|| (value !== '' && String(assisted[input.name]) !== value);
|
||||
var item = input.closest('.form-item');
|
||||
if (item) {
|
||||
item.classList.toggle('figli-repartition-locked', figee);
|
||||
}
|
||||
if (figee) {
|
||||
var v = parseFloat(value);
|
||||
if (!isNaN(v)) {
|
||||
sommeFigees = round2(sommeFigees + v);
|
||||
}
|
||||
}
|
||||
else {
|
||||
libres.push(input);
|
||||
}
|
||||
});
|
||||
|
||||
// Distribute: sequential split, each row gets round(restant /
|
||||
// restantes), remainder carried by the later rows -- sum exact to
|
||||
// the centime (a naive HT/n leaves 0,03€ of écart on 3 rows).
|
||||
var restant = isNaN(ht) ? NaN : round2(ht - sommeFigees);
|
||||
libres.forEach(function (input, i) {
|
||||
if (isNaN(restant)) {
|
||||
// No HT to distribute yet: clear our own previous writes only.
|
||||
if (input.value !== '' && String(assisted[input.name]) === input.value) {
|
||||
input.value = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
var n = libres.length - i;
|
||||
var v = restant <= 0 ? 0 : round2(restant / n);
|
||||
writeAssisted(input, v);
|
||||
restant = round2(restant - v);
|
||||
});
|
||||
|
||||
// Live écart, mirroring the server exactly (same rounding, same
|
||||
// 0.01 tolerance as figli_compta_ledger_node_presave()).
|
||||
var somme = 0;
|
||||
inputs.forEach(function (input) {
|
||||
var v = parseFloat(input.value);
|
||||
if (!isNaN(v)) {
|
||||
somme = round2(somme + v);
|
||||
}
|
||||
});
|
||||
var ecart = isNaN(ht) ? NaN : round2(ht - somme);
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
row.querySelector('.figli-repartition-ecart-sum').textContent = 'Réparti : ' + eur(somme);
|
||||
var val = row.querySelector('.figli-repartition-ecart-val');
|
||||
val.textContent = isNaN(ecart) ? 'Écart : —' : 'Écart : ' + eur(ecart);
|
||||
row.classList.toggle('is-ok', !isNaN(ecart) && Math.abs(ecart) <= 0.01);
|
||||
row.classList.toggle('is-ko', !isNaN(ecart) && Math.abs(ecart) > 0.01);
|
||||
}
|
||||
|
||||
// One delegated listener on the form (survives every Paragraphs
|
||||
// re-render, unlike per-input listeners). Programmatic writes above
|
||||
// fire no event, so any 'input' seen here IS a human edit: lock it
|
||||
// -- or unlock when cleared, so an emptied row becomes free again.
|
||||
form.addEventListener('input', function (e) {
|
||||
var t = e.target;
|
||||
if (!t.matches) {
|
||||
return;
|
||||
}
|
||||
if (t.matches(MONTANT_SEL)) {
|
||||
if (t.value.trim() === '') {
|
||||
delete figees[t.name];
|
||||
}
|
||||
else {
|
||||
figees[t.name] = true;
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
else if (t.name === HT_NAME) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
return { refresh: refresh };
|
||||
}
|
||||
})(Drupal, once);
|
||||
|
||||
Reference in New Issue
Block a user