Files
bruno 634ba8a272
CI / lint (push) Successful in 2m43s
CI / security (push) Successful in 1m33s
CI / test (push) Canceled after 0s
CI / build (push) Canceled after 0s
CI / e2e (push) Canceled after 0s
feat: éditeur tableur — grille A → Z par blocs, barre de menus Sheets, panneau de couleurs, création .xlsx (#180 → #186)
Restaure et termine le lot tableur resté non committé. Il n'existait que dans un
`git stash` (24 fichiers suivis) et en fichiers non suivis (menus.js, formula.js,
color-picker.js, tests, fiches) : la 2.53.3 livrée ne le contenait donc pas. Le
stash, créé avant le commit BUG-108, n'a jamais été restauré.

#180 Redimensionnement au curseur (en-tête de bord miroité + classe de repaint),
grille thémée sur les 15 thèmes et les modes contraste élevé / sépia, menu
Fichier au niveau des onglets, barre épinglée pleine largeur.

#181 Modèle de saisie Google Sheets : sélection ≠ édition (caret masqué sans
quitter contenteditable, double-clic / F2 / première frappe qui remplace,
Entrée contextuelle, Échap qui restaure) + inventaire priorisé des écarts.

#182 Désélection fiable (clic simple sans dépendre du focus), contours de plage
non empilés (sélecteur de classes sans point), couleurs texte/fond sur une
plage, clic droit qui préserve la sélection multiple.

#183 Panneau de couleurs façon Google Sheets : palette 8 × 10, STANDARD,
PERSONNALISÉ, coche selon la luminance, sortie `#rrggbb` (une valeur HSL était
rejetée par normHex).

#184 Peinture de format complète (toutes propriétés, source sans format =
réinitialisation), sélection multi-lignes/colonnes depuis les marges, grille
étendue : colonnes A → Z d'emblée, lignes ajoutées PAR BLOCS DE 100 au
défilement jusqu'à 1000 — matérialiser 1000 lignes d'un coup = ~26 000 cellules
câblées par feuille, ce qui épuisait le tas de la suite JSDOM ; index de
cellules `ref → td`, court-circuits formule/styles, marqueur data-wired.

#185 Barre de menus : les 10 menus Google Sheets (161 entrées, 125 câblées, 36
annoncées indisponibles), ruban façon Sheets, grille unie 1 px dérivée du thème.

#186 « Créer un fichier » propose .xlsx et construit un vrai classeur OPC
(openpyxl) au lieu d'une charge utile texte illisible.

Corrections trouvées en restaurant et en exerçant le lot :
- fuite mémoire : écouteur `click` anonyme posé sur #content-area à chaque
  rendu, jamais retiré — sa fermeture retenait la grille précédente en entier ;
- sorties Markdown / HTML / Imprimer de la barre de menus inertes
  (`data-xlsx-export` jamais réparti, seul `data-xlsx-action` l'était) ;
- collision de classe `.xlsx-structure-menu` entre la barre de menus et le menu
  Structure de la barre d'outils (toute requête tombait sur un nœud masqué) ;
- curseur col/row-resize absent quand le pointeur est sur la table elle-même ;
- l'export emportait les lignes et colonnes vides du quadrillage (CSV, Markdown,
  HTML, impression) ;
- clic extérieur avalé par la grâce de 250 ms du menu contextuel (destinée au
  seul appui long tactile) ;
- une entrée indisponible laissait la barre de menus ouverte.

Tests : pytest 1595 passés / 2 ignorés ; ruff et mypy 0 erreur ; bandit 0 ;
33 suites frontend vertes (xlsx-viewer 163/163, xlsx-menus 19/19,
xlsx-formula 14/14) ; E2E complet 132 passés / 12 ignorés ; E2E xlsx-viewer
19/19.
2026-10-08 12:00:15 -04:00

513 lines
22 KiB
JavaScript

/* ObsiGate — XLSX menu bar (the ten Google Sheets menus).
*
* Pure declarative data + a string builder, in the same spirit as
* `command-bar.js`: the menu tree lives here and stays testable on its own,
* while `viewer.js` only resolves each entry's symbolic `action` against its
* own flows. No DOM here beyond `innerHTML` of the shell.
*
* Entry shape
* -----------
* { labelKey, icon, action, children, unavailable, danger, needs, sep }
* - `labelKey` i18n key of the visible label (never raw text).
* - `icon` Lucide icon name, hydrated later by `safeCreateIcons()`.
* - `action` symbolic name resolved by the viewer. An entry with no
* action — or flagged `unavailable` — is rendered honestly and
* reports that ObsiGate does not implement it: a documented
* dead end, never a silent no-op.
* - `children` a Sheets-style accordion section (Nombre, Thème, Zoom, …):
* collapsed, expanded by a click on its parent row.
* - `needs` `"editable"` (hide unless the workbook can be written) or
* `"notReadOnly"` (hide only for .xls/.ods).
* - `sep` a thin rule instead of an entry.
*
* The Fichier menu keeps the historical `#xlsx-file-btn` / `#xlsx-file-menu` /
* `#xlsx-download-btn` / `#xlsx-csv-btn` ids and the `[data-xlsx-export]`
* markers: the File menu moved from the sheet strip to the menu bar, but every
* consumer (viewer wiring, tests, E2E) keeps working untouched.
*/
import { t } from '../i18n.js';
import { escapeHtml } from '../utils.js';
const IC = (name) => `<i data-lucide="${name}" class="xlsx-menu-icon" aria-hidden="true"></i>`;
/** Google Sheets' menu bar: same order, same entries, same French labels. */
export const SHEETS_MENUS = [
{
id: "file",
labelKey: "xlsx.mbar_file",
btnId: "xlsx-file-btn",
dropId: "xlsx-file-menu",
items: [
{ labelKey: "xlsx.mi_new", icon: "file-plus", unavailable: true },
{ labelKey: "xlsx.mi_open", icon: "folder-open", unavailable: true },
{ labelKey: "xlsx.mi_import", icon: "file-input", unavailable: true },
{ labelKey: "xlsx.mi_make_copy", icon: "copy", unavailable: true },
{ labelKey: "xlsx.mi_share", icon: "share-2", action: "share" },
{ labelKey: "xlsx.mi_email", icon: "mail", unavailable: true },
{ sep: true },
{
labelKey: "xlsx.mi_download",
icon: "download",
children: [
{ labelKey: "viewer.download", icon: "download", id: "xlsx-download-btn", action: "download" },
{ labelKey: "xlsx.export_csv", icon: "file-spreadsheet", id: "xlsx-csv-btn", action: "export-csv", titleKey: "xlsx.csv_export" },
{ sep: true },
{ labelKey: "xlsx.export_md", icon: "file-text", export: "md", action: "export-md" },
{ labelKey: "xlsx.export_html", icon: "code", export: "html", action: "export-html" },
{ labelKey: "xlsx.print", icon: "printer", export: "print", action: "print" },
],
},
{ labelKey: "xlsx.mi_rename", icon: "pencil", action: "rename" },
{ labelKey: "xlsx.mi_trash", icon: "trash-2", action: "trash", danger: true },
{ labelKey: "xlsx.mi_history", icon: "history", action: "history" },
{ labelKey: "xlsx.mi_offline", icon: "cloud-off", unavailable: true },
{ sep: true },
{ labelKey: "xlsx.mi_details", icon: "info", action: "details" },
{ labelKey: "xlsx.mi_settings", icon: "settings", action: "settings" },
{ labelKey: "xlsx.print", icon: "printer", action: "print" },
],
},
{
id: "edit",
labelKey: "xlsx.mbar_edit",
items: [
{ labelKey: "xlsx.undo", icon: "undo-2", action: "undo", needs: "notReadOnly" },
{ labelKey: "xlsx.redo", icon: "redo-2", action: "redo", needs: "notReadOnly" },
{ sep: true },
{ labelKey: "xlsx.cut", icon: "scissors", action: "cut", needs: "editable" },
{ labelKey: "xlsx.copy", icon: "copy", action: "copy" },
{ labelKey: "xlsx.paste", icon: "clipboard-paste", action: "paste", needs: "editable" },
{
labelKey: "xlsx.mi_paste_special",
icon: "clipboard-paste",
needs: "editable",
children: [
{ labelKey: "xlsx.mi_paste_all", icon: "clipboard-paste", action: "paste" },
{ labelKey: "xlsx.mi_paste_values", icon: "pilcrow", action: "paste-values" },
],
},
{ sep: true },
{
labelKey: "xlsx.mi_move",
icon: "move",
needs: "editable",
children: [
{ labelKey: "xlsx.mi_move_rows", icon: "rows", unavailable: true },
{ labelKey: "xlsx.mi_move_cols", icon: "columns", unavailable: true },
],
},
{
labelKey: "xlsx.mi_delete",
icon: "trash-2",
needs: "editable",
children: [
{ labelKey: "xlsx.mi_delete_rows", icon: "rows", action: "delete-rows", danger: true },
{ labelKey: "xlsx.mi_delete_cols", icon: "columns", action: "delete-cols", danger: true },
{ sep: true },
{ labelKey: "xlsx.clear_contents", icon: "eraser", action: "clear-contents" },
],
},
{ sep: true },
{ labelKey: "xlsx.mi_select_all", icon: "check-check", action: "select-all" },
{ labelKey: "xlsx.mi_replace", icon: "replace", action: "find-replace", needs: "editable" },
],
},
{
id: "view",
labelKey: "xlsx.mbar_view",
items: [
{
labelKey: "xlsx.mi_freeze",
icon: "snowflake",
children: [
{ labelKey: "xlsx.mi_freeze_here", icon: "snowflake", action: "freeze" },
{ labelKey: "xlsx.mi_freeze_row", icon: "rows", action: "freeze-row" },
{ labelKey: "xlsx.mi_freeze_col", icon: "columns", action: "freeze-col" },
{ sep: true },
{ labelKey: "xlsx.format_unfreeze", icon: "sun", action: "unfreeze" },
],
},
{
labelKey: "xlsx.mi_show",
icon: "eye",
children: [
{ labelKey: "xlsx.mi_show_formula", icon: "chevrons-right", action: "toggle-formula-bar" },
{ labelKey: "xlsx.mi_show_grid", icon: "grid-3x3", action: "toggle-grid" },
{ labelKey: "xlsx.mi_show_headers", icon: "rows-3", action: "toggle-headers" },
{ sep: true },
{ labelKey: "xlsx.mi_show_dashboard", icon: "layout-dashboard", action: "dashboard" },
],
},
{
labelKey: "xlsx.mi_group",
icon: "group",
children: [
{ labelKey: "xlsx.mi_group_rows", icon: "rows", unavailable: true },
{ labelKey: "xlsx.mi_group_cols", icon: "columns", unavailable: true },
{ sep: true },
{ labelKey: "xlsx.mi_ungroup_rows", icon: "ungroup", unavailable: true },
{ labelKey: "xlsx.mi_ungroup_cols", icon: "ungroup", unavailable: true },
],
},
{
labelKey: "xlsx.mi_hidden_sheets",
icon: "eye-off",
children: [
{ labelKey: "xlsx.mi_hide_sheet", icon: "eye-off", unavailable: true },
{ labelKey: "xlsx.mi_unhide_sheets", icon: "eye", unavailable: true },
],
},
{
labelKey: "xlsx.mi_zoom",
icon: "zoom-in",
children: [50, 75, 90, 100, 125, 150, 200].map((z) => ({
labelKey: null,
label: `${z} %`,
icon: z === 100 ? "crosshair" : null,
action: "zoom",
arg: z,
})),
},
{ labelKey: "xlsx.mi_fullscreen", icon: "fullscreen", action: "fullscreen" },
],
},
{
id: "insert",
labelKey: "xlsx.mbar_insert",
items: [
{
labelKey: "xlsx.mi_insert_rows",
icon: "rows",
needs: "editable",
children: [
{ labelKey: "xlsx.insert_row_above", icon: "arrow-up", action: "insert-rows-above" },
{ labelKey: "xlsx.insert_row_below", icon: "arrow-down", action: "insert-rows-below" },
],
},
{
labelKey: "xlsx.mi_insert_cols",
icon: "columns",
needs: "editable",
children: [
{ labelKey: "xlsx.insert_col_left", icon: "arrow-left", action: "insert-cols-left" },
{ labelKey: "xlsx.insert_col_right", icon: "arrow-right", action: "insert-cols-right" },
],
},
{ labelKey: "xlsx.mi_insert_cells", icon: "grid-2x2-plus", action: "insert-cells", needs: "editable" },
{ sep: true },
{ labelKey: "xlsx.mi_image", icon: "image", unavailable: true },
{ labelKey: "xlsx.mi_chart", icon: "bar-chart-3", unavailable: true },
{ labelKey: "xlsx.mi_drawing", icon: "pen-tool", unavailable: true },
{ labelKey: "xlsx.link_add", icon: "link", action: "link", needs: "editable" },
{ labelKey: "xlsx.comment_add", icon: "message-square", action: "comment", needs: "editable" },
{
labelKey: "xlsx.mi_function",
icon: "function-square",
needs: "editable",
titleKey: "xlsx.mi_function_hint",
action: "function",
},
{ sep: true },
{ labelKey: "xlsx.mi_pivot", icon: "table-2", unavailable: true },
{ labelKey: "xlsx.mi_smart_chips", icon: "tags", unavailable: true },
{ labelKey: "xlsx.mi_form", icon: "clipboard-list", unavailable: true },
],
},
{
id: "format",
labelKey: "xlsx.mbar_format",
items: [
{
labelKey: "xlsx.mi_number",
icon: "hash",
needs: "editable",
children: [
{ labelKey: "xlsx.format_num_general", icon: "type", action: "numfmt", arg: "General" },
{ labelKey: "xlsx.format_num_number", icon: "hash", action: "numfmt", arg: "0.00" },
{ labelKey: "xlsx.format_num_percent", icon: "percent", action: "numfmt", arg: "0.0%" },
{ labelKey: "xlsx.format_num_currency", icon: "euro", action: "numfmt", arg: '#,##0.00 "€"' },
{ labelKey: "xlsx.format_num_date", icon: "calendar", action: "numfmt", arg: "DD/MM/YYYY" },
{ labelKey: "xlsx.format_num_text", icon: "pilcrow", action: "numfmt", arg: "@" },
],
},
{
labelKey: "xlsx.mi_theme",
icon: "swatch-book",
// Filled on demand by the viewer (the theme list is built from
// themes.js, so it must not be hard-coded here).
children: [],
action: "theme-menu",
},
{ sep: true },
{ labelKey: "xlsx.format_bold", icon: "bold", action: "bold", needs: "editable" },
{ labelKey: "xlsx.format_italic", icon: "italic", action: "italic", needs: "editable" },
{ labelKey: "xlsx.format_underline", icon: "underline", action: "underline", needs: "editable" },
{ labelKey: "xlsx.format_strike", icon: "strikethrough", action: "strike", needs: "editable" },
{
labelKey: "xlsx.mi_font_size",
icon: "type",
needs: "editable",
children: [9, 10, 11, 12, 14, 18, 24, 36, 40].map((s) => ({
labelKey: null,
label: String(s),
icon: null,
action: "fontsize",
arg: s,
})),
},
{
labelKey: "xlsx.mi_align",
icon: "align-justify",
needs: "editable",
children: [
{
labelKey: "xlsx.mi_align_h",
icon: "align-left",
children: [
{ labelKey: "xlsx.format_align_left", icon: "align-left", action: "align", arg: "left" },
{ labelKey: "xlsx.format_align_center", icon: "align-center", action: "align", arg: "center" },
{ labelKey: "xlsx.format_align_right", icon: "align-right", action: "align", arg: "right" },
{ labelKey: "xlsx.format_align_justify", icon: "align-justify", action: "align", arg: "justify" },
],
},
{
labelKey: "xlsx.mi_align_v",
icon: "align-center-vertical",
children: [
{ labelKey: "xlsx.valign_top", icon: "align-start-vertical", action: "valign", arg: "top" },
{ labelKey: "xlsx.valign_middle", icon: "align-center-vertical", action: "valign", arg: "middle" },
{ labelKey: "xlsx.valign_bottom", icon: "align-end-vertical", action: "valign", arg: "bottom" },
],
},
],
},
{
labelKey: "xlsx.mi_wrap",
icon: "wrap-text",
needs: "editable",
children: [
{ labelKey: "xlsx.mi_wrap_on", icon: "wrap-text", action: "wrap-on" },
{ labelKey: "xlsx.mi_wrap_off", icon: "remove-formatting", action: "wrap-off" },
],
},
{
labelKey: "xlsx.mi_rotation",
icon: "rotate-cw",
needs: "editable",
children: [
{ labelKey: "xlsx.rotation_0", icon: "minus", action: "rotate", arg: 0 },
{ labelKey: "xlsx.rotation_45", icon: "trending-up", action: "rotate", arg: 45 },
{ labelKey: "xlsx.rotation_315", icon: "trending-down", action: "rotate", arg: -45 },
{ labelKey: "xlsx.rotation_90", icon: "arrow-up", action: "rotate", arg: 90 },
],
},
{ sep: true },
// Tailles de colonnes / de lignes — même emplacement que Google Sheets
// (Format ▸ Redimensionner) et Excel (Format ▸ Colonne / Ligne) :
// saisie manuelle (unités Excel : caractères / points) + ajustement
// automatique au contenu (AutoFit / « Fit to data »).
{
labelKey: "xlsx.mi_resize",
icon: "move-horizontal",
needs: "editable",
children: [
{ labelKey: "xlsx.format_col_width", icon: "move-horizontal", action: "col-width" },
{ labelKey: "xlsx.format_col_fit", icon: "maximize-2", action: "col-fit" },
{ labelKey: "xlsx.format_row_height", icon: "move-vertical", action: "row-height" },
{ labelKey: "xlsx.format_row_fit", icon: "maximize-2", action: "row-fit" },
],
},
{ sep: true },
{ labelKey: "xlsx.mi_conditional", icon: "git-compare", unavailable: true },
{
labelKey: "xlsx.mi_banded",
icon: "rows-3",
needs: "editable",
titleKey: "xlsx.mi_banded_hint",
action: "banded",
},
{ labelKey: "xlsx.format_clear", icon: "remove-formatting", action: "clear-format", needs: "editable" },
],
},
{
id: "data",
labelKey: "xlsx.mbar_data",
items: [
{
labelKey: "xlsx.mi_sort",
icon: "arrow-up-down",
children: [
{ labelKey: "xlsx.sort_asc", icon: "arrow-up", action: "sort-asc" },
{ labelKey: "xlsx.sort_desc", icon: "arrow-down", action: "sort-desc" },
{ sep: true },
{ labelKey: "xlsx.sort_reset", icon: "rotate-ccw", action: "sort-reset" },
],
},
{ labelKey: "xlsx.mi_filter", icon: "filter", titleKey: "xlsx.mi_filter_hint", action: "filter-column" },
{ sep: true },
{ labelKey: "xlsx.mi_validation", icon: "list-checks", unavailable: true },
{ labelKey: "xlsx.mi_named", icon: "bookmark", action: "named-ranges" },
{ labelKey: "xlsx.mi_protect", icon: "lock", unavailable: true },
{ sep: true },
{
labelKey: "xlsx.mi_cleanup",
icon: "wand-2",
needs: "editable",
children: [
{ labelKey: "xlsx.mi_cleanup_dupes", icon: "copy-x", action: "cleanup-dupes" },
{ labelKey: "xlsx.mi_cleanup_trim", icon: "scissors", action: "cleanup-trim" },
],
},
{
labelKey: "xlsx.mi_split",
icon: "columns-2",
needs: "editable",
titleKey: "xlsx.mi_split_hint",
action: "split-text",
},
],
},
{
id: "tools",
labelKey: "xlsx.mbar_tools",
items: [
{ labelKey: "xlsx.mi_form_create", icon: "clipboard-list", unavailable: true },
{ labelKey: "xlsx.mi_spellcheck", icon: "spell-check", unavailable: true },
{
labelKey: "xlsx.mi_autocomplete",
icon: "text-cursor-input",
children: [
{ labelKey: "xlsx.mi_autocomplete_on", icon: "check", action: "autocomplete", arg: true },
{ labelKey: "xlsx.mi_autocomplete_off", icon: "x", action: "autocomplete", arg: false },
],
},
{ sep: true },
{ labelKey: "xlsx.mi_notifications", icon: "bell", unavailable: true },
{ labelKey: "xlsx.mi_macros", icon: "workflow", unavailable: true },
{ labelKey: "xlsx.mi_script_editor", icon: "code", unavailable: true },
{ sep: true },
{ labelKey: "xlsx.mi_a11y", icon: "accessibility", action: "a11y" },
],
},
{
id: "gemini",
labelKey: "xlsx.mbar_gemini",
items: [
{ labelKey: "xlsx.mi_ask_gemini", icon: "sparkles", action: "gemini" },
{ sep: true },
{ labelKey: "xlsx.mi_gem_table", icon: "table-2", action: "gemini" },
{ labelKey: "xlsx.mi_gem_formula", icon: "function-square", action: "gemini" },
{ labelKey: "xlsx.mi_gem_insights", icon: "lightbulb", action: "gemini" },
{ labelKey: "xlsx.mi_gem_chart", icon: "bar-chart-3", unavailable: true },
{ labelKey: "xlsx.mi_gem_summarize", icon: "file-text", unavailable: true },
{ sep: true },
{ labelKey: "xlsx.mi_gem_actions", icon: "wand-sparkles", action: "gemini" },
{ labelKey: "xlsx.mi_gem_more", icon: "ellipsis", action: "gemini" },
],
},
{
id: "extensions",
labelKey: "xlsx.mbar_extensions",
items: [
{ labelKey: "xlsx.mi_addons", icon: "blocks", unavailable: true },
{ labelKey: "xlsx.mi_apps_script", icon: "code", unavailable: true },
{ labelKey: "xlsx.mi_appsheet", icon: "app-window", unavailable: true },
{ labelKey: "xlsx.mi_looker", icon: "bar-chart", unavailable: true },
],
},
{
id: "help",
labelKey: "xlsx.mbar_help",
items: [
{ labelKey: "xlsx.mi_help_sheets", icon: "life-buoy", action: "help" },
{ labelKey: "xlsx.mi_training", icon: "graduation-cap", unavailable: true },
{ labelKey: "xlsx.mi_whatsnew", icon: "sparkles", action: "whatsnew" },
{ sep: true },
{ labelKey: "xlsx.mi_shortcuts", icon: "keyboard", action: "shortcuts" },
{ labelKey: "xlsx.mi_report", icon: "bug", action: "report" },
{ labelKey: "xlsx.mi_abuse", icon: "flag", unavailable: true },
],
},
];
/** Drop an entry when the workbook cannot honour it. */
const allowed = (entry, { editable, readOnly }) => {
if (entry.needs === "editable") return editable;
if (entry.needs === "notReadOnly") return !readOnly;
return true;
};
/** Label of one entry: an explicit label, else the i18n key, else nothing. */
const labelOf = (entry) => (entry.label !== undefined
? entry.label
: (entry.labelKey ? t(entry.labelKey) : ""));
/** One clickable row. `depth` > 0 = nested inside an accordion section. */
const rowHtml = (entry, depth, ctx) => {
if (entry.sep) return `<div class="xlsx-structure-sep" role="separator"></div>`;
const unavailable = entry.unavailable || (!entry.children && !entry.action && !entry.export);
const id = entry.id ? ` id="${entry.id}"` : "";
const exportAttr = entry.export ? ` data-xlsx-export="${entry.export}"` : "";
const argAttr = entry.arg !== undefined ? ` data-xlsx-arg="${escapeHtml(String(entry.arg))}"` : "";
const title = entry.titleKey ? ` title="${escapeHtml(t(entry.titleKey))}"` : "";
const cls = [
"btn-action",
"xlsx-structure-item",
"xlsx-menu-item",
unavailable ? "xlsx-menu-item-na" : "",
entry.danger ? "xlsx-context-danger" : "",
depth ? `xlsx-menu-item-d${depth}` : "",
].filter(Boolean).join(" ");
const caret = entry.children
? `<i data-lucide="chevron-right" class="xlsx-menu-caret" aria-hidden="true"></i>`
: "";
const body = entry.children
// A section may ALSO declare an action: the viewer runs it instead of
// toggling, which is how the empty « Thème » section gets filled.
? `<button type="button" class="${cls}" role="menuitem" aria-haspopup="menu" aria-expanded="false"`
+ `${entry.action ? ` data-xlsx-action="${entry.action}"` : ""}${title}${id}>`
+ `${entry.icon ? IC(entry.icon) : ""}<span class="xlsx-menu-label">${escapeHtml(labelOf(entry))}</span>${caret}</button>`
+ `<div class="xlsx-menu-sub" hidden>${entry.children
.filter((c) => allowed(c, ctx))
.map((c) => rowHtml(c, depth + 1, ctx))
.join("")}</div>`
: `<button type="button" class="${cls}" role="menuitem"`
+ `${unavailable ? ' data-xlsx-unavailable="1"' : ""}`
+ `${entry.action ? ` data-xlsx-action="${entry.action}"` : ""}`
+ `${argAttr}${exportAttr}${title}${id}>`
+ `${entry.icon ? IC(entry.icon) : ""}<span class="xlsx-menu-label">${escapeHtml(labelOf(entry))}</span></button>`;
return body;
};
/** One dropdown: the filtered rows of a menu. */
const menuItemsHtml = (menu, ctx) => menu.items
.filter((entry) => allowed(entry, ctx))
.map((entry) => rowHtml(entry, 0, ctx))
.join("");
/**
* Build the menu bar: one button per menu plus its (hidden) dropdown.
*
* @param {object} ctx
* @param {boolean} ctx.editable - .xlsx that can be written
* @param {boolean} ctx.readOnly - .xls / .ods
* @returns {string} the `.xlsx-menubar` markup
*/
export function buildMenuBar({ editable = true, readOnly = false } = {}) {
const ctx = { editable, readOnly };
const bars = SHEETS_MENUS.map((menu) => {
const btnId = menu.btnId || `xlsx-menu-btn-${menu.id}`;
const dropId = menu.dropId || `xlsx-menu-${menu.id}`;
const label = escapeHtml(t(menu.labelKey));
return `<span class="xlsx-menuwrap${menu.id === "gemini" ? " xlsx-menuwrap-gemini" : ""}">`
+ `<button type="button" class="xlsx-menubtn" id="${btnId}" role="menuitem"`
+ ` aria-haspopup="menu" aria-expanded="false" aria-controls="${dropId}"`
+ ` data-xlsx-menu="${menu.id}">${label}</button>`
+ `<div class="xlsx-menudrop" id="${dropId}" role="menu"`
+ ` aria-label="${label}" style="display:none">`
+ `${menuItemsHtml(menu, ctx)}`
+ `</div></span>`;
});
return `<div class="xlsx-menubar" role="menubar" aria-label="${escapeHtml(t("xlsx.mbar_label"))}">`
+ `${bars.join("")}</div>`;
}