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.
513 lines
22 KiB
JavaScript
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>`;
|
|
} |