390 lines
18 KiB
PHP
390 lines
18 KiB
PHP
<?php
|
||
/**
|
||
* MSIN-4512 — UI kit MS1 (mockup A : Disponibles ↔ Sélectionnés + drag).
|
||
* Pas de métier PDF ici — données + names passés par le panneau.
|
||
*/
|
||
|
||
if (!function_exists('fxMs1UiEsc')) {
|
||
|
||
function fxMs1UiEsc($str) {
|
||
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
|
||
/**
|
||
* Marqueur kit — root de chaque écran migré.
|
||
* Grep : data-ms1-ui-kit | MS1-UI-KIT | data-ms1-ui-screen
|
||
*/
|
||
function fxMs1UiKitRootAttrs($tabCfg = []) {
|
||
$strScreen = trim((string)($tabCfg['screen'] ?? ''));
|
||
$strTicket = trim((string)($tabCfg['ticket'] ?? ''));
|
||
$mixPatterns = $tabCfg['patterns'] ?? '';
|
||
if (is_array($mixPatterns)) {
|
||
$strPatterns = implode(',', array_map('strval', $mixPatterns));
|
||
} else {
|
||
$strPatterns = trim((string)$mixPatterns);
|
||
}
|
||
$strExtraClass = trim((string)($tabCfg['class'] ?? ''));
|
||
$strClass = 'ms1-ui-chooser';
|
||
if ($strExtraClass !== '') {
|
||
$strClass .= ' ' . $strExtraClass;
|
||
}
|
||
|
||
$str = 'class="' . fxMs1UiEsc($strClass) . '" data-ms1-ui-kit="1"';
|
||
if ($strScreen !== '') {
|
||
$str .= ' data-ms1-ui-screen="' . fxMs1UiEsc($strScreen) . '"';
|
||
}
|
||
if ($strPatterns !== '') {
|
||
$str .= ' data-ms1-ui-patterns="' . fxMs1UiEsc($strPatterns) . '"';
|
||
}
|
||
if ($strTicket !== '') {
|
||
$str .= ' data-ms1-ui-ticket="' . fxMs1UiEsc($strTicket) . '"';
|
||
}
|
||
return $str;
|
||
}
|
||
|
||
function fxMs1UiKitPhpMarker($strScreen, $strPatterns, $strTicket = '') {
|
||
$str = '// MS1-UI-KIT — ' . $strScreen . ' — ' . $strPatterns;
|
||
if ($strTicket !== '') {
|
||
$str .= ' — ' . $strTicket;
|
||
}
|
||
return $str;
|
||
}
|
||
|
||
/** P4 — Appliquer à toutes. */
|
||
function fxMs1UiRenderApplyAllButton($tabCfg = []) {
|
||
$strLabel = (string)($tabCfg['label'] ?? '');
|
||
$strExtra = trim((string)($tabCfg['class'] ?? ''));
|
||
$strAttrs = (string)($tabCfg['attrs'] ?? '');
|
||
$strClass = 'btn btn-outline-secondary btn-sm ms1-ui-chooser-apply-all';
|
||
if ($strExtra !== '') {
|
||
$strClass .= ' ' . $strExtra;
|
||
}
|
||
return '<button type="button" class="' . fxMs1UiEsc($strClass) . '"'
|
||
. ($strAttrs !== '' ? ' ' . $strAttrs : '')
|
||
. '>' . fxMs1UiEsc($strLabel) . '</button>';
|
||
}
|
||
|
||
/**
|
||
* P5 — Orientation / choix exclusif (segmented pills, mockup A).
|
||
* options[] : value, label, checked
|
||
*/
|
||
function fxMs1UiRenderSegmented($tabCfg = []) {
|
||
$strName = (string)($tabCfg['name'] ?? 'ms1_ui_seg');
|
||
$strLegend = (string)($tabCfg['legend'] ?? '');
|
||
$strLegendHtml = (string)($tabCfg['legend_html'] ?? '');
|
||
$strHint = (string)($tabCfg['hint'] ?? '');
|
||
$strRadioClass = trim((string)($tabCfg['radio_class'] ?? ''));
|
||
$tabOptions = is_array($tabCfg['options'] ?? null) ? $tabCfg['options'] : [];
|
||
|
||
ob_start();
|
||
?>
|
||
<div class="ms1-ui-chooser-seg-wrap mb-3">
|
||
<?php if ($strLegendHtml !== '' || $strLegend !== '') { ?>
|
||
<div class="ms1-ui-chooser-seg-legend">
|
||
<?php echo $strLegendHtml !== '' ? $strLegendHtml : fxMs1UiEsc($strLegend); ?>
|
||
</div>
|
||
<?php } ?>
|
||
<div class="ms1-ui-chooser-seg" role="radiogroup">
|
||
<?php foreach ($tabOptions as $opt) {
|
||
$strVal = (string)($opt['value'] ?? '');
|
||
$strOptLabel = (string)($opt['label'] ?? $strVal);
|
||
$blnChecked = !empty($opt['checked']);
|
||
$strCbClass = 'ms1-ui-chooser-radio';
|
||
if ($strRadioClass !== '') {
|
||
$strCbClass .= ' ' . $strRadioClass;
|
||
}
|
||
?>
|
||
<label class="ms1-ui-chooser-seg-opt<?php echo $blnChecked ? ' is-active' : ''; ?>">
|
||
<input type="radio"
|
||
class="<?php echo fxMs1UiEsc($strCbClass); ?>"
|
||
name="<?php echo fxMs1UiEsc($strName); ?>"
|
||
value="<?php echo fxMs1UiEsc($strVal); ?>"
|
||
<?php echo $blnChecked ? 'checked' : ''; ?>>
|
||
<span><?php echo fxMs1UiEsc($strOptLabel); ?></span>
|
||
</label>
|
||
<?php } ?>
|
||
</div>
|
||
<?php if ($strHint !== '') { ?>
|
||
<p class="text-muted small mb-0 mt-1"><?php echo fxMs1UiEsc($strHint); ?></p>
|
||
<?php } ?>
|
||
</div>
|
||
<?php
|
||
return ob_get_clean();
|
||
}
|
||
|
||
/** Alias legacy → segmented. */
|
||
function fxMs1UiRenderRadios($tabCfg = []) {
|
||
return fxMs1UiRenderSegmented($tabCfg);
|
||
}
|
||
|
||
/**
|
||
* P6 — Fit / budget (barre + compteur en entête).
|
||
* MSIN-4550 — le message dépassement vit sous la liste Sélectionnés (dual panel), pas ici.
|
||
*/
|
||
function fxMs1UiRenderFitFeedback($tabCfg = []) {
|
||
$strHint = (string)($tabCfg['hint'] ?? '');
|
||
$strHintTpl = (string)($tabCfg['hint_tpl'] ?? '');
|
||
$intUsed = (int)($tabCfg['used'] ?? 0);
|
||
$intBudget = (int)($tabCfg['budget'] ?? 0);
|
||
$intPct = ($intBudget > 0) ? (int)min(100, round(100 * $intUsed / $intBudget)) : 0;
|
||
|
||
ob_start();
|
||
?>
|
||
<div class="ms1-ui-chooser-fit mb-3">
|
||
<div class="ms1-ui-chooser-fit-bar" aria-hidden="true">
|
||
<span class="ms1-ui-chooser-fit-bar-fill" style="width:<?php echo $intPct; ?>%;"></span>
|
||
</div>
|
||
<p class="ms1-ui-chooser-fit-hint text-muted small mb-0"
|
||
data-hint-tpl="<?php echo fxMs1UiEsc($strHintTpl !== '' ? $strHintTpl : $strHint); ?>">
|
||
<?php echo fxMs1UiEsc($strHint); ?>
|
||
</p>
|
||
<p class="ms1-ui-chooser-fit-status small mb-0" aria-live="polite"></p>
|
||
</div>
|
||
<?php
|
||
return ob_get_clean();
|
||
}
|
||
|
||
/** Une ligne sélectionnée (drag + retirer). fixed=true → toujours là (drag OK, pas de ×). */
|
||
function fxMs1UiRenderDualSelectedItem($tabCfg = []) {
|
||
$strKey = (string)($tabCfg['key'] ?? '');
|
||
$strLabel = (string)($tabCfg['label'] ?? $strKey);
|
||
$intW = (int)($tabCfg['width'] ?? 0);
|
||
$strInputName = (string)($tabCfg['input_name'] ?? '');
|
||
$strRemove = (string)($tabCfg['label_remove'] ?? '×');
|
||
$blnFixed = !empty($tabCfg['fixed']);
|
||
|
||
$strLiClass = 'ms1-ui-chooser-selected-item';
|
||
if ($blnFixed) {
|
||
$strLiClass .= ' is-fixed';
|
||
}
|
||
|
||
ob_start();
|
||
?>
|
||
<li class="<?php echo fxMs1UiEsc($strLiClass); ?>"
|
||
data-item-key="<?php echo fxMs1UiEsc($strKey); ?>"
|
||
<?php if ($blnFixed) { ?>data-fixed="1"<?php } ?>
|
||
<?php if ($intW > 0) { ?>data-item-width="<?php echo $intW; ?>"<?php } ?>>
|
||
<span class="ms1-ui-chooser-drag-handle" aria-hidden="true" title="⋮⋮">
|
||
<i class="fa fa-bars" aria-hidden="true"></i>
|
||
</span>
|
||
<?php if ($blnFixed) { ?>
|
||
<span class="ms1-ui-chooser-fixed-lock" aria-hidden="true" title="">
|
||
<i class="fa fa-lock" aria-hidden="true"></i>
|
||
</span>
|
||
<?php } ?>
|
||
<span class="ms1-ui-chooser-selected-label"><?php echo fxMs1UiEsc($strLabel); ?></span>
|
||
<?php if (!$blnFixed) { ?>
|
||
<button type="button"
|
||
class="ms1-ui-chooser-selected-remove"
|
||
aria-label="<?php echo fxMs1UiEsc($strRemove); ?>"
|
||
title="<?php echo fxMs1UiEsc($strRemove); ?>">×</button>
|
||
<?php } ?>
|
||
<?php if ($strInputName !== '') { ?>
|
||
<input type="hidden" name="<?php echo fxMs1UiEsc($strInputName); ?>" value="<?php echo fxMs1UiEsc($strKey); ?>">
|
||
<?php } ?>
|
||
</li>
|
||
<?php
|
||
return ob_get_clean();
|
||
}
|
||
|
||
/**
|
||
* P2+P3 mockup A — dual panel Disponibles (chips) ↔ Sélectionnés (drag).
|
||
*
|
||
* available[] / selected[] : key, label, width
|
||
* fixed[] optionnel : key, label (toujours en tête de l’ordre, non soumis)
|
||
* groups optionnel : [ ['label'=>, 'keys'=>[...]], … ] pour sous-titres chips
|
||
*/
|
||
function fxMs1UiRenderDualPanel($tabCfg = []) {
|
||
$strSectionId = (string)($tabCfg['section_id'] ?? '');
|
||
$strAvailTitle = (string)($tabCfg['available_title'] ?? '');
|
||
$strSelectedTitle = (string)($tabCfg['selected_title'] ?? '');
|
||
$strSearchPh = (string)($tabCfg['search_placeholder'] ?? '');
|
||
$strEmpty = (string)($tabCfg['empty_label'] ?? '');
|
||
$strRemove = (string)($tabCfg['label_remove'] ?? '');
|
||
$strInputNameTpl = (string)($tabCfg['input_name_tpl'] ?? 'items[%s][]');
|
||
$strInputName = sprintf($strInputNameTpl, $strSectionId);
|
||
$tabAvailable = is_array($tabCfg['available'] ?? null) ? $tabCfg['available'] : [];
|
||
$tabSelected = is_array($tabCfg['selected'] ?? null) ? $tabCfg['selected'] : [];
|
||
$tabFixed = is_array($tabCfg['fixed'] ?? null) ? $tabCfg['fixed'] : [];
|
||
$tabGroups = is_array($tabCfg['groups'] ?? null) ? $tabCfg['groups'] : [];
|
||
|
||
$tabSelectedKeys = [];
|
||
foreach ($tabSelected as $opt) {
|
||
$strK = (string)($opt['key'] ?? '');
|
||
if ($strK !== '') {
|
||
$tabSelectedKeys[$strK] = true;
|
||
}
|
||
}
|
||
|
||
// Index available by key for chip render
|
||
$tabAvailByKey = [];
|
||
foreach ($tabAvailable as $opt) {
|
||
$strK = (string)($opt['key'] ?? '');
|
||
if ($strK !== '') {
|
||
$tabAvailByKey[$strK] = $opt;
|
||
}
|
||
}
|
||
|
||
ob_start();
|
||
?>
|
||
<div class="ms1-ui-chooser-dual" data-section-id="<?php echo fxMs1UiEsc($strSectionId); ?>">
|
||
<div class="ms1-ui-chooser-available">
|
||
<div class="ms1-ui-chooser-panel-title"><?php echo fxMs1UiEsc($strAvailTitle); ?></div>
|
||
<?php if ($strSearchPh !== '') { ?>
|
||
<input type="search"
|
||
class="form-control form-control-sm ms1-ui-chooser-search"
|
||
placeholder="<?php echo fxMs1UiEsc($strSearchPh); ?>"
|
||
autocomplete="off">
|
||
<?php } ?>
|
||
<div class="ms1-ui-chooser-chips">
|
||
<?php
|
||
if (!empty($tabGroups)) {
|
||
foreach ($tabGroups as $tabG) {
|
||
$strGLabel = (string)($tabG['label'] ?? '');
|
||
$tabKeys = is_array($tabG['keys'] ?? null) ? $tabG['keys'] : [];
|
||
if ($strGLabel !== '') {
|
||
echo '<div class="ms1-ui-chooser-chip-group-label">' . fxMs1UiEsc($strGLabel) . '</div>';
|
||
}
|
||
foreach ($tabKeys as $strK) {
|
||
$strK = (string)$strK;
|
||
if (!isset($tabAvailByKey[$strK])) {
|
||
continue;
|
||
}
|
||
$opt = $tabAvailByKey[$strK];
|
||
$blnUsed = !empty($tabSelectedKeys[$strK]);
|
||
$intW = (int)($opt['width'] ?? 0);
|
||
$strLab = (string)($opt['label'] ?? $strK);
|
||
?>
|
||
<button type="button"
|
||
class="ms1-ui-chooser-chip<?php echo $blnUsed ? ' is-used' : ''; ?>"
|
||
data-item-key="<?php echo fxMs1UiEsc($strK); ?>"
|
||
data-item-label="<?php echo fxMs1UiEsc($strLab); ?>"
|
||
<?php if ($intW > 0) { ?>data-item-width="<?php echo $intW; ?>"<?php } ?>
|
||
<?php echo $blnUsed ? 'hidden' : ''; ?>>
|
||
<?php echo fxMs1UiEsc($strLab); ?>
|
||
</button>
|
||
<?php
|
||
}
|
||
}
|
||
} else {
|
||
foreach ($tabAvailable as $opt) {
|
||
$strK = (string)($opt['key'] ?? '');
|
||
if ($strK === '') {
|
||
continue;
|
||
}
|
||
$blnUsed = !empty($tabSelectedKeys[$strK]);
|
||
$intW = (int)($opt['width'] ?? 0);
|
||
$strLab = (string)($opt['label'] ?? $strK);
|
||
?>
|
||
<button type="button"
|
||
class="ms1-ui-chooser-chip<?php echo $blnUsed ? ' is-used' : ''; ?>"
|
||
data-item-key="<?php echo fxMs1UiEsc($strK); ?>"
|
||
data-item-label="<?php echo fxMs1UiEsc($strLab); ?>"
|
||
<?php if ($intW > 0) { ?>data-item-width="<?php echo $intW; ?>"<?php } ?>
|
||
<?php echo $blnUsed ? 'hidden' : ''; ?>>
|
||
<?php echo fxMs1UiEsc($strLab); ?>
|
||
</button>
|
||
<?php
|
||
}
|
||
}
|
||
?>
|
||
</div>
|
||
</div>
|
||
<div class="ms1-ui-chooser-selected">
|
||
<div class="ms1-ui-chooser-panel-title"><?php echo fxMs1UiEsc($strSelectedTitle); ?></div>
|
||
<ol class="ms1-ui-chooser-order list-unstyled mb-0"
|
||
data-section-id="<?php echo fxMs1UiEsc($strSectionId); ?>"
|
||
data-input-name="<?php echo fxMs1UiEsc($strInputName); ?>">
|
||
<?php
|
||
$blnAnyOptional = false;
|
||
foreach ($tabFixed as $opt) {
|
||
$strK = (string)($opt['key'] ?? '');
|
||
if ($strK === '') {
|
||
continue;
|
||
}
|
||
echo fxMs1UiRenderDualSelectedItem([
|
||
'key' => $strK,
|
||
'label' => (string)($opt['label'] ?? $strK),
|
||
'width' => 0,
|
||
'fixed' => true,
|
||
'input_name' => $strInputName,
|
||
]);
|
||
}
|
||
foreach ($tabSelected as $opt) {
|
||
$strK = (string)($opt['key'] ?? '');
|
||
if ($strK === '') {
|
||
continue;
|
||
}
|
||
$blnFixedItem = !empty($opt['fixed']);
|
||
if (!$blnFixedItem) {
|
||
$blnAnyOptional = true;
|
||
}
|
||
echo fxMs1UiRenderDualSelectedItem([
|
||
'key' => $strK,
|
||
'label' => (string)($opt['label'] ?? $strK),
|
||
'width' => $blnFixedItem ? 0 : (int)($opt['width'] ?? 0),
|
||
'input_name' => $strInputName,
|
||
'label_remove' => $strRemove,
|
||
'fixed' => $blnFixedItem,
|
||
]);
|
||
}
|
||
?>
|
||
</ol>
|
||
<p class="ms1-ui-chooser-order-empty text-muted small mb-0"<?php echo $blnAnyOptional ? ' hidden' : ''; ?>>
|
||
<?php echo fxMs1UiEsc($strEmpty); ?>
|
||
</p>
|
||
<?php // MSIN-4550 — message trop de colonnes sous la liste choisie (pas en entête globale). ?>
|
||
<p class="ms1-ui-chooser-fit-warn text-danger small mb-0" hidden></p>
|
||
</div>
|
||
</div>
|
||
<?php
|
||
return ob_get_clean();
|
||
}
|
||
|
||
/**
|
||
* Section épreuve : titre + dual panel (+ apply-all optionnel dans le head).
|
||
* Pas de boîte « config commune » — apply-all sur l’épreuve source.
|
||
*/
|
||
function fxMs1UiRenderChooserSection($tabCfg = []) {
|
||
$strSectionId = (string)($tabCfg['section_id'] ?? '');
|
||
$strTitle = (string)($tabCfg['title'] ?? '');
|
||
$strTitleSuffix = (string)($tabCfg['title_suffix_html'] ?? '');
|
||
$strExtraClass = trim((string)($tabCfg['class'] ?? ''));
|
||
$tabApply = $tabCfg['apply_all'] ?? null;
|
||
$tabDual = $tabCfg['dual'] ?? null;
|
||
$strBodyExtra = (string)($tabCfg['body_html'] ?? '');
|
||
|
||
$strClass = 'ms1-ui-chooser-section';
|
||
if ($strExtraClass !== '') {
|
||
$strClass .= ' ' . $strExtraClass;
|
||
}
|
||
|
||
ob_start();
|
||
?>
|
||
<section class="<?php echo fxMs1UiEsc($strClass); ?>" data-section-id="<?php echo fxMs1UiEsc($strSectionId); ?>">
|
||
<div class="ms1-ui-chooser-section-head">
|
||
<h3 class="ms1-ui-chooser-section-title">
|
||
<?php echo fxMs1UiEsc($strTitle); ?>
|
||
<?php echo $strTitleSuffix; ?>
|
||
</h3>
|
||
<?php
|
||
if (is_array($tabApply) && !empty($tabApply['label'])) {
|
||
echo fxMs1UiRenderApplyAllButton($tabApply);
|
||
}
|
||
?>
|
||
</div>
|
||
<div class="ms1-ui-chooser-section-body">
|
||
<?php
|
||
if (is_array($tabDual)) {
|
||
$tabDual['section_id'] = $strSectionId;
|
||
echo fxMs1UiRenderDualPanel($tabDual);
|
||
}
|
||
echo $strBodyExtra;
|
||
?>
|
||
</div>
|
||
</section>
|
||
<?php
|
||
return ob_get_clean();
|
||
}
|
||
}
|