/* 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) => ``; /** 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 ``; 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 ? `` : ""; 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. ? `` + `` : ``; 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 `` + `` + ``; }); return ``; }